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