Development of an internal social media platform with personalised dashboards for students
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

utils.py 7.9KB

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