123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241 |
- "Misc. utility functions/classes for admin documentation generator."
-
- import re
- from email.errors import HeaderParseError
- from email.parser import HeaderParser
- from inspect import cleandoc
-
- from django.urls import reverse
- from django.utils.regex_helper import _lazy_re_compile
- from django.utils.safestring import mark_safe
-
- try:
- import docutils.core
- import docutils.nodes
- import docutils.parsers.rst.roles
- except ImportError:
- docutils_is_available = False
- else:
- docutils_is_available = True
-
-
- def get_view_name(view_func):
- if hasattr(view_func, "view_class"):
- klass = view_func.view_class
- return f"{klass.__module__}.{klass.__qualname__}"
- mod_name = view_func.__module__
- view_name = getattr(view_func, "__qualname__", view_func.__class__.__name__)
- return mod_name + "." + view_name
-
-
- def parse_docstring(docstring):
- """
- Parse out the parts of a docstring. Return (title, body, metadata).
- """
- if not docstring:
- return "", "", {}
- docstring = cleandoc(docstring)
- parts = re.split(r"\n{2,}", docstring)
- title = parts[0]
- if len(parts) == 1:
- body = ""
- metadata = {}
- else:
- parser = HeaderParser()
- try:
- metadata = parser.parsestr(parts[-1])
- except HeaderParseError:
- metadata = {}
- body = "\n\n".join(parts[1:])
- else:
- metadata = dict(metadata.items())
- if metadata:
- body = "\n\n".join(parts[1:-1])
- else:
- body = "\n\n".join(parts[1:])
- return title, body, metadata
-
-
- def parse_rst(text, default_reference_context, thing_being_parsed=None):
- """
- Convert the string from reST to an XHTML fragment.
- """
- overrides = {
- "doctitle_xform": True,
- "initial_header_level": 3,
- "default_reference_context": default_reference_context,
- "link_base": reverse("django-admindocs-docroot").rstrip("/"),
- "raw_enabled": False,
- "file_insertion_enabled": False,
- }
- thing_being_parsed = thing_being_parsed and "<%s>" % thing_being_parsed
- # Wrap ``text`` in some reST that sets the default role to ``cmsreference``,
- # then restores it.
- source = """
- .. default-role:: cmsreference
-
- %s
-
- .. default-role::
- """
- parts = docutils.core.publish_parts(
- source % text,
- source_path=thing_being_parsed,
- destination_path=None,
- writer_name="html",
- settings_overrides=overrides,
- )
- return mark_safe(parts["fragment"])
-
-
- #
- # reST roles
- #
- ROLES = {
- "model": "%s/models/%s/",
- "view": "%s/views/%s/",
- "template": "%s/templates/%s/",
- "filter": "%s/filters/#%s",
- "tag": "%s/tags/#%s",
- }
-
-
- def create_reference_role(rolename, urlbase):
- def _role(name, rawtext, text, lineno, inliner, options=None, content=None):
- if options is None:
- options = {}
- node = docutils.nodes.reference(
- rawtext,
- text,
- refuri=(
- urlbase
- % (
- inliner.document.settings.link_base,
- text.lower(),
- )
- ),
- **options,
- )
- return [node], []
-
- docutils.parsers.rst.roles.register_canonical_role(rolename, _role)
-
-
- def default_reference_role(
- name, rawtext, text, lineno, inliner, options=None, content=None
- ):
- if options is None:
- options = {}
- context = inliner.document.settings.default_reference_context
- node = docutils.nodes.reference(
- rawtext,
- text,
- refuri=(
- ROLES[context]
- % (
- inliner.document.settings.link_base,
- text.lower(),
- )
- ),
- **options,
- )
- return [node], []
-
-
- if docutils_is_available:
- docutils.parsers.rst.roles.register_canonical_role(
- "cmsreference", default_reference_role
- )
-
- for name, urlbase in ROLES.items():
- create_reference_role(name, urlbase)
-
- # Match the beginning of a named, unnamed, or non-capturing groups.
- named_group_matcher = _lazy_re_compile(r"\(\?P(<\w+>)")
- unnamed_group_matcher = _lazy_re_compile(r"\(")
- non_capturing_group_matcher = _lazy_re_compile(r"\(\?\:")
-
-
- def replace_metacharacters(pattern):
- """Remove unescaped metacharacters from the pattern."""
- return re.sub(
- r"((?:^|(?<!\\))(?:\\\\)*)(\\?)([?*+^$]|\\[bBAZ])",
- lambda m: m[1] + m[3] if m[2] else m[1],
- pattern,
- )
-
-
- def _get_group_start_end(start, end, pattern):
- # Handle nested parentheses, e.g. '^(?P<a>(x|y))/b' or '^b/((x|y)\w+)$'.
- unmatched_open_brackets, prev_char = 1, None
- for idx, val in enumerate(pattern[end:]):
- # Check for unescaped `(` and `)`. They mark the start and end of a
- # nested group.
- if val == "(" and prev_char != "\\":
- unmatched_open_brackets += 1
- elif val == ")" and prev_char != "\\":
- unmatched_open_brackets -= 1
- prev_char = val
- # If brackets are balanced, the end of the string for the current named
- # capture group pattern has been reached.
- if unmatched_open_brackets == 0:
- return start, end + idx + 1
-
-
- def _find_groups(pattern, group_matcher):
- prev_end = None
- for match in group_matcher.finditer(pattern):
- if indices := _get_group_start_end(match.start(0), match.end(0), pattern):
- start, end = indices
- if prev_end and start > prev_end or not prev_end:
- yield start, end, match
- prev_end = end
-
-
- def replace_named_groups(pattern):
- r"""
- Find named groups in `pattern` and replace them with the group name. E.g.,
- 1. ^(?P<a>\w+)/b/(\w+)$ ==> ^<a>/b/(\w+)$
- 2. ^(?P<a>\w+)/b/(?P<c>\w+)/$ ==> ^<a>/b/<c>/$
- 3. ^(?P<a>\w+)/b/(\w+) ==> ^<a>/b/(\w+)
- 4. ^(?P<a>\w+)/b/(?P<c>\w+) ==> ^<a>/b/<c>
- """
- group_pattern_and_name = [
- (pattern[start:end], match[1])
- for start, end, match in _find_groups(pattern, named_group_matcher)
- ]
- for group_pattern, group_name in group_pattern_and_name:
- pattern = pattern.replace(group_pattern, group_name)
- return pattern
-
-
- def replace_unnamed_groups(pattern):
- r"""
- Find unnamed groups in `pattern` and replace them with '<var>'. E.g.,
- 1. ^(?P<a>\w+)/b/(\w+)$ ==> ^(?P<a>\w+)/b/<var>$
- 2. ^(?P<a>\w+)/b/((x|y)\w+)$ ==> ^(?P<a>\w+)/b/<var>$
- 3. ^(?P<a>\w+)/b/(\w+) ==> ^(?P<a>\w+)/b/<var>
- 4. ^(?P<a>\w+)/b/((x|y)\w+) ==> ^(?P<a>\w+)/b/<var>
- """
- final_pattern, prev_end = "", None
- for start, end, _ in _find_groups(pattern, unnamed_group_matcher):
- if prev_end:
- final_pattern += pattern[prev_end:start]
- final_pattern += pattern[:start] + "<var>"
- prev_end = end
- return final_pattern + pattern[prev_end:]
-
-
- def remove_non_capturing_groups(pattern):
- r"""
- Find non-capturing groups in the given `pattern` and remove them, e.g.
- 1. (?P<a>\w+)/b/(?:\w+)c(?:\w+) => (?P<a>\\w+)/b/c
- 2. ^(?:\w+(?:\w+))a => ^a
- 3. ^a(?:\w+)/b(?:\w+) => ^a/b
- """
- group_start_end_indices = _find_groups(pattern, non_capturing_group_matcher)
- final_pattern, prev_end = "", None
- for start, end, _ in group_start_end_indices:
- final_pattern += pattern[prev_end:start]
- prev_end = end
- return final_pattern + pattern[prev_end:]
|