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

1"Misc. utility functions/classes for admin documentation generator." 

2 

3import re 

4from email.errors import HeaderParseError 

5from email.parser import HeaderParser 

6from inspect import cleandoc 

7 

8from django.urls import reverse 

9from django.utils.regex_helper import _lazy_re_compile 

10from django.utils.safestring import mark_safe 

11 

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 

20 

21 

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 

29 

30 

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 

57 

58 

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 

76 

77%s 

78 

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"]) 

89 

90 

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} 

101 

102 

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], [] 

120 

121 docutils.parsers.rst.roles.register_canonical_role(rolename, _role) 

122 

123 

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], [] 

143 

144 

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 ) 

149 

150 for name, urlbase in ROLES.items(): 

151 create_reference_role(name, urlbase) 

152 

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"\(") 

156 

157 

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 

190 

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 

195 

196 

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 

222 

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 

233 

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