Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/django/contrib/admindocs/utils.py: 14%
114 statements
« prev ^ index » next coverage.py v6.4.4, created at 2023-07-17 14:22 -0600
« prev ^ index » next coverage.py v6.4.4, created at 2023-07-17 14:22 -0600
1"Misc. utility functions/classes for admin documentation generator."
3import re
4from email.errors import HeaderParseError
5from email.parser import HeaderParser
6from inspect import cleandoc
8from django.urls import reverse
9from django.utils.regex_helper import _lazy_re_compile
10from django.utils.safestring import mark_safe
12try:
13 import docutils.core
14 import docutils.nodes
15 import docutils.parsers.rst.roles
16except ImportError:
17 docutils_is_available = False
18else:
19 docutils_is_available = True
22def get_view_name(view_func):
23 if hasattr(view_func, "view_class"):
24 klass = view_func.view_class
25 return f"{klass.__module__}.{klass.__qualname__}"
26 mod_name = view_func.__module__
27 view_name = getattr(view_func, "__qualname__", view_func.__class__.__name__)
28 return mod_name + "." + view_name
31def parse_docstring(docstring):
32 """
33 Parse out the parts of a docstring. Return (title, body, metadata).
34 """
35 if not docstring:
36 return "", "", {}
37 docstring = cleandoc(docstring)
38 parts = re.split(r"\n{2,}", docstring)
39 title = parts[0]
40 if len(parts) == 1:
41 body = ""
42 metadata = {}
43 else:
44 parser = HeaderParser()
45 try:
46 metadata = parser.parsestr(parts[-1])
47 except HeaderParseError:
48 metadata = {}
49 body = "\n\n".join(parts[1:])
50 else:
51 metadata = dict(metadata.items())
52 if metadata:
53 body = "\n\n".join(parts[1:-1])
54 else:
55 body = "\n\n".join(parts[1:])
56 return title, body, metadata
59def parse_rst(text, default_reference_context, thing_being_parsed=None):
60 """
61 Convert the string from reST to an XHTML fragment.
62 """
63 overrides = {
64 "doctitle_xform": True,
65 "initial_header_level": 3,
66 "default_reference_context": default_reference_context,
67 "link_base": reverse("django-admindocs-docroot").rstrip("/"),
68 "raw_enabled": False,
69 "file_insertion_enabled": False,
70 }
71 thing_being_parsed = thing_being_parsed and "<%s>" % thing_being_parsed
72 # Wrap ``text`` in some reST that sets the default role to ``cmsreference``,
73 # then restores it.
74 source = """
75.. default-role:: cmsreference
77%s
79.. default-role::
80"""
81 parts = docutils.core.publish_parts(
82 source % text,
83 source_path=thing_being_parsed,
84 destination_path=None,
85 writer_name="html",
86 settings_overrides=overrides,
87 )
88 return mark_safe(parts["fragment"])
91#
92# reST roles
93#
94ROLES = {
95 "model": "%s/models/%s/",
96 "view": "%s/views/%s/",
97 "template": "%s/templates/%s/",
98 "filter": "%s/filters/#%s",
99 "tag": "%s/tags/#%s",
100}
103def create_reference_role(rolename, urlbase):
104 def _role(name, rawtext, text, lineno, inliner, options=None, content=None):
105 if options is None:
106 options = {}
107 node = docutils.nodes.reference(
108 rawtext,
109 text,
110 refuri=(
111 urlbase
112 % (
113 inliner.document.settings.link_base,
114 text.lower(),
115 )
116 ),
117 **options,
118 )
119 return [node], []
121 docutils.parsers.rst.roles.register_canonical_role(rolename, _role)
124def default_reference_role(
125 name, rawtext, text, lineno, inliner, options=None, content=None
126):
127 if options is None:
128 options = {}
129 context = inliner.document.settings.default_reference_context
130 node = docutils.nodes.reference(
131 rawtext,
132 text,
133 refuri=(
134 ROLES[context]
135 % (
136 inliner.document.settings.link_base,
137 text.lower(),
138 )
139 ),
140 **options,
141 )
142 return [node], []
145if docutils_is_available: 145 ↛ 146line 145 didn't jump to line 146, because the condition on line 145 was never true
146 docutils.parsers.rst.roles.register_canonical_role(
147 "cmsreference", default_reference_role
148 )
150 for name, urlbase in ROLES.items():
151 create_reference_role(name, urlbase)
153# Match the beginning of a named or unnamed group.
154named_group_matcher = _lazy_re_compile(r"\(\?P(<\w+>)")
155unnamed_group_matcher = _lazy_re_compile(r"\(")
158def replace_named_groups(pattern):
159 r"""
160 Find named groups in `pattern` and replace them with the group name. E.g.,
161 1. ^(?P<a>\w+)/b/(\w+)$ ==> ^<a>/b/(\w+)$
162 2. ^(?P<a>\w+)/b/(?P<c>\w+)/$ ==> ^<a>/b/<c>/$
163 3. ^(?P<a>\w+)/b/(\w+) ==> ^<a>/b/(\w+)
164 4. ^(?P<a>\w+)/b/(?P<c>\w+) ==> ^<a>/b/<c>
165 """
166 named_group_indices = [
167 (m.start(0), m.end(0), m[1]) for m in named_group_matcher.finditer(pattern)
168 ]
169 # Tuples of (named capture group pattern, group name).
170 group_pattern_and_name = []
171 # Loop over the groups and their start and end indices.
172 for start, end, group_name in named_group_indices:
173 # Handle nested parentheses, e.g. '^(?P<a>(x|y))/b'.
174 unmatched_open_brackets, prev_char = 1, None
175 for idx, val in enumerate(pattern[end:]):
176 # Check for unescaped `(` and `)`. They mark the start and end of a
177 # nested group.
178 if val == "(" and prev_char != "\\":
179 unmatched_open_brackets += 1
180 elif val == ")" and prev_char != "\\":
181 unmatched_open_brackets -= 1
182 prev_char = val
183 # If brackets are balanced, the end of the string for the current
184 # named capture group pattern has been reached.
185 if unmatched_open_brackets == 0:
186 group_pattern_and_name.append(
187 (pattern[start : end + idx + 1], group_name)
188 )
189 break
191 # Replace the string for named capture groups with their group names.
192 for group_pattern, group_name in group_pattern_and_name:
193 pattern = pattern.replace(group_pattern, group_name)
194 return pattern
197def replace_unnamed_groups(pattern):
198 r"""
199 Find unnamed groups in `pattern` and replace them with '<var>'. E.g.,
200 1. ^(?P<a>\w+)/b/(\w+)$ ==> ^(?P<a>\w+)/b/<var>$
201 2. ^(?P<a>\w+)/b/((x|y)\w+)$ ==> ^(?P<a>\w+)/b/<var>$
202 3. ^(?P<a>\w+)/b/(\w+) ==> ^(?P<a>\w+)/b/<var>
203 4. ^(?P<a>\w+)/b/((x|y)\w+) ==> ^(?P<a>\w+)/b/<var>
204 """
205 unnamed_group_indices = [
206 m.start(0) for m in unnamed_group_matcher.finditer(pattern)
207 ]
208 # Indices of the start of unnamed capture groups.
209 group_indices = []
210 # Loop over the start indices of the groups.
211 for start in unnamed_group_indices:
212 # Handle nested parentheses, e.g. '^b/((x|y)\w+)$'.
213 unmatched_open_brackets, prev_char = 1, None
214 for idx, val in enumerate(pattern[start + 1 :]):
215 # Check for unescaped `(` and `)`. They mark the start and end of
216 # a nested group.
217 if val == "(" and prev_char != "\\":
218 unmatched_open_brackets += 1
219 elif val == ")" and prev_char != "\\":
220 unmatched_open_brackets -= 1
221 prev_char = val
223 if unmatched_open_brackets == 0:
224 group_indices.append((start, start + 2 + idx))
225 break
226 # Remove unnamed group matches inside other unnamed capture groups.
227 group_start_end_indices = []
228 prev_end = None
229 for start, end in group_indices:
230 if prev_end and start > prev_end or not prev_end:
231 group_start_end_indices.append((start, end))
232 prev_end = end
234 if group_start_end_indices:
235 # Replace unnamed groups with <var>. Handle the fact that replacing the
236 # string between indices will change string length and thus indices
237 # will point to the wrong substring if not corrected.
238 final_pattern, prev_end = [], None
239 for start, end in group_start_end_indices:
240 if prev_end:
241 final_pattern.append(pattern[prev_end:start])
242 final_pattern.append(pattern[:start] + "<var>")
243 prev_end = end
244 final_pattern.append(pattern[prev_end:])
245 return "".join(final_pattern)
246 else:
247 return pattern