utils.py 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  1. "Misc. utility functions/classes for admin documentation generator."
  2. import re
  3. from email.errors import HeaderParseError
  4. from email.parser import HeaderParser
  5. from inspect import cleandoc
  6. from django.urls import reverse
  7. from django.utils.regex_helper import _lazy_re_compile
  8. from django.utils.safestring import mark_safe
  9. try:
  10. import docutils.core
  11. import docutils.nodes
  12. import docutils.parsers.rst.roles
  13. except ImportError:
  14. docutils_is_available = False
  15. else:
  16. docutils_is_available = True
  17. def get_view_name(view_func):
  18. mod_name = view_func.__module__
  19. view_name = getattr(view_func, '__qualname__', view_func.__class__.__name__)
  20. return mod_name + '.' + view_name
  21. def parse_docstring(docstring):
  22. """
  23. Parse out the parts of a docstring. Return (title, body, metadata).
  24. """
  25. if not docstring:
  26. return '', '', {}
  27. docstring = cleandoc(docstring)
  28. parts = re.split(r'\n{2,}', docstring)
  29. title = parts[0]
  30. if len(parts) == 1:
  31. body = ''
  32. metadata = {}
  33. else:
  34. parser = HeaderParser()
  35. try:
  36. metadata = parser.parsestr(parts[-1])
  37. except HeaderParseError:
  38. metadata = {}
  39. body = "\n\n".join(parts[1:])
  40. else:
  41. metadata = dict(metadata.items())
  42. if metadata:
  43. body = "\n\n".join(parts[1:-1])
  44. else:
  45. body = "\n\n".join(parts[1:])
  46. return title, body, metadata
  47. def parse_rst(text, default_reference_context, thing_being_parsed=None):
  48. """
  49. Convert the string from reST to an XHTML fragment.
  50. """
  51. overrides = {
  52. 'doctitle_xform': True,
  53. 'initial_header_level': 3,
  54. "default_reference_context": default_reference_context,
  55. "link_base": reverse('django-admindocs-docroot').rstrip('/'),
  56. 'raw_enabled': False,
  57. 'file_insertion_enabled': False,
  58. }
  59. thing_being_parsed = thing_being_parsed and '<%s>' % thing_being_parsed
  60. # Wrap ``text`` in some reST that sets the default role to ``cmsreference``,
  61. # then restores it.
  62. source = """
  63. .. default-role:: cmsreference
  64. %s
  65. .. default-role::
  66. """
  67. parts = docutils.core.publish_parts(
  68. source % text,
  69. source_path=thing_being_parsed, destination_path=None,
  70. writer_name='html', settings_overrides=overrides,
  71. )
  72. return mark_safe(parts['fragment'])
  73. #
  74. # reST roles
  75. #
  76. ROLES = {
  77. 'model': '%s/models/%s/',
  78. 'view': '%s/views/%s/',
  79. 'template': '%s/templates/%s/',
  80. 'filter': '%s/filters/#%s',
  81. 'tag': '%s/tags/#%s',
  82. }
  83. def create_reference_role(rolename, urlbase):
  84. def _role(name, rawtext, text, lineno, inliner, options=None, content=None):
  85. if options is None:
  86. options = {}
  87. node = docutils.nodes.reference(
  88. rawtext,
  89. text,
  90. refuri=(urlbase % (
  91. inliner.document.settings.link_base,
  92. text.lower(),
  93. )),
  94. **options
  95. )
  96. return [node], []
  97. docutils.parsers.rst.roles.register_canonical_role(rolename, _role)
  98. def default_reference_role(name, rawtext, text, lineno, inliner, options=None, content=None):
  99. if options is None:
  100. options = {}
  101. context = inliner.document.settings.default_reference_context
  102. node = docutils.nodes.reference(
  103. rawtext,
  104. text,
  105. refuri=(ROLES[context] % (
  106. inliner.document.settings.link_base,
  107. text.lower(),
  108. )),
  109. **options
  110. )
  111. return [node], []
  112. if docutils_is_available:
  113. docutils.parsers.rst.roles.register_canonical_role('cmsreference', default_reference_role)
  114. for name, urlbase in ROLES.items():
  115. create_reference_role(name, urlbase)
  116. # Match the beginning of a named or unnamed group.
  117. named_group_matcher = _lazy_re_compile(r'\(\?P(<\w+>)')
  118. unnamed_group_matcher = _lazy_re_compile(r'\(')
  119. def replace_named_groups(pattern):
  120. r"""
  121. Find named groups in `pattern` and replace them with the group name. E.g.,
  122. 1. ^(?P<a>\w+)/b/(\w+)$ ==> ^<a>/b/(\w+)$
  123. 2. ^(?P<a>\w+)/b/(?P<c>\w+)/$ ==> ^<a>/b/<c>/$
  124. 3. ^(?P<a>\w+)/b/(\w+) ==> ^<a>/b/(\w+)
  125. 4. ^(?P<a>\w+)/b/(?P<c>\w+) ==> ^<a>/b/<c>
  126. """
  127. named_group_indices = [
  128. (m.start(0), m.end(0), m[1])
  129. for m in named_group_matcher.finditer(pattern)
  130. ]
  131. # Tuples of (named capture group pattern, group name).
  132. group_pattern_and_name = []
  133. # Loop over the groups and their start and end indices.
  134. for start, end, group_name in named_group_indices:
  135. # Handle nested parentheses, e.g. '^(?P<a>(x|y))/b'.
  136. unmatched_open_brackets, prev_char = 1, None
  137. for idx, val in enumerate(pattern[end:]):
  138. # Check for unescaped `(` and `)`. They mark the start and end of a
  139. # nested group.
  140. if val == '(' and prev_char != '\\':
  141. unmatched_open_brackets += 1
  142. elif val == ')' and prev_char != '\\':
  143. unmatched_open_brackets -= 1
  144. prev_char = val
  145. # If brackets are balanced, the end of the string for the current
  146. # named capture group pattern has been reached.
  147. if unmatched_open_brackets == 0:
  148. group_pattern_and_name.append((pattern[start:end + idx + 1], group_name))
  149. break
  150. # Replace the string for named capture groups with their group names.
  151. for group_pattern, group_name in group_pattern_and_name:
  152. pattern = pattern.replace(group_pattern, group_name)
  153. return pattern
  154. def replace_unnamed_groups(pattern):
  155. r"""
  156. Find unnamed groups in `pattern` and replace them with '<var>'. E.g.,
  157. 1. ^(?P<a>\w+)/b/(\w+)$ ==> ^(?P<a>\w+)/b/<var>$
  158. 2. ^(?P<a>\w+)/b/((x|y)\w+)$ ==> ^(?P<a>\w+)/b/<var>$
  159. 3. ^(?P<a>\w+)/b/(\w+) ==> ^(?P<a>\w+)/b/<var>
  160. 4. ^(?P<a>\w+)/b/((x|y)\w+) ==> ^(?P<a>\w+)/b/<var>
  161. """
  162. unnamed_group_indices = [m.start(0) for m in unnamed_group_matcher.finditer(pattern)]
  163. # Indices of the start of unnamed capture groups.
  164. group_indices = []
  165. # Loop over the start indices of the groups.
  166. for start in unnamed_group_indices:
  167. # Handle nested parentheses, e.g. '^b/((x|y)\w+)$'.
  168. unmatched_open_brackets, prev_char = 1, None
  169. for idx, val in enumerate(pattern[start + 1:]):
  170. # Check for unescaped `(` and `)`. They mark the start and end of
  171. # a nested group.
  172. if val == '(' and prev_char != '\\':
  173. unmatched_open_brackets += 1
  174. elif val == ')' and prev_char != '\\':
  175. unmatched_open_brackets -= 1
  176. prev_char = val
  177. if unmatched_open_brackets == 0:
  178. group_indices.append((start, start + 2 + idx))
  179. break
  180. # Remove unnamed group matches inside other unnamed capture groups.
  181. group_start_end_indices = []
  182. prev_end = None
  183. for start, end in group_indices:
  184. if prev_end and start > prev_end or not prev_end:
  185. group_start_end_indices.append((start, end))
  186. prev_end = end
  187. if group_start_end_indices:
  188. # Replace unnamed groups with <var>. Handle the fact that replacing the
  189. # string between indices will change string length and thus indices
  190. # will point to the wrong substring if not corrected.
  191. final_pattern, prev_end = [], None
  192. for start, end in group_start_end_indices:
  193. if prev_end:
  194. final_pattern.append(pattern[prev_end:start])
  195. final_pattern.append(pattern[:start] + '<var>')
  196. prev_end = end
  197. final_pattern.append(pattern[prev_end:])
  198. return ''.join(final_pattern)
  199. else:
  200. return pattern