"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"((?:^|(?(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\w+)/b/(\w+)$ ==> ^/b/(\w+)$ 2. ^(?P\w+)/b/(?P\w+)/$ ==> ^/b//$ 3. ^(?P\w+)/b/(\w+) ==> ^/b/(\w+) 4. ^(?P\w+)/b/(?P\w+) ==> ^/b/ """ 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 ''. E.g., 1. ^(?P\w+)/b/(\w+)$ ==> ^(?P\w+)/b/$ 2. ^(?P\w+)/b/((x|y)\w+)$ ==> ^(?P\w+)/b/$ 3. ^(?P\w+)/b/(\w+) ==> ^(?P\w+)/b/ 4. ^(?P\w+)/b/((x|y)\w+) ==> ^(?P\w+)/b/ """ 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] + "" 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\w+)/b/(?:\w+)c(?:\w+) => (?P\\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:]