diff options
Diffstat (limited to 'sphinx/util/docstrings.py')
-rw-r--r-- | sphinx/util/docstrings.py | 88 |
1 files changed, 88 insertions, 0 deletions
diff --git a/sphinx/util/docstrings.py b/sphinx/util/docstrings.py new file mode 100644 index 0000000..6ccc538 --- /dev/null +++ b/sphinx/util/docstrings.py @@ -0,0 +1,88 @@ +"""Utilities for docstring processing.""" + +from __future__ import annotations + +import re +import sys + +from docutils.parsers.rst.states import Body + +field_list_item_re = re.compile(Body.patterns['field_marker']) + + +def separate_metadata(s: str | None) -> tuple[str | None, dict[str, str]]: + """Separate docstring into metadata and others.""" + in_other_element = False + metadata: dict[str, str] = {} + lines = [] + + if not s: + return s, metadata + + for line in prepare_docstring(s): + if line.strip() == '': + in_other_element = False + lines.append(line) + else: + matched = field_list_item_re.match(line) + if matched and not in_other_element: + field_name = matched.group()[1:].split(':', 1)[0] + if field_name.startswith('meta '): + name = field_name[5:].strip() + metadata[name] = line[matched.end():].strip() + else: + lines.append(line) + else: + in_other_element = True + lines.append(line) + + return '\n'.join(lines), metadata + + +def prepare_docstring(s: str, tabsize: int = 8) -> list[str]: + """Convert a docstring into lines of parseable reST. Remove common leading + indentation, where the indentation of the first line is ignored. + + Return the docstring as a list of lines usable for inserting into a docutils + ViewList (used as argument of nested_parse().) An empty line is added to + act as a separator between this docstring and following content. + """ + lines = s.expandtabs(tabsize).splitlines() + # Find minimum indentation of any non-blank lines after ignored lines. + margin = sys.maxsize + for line in lines[1:]: + content = len(line.lstrip()) + if content: + indent = len(line) - content + margin = min(margin, indent) + # Remove indentation from the first line. + if len(lines): + lines[0] = lines[0].lstrip() + if margin < sys.maxsize: + for i in range(1, len(lines)): + lines[i] = lines[i][margin:] + # Remove any leading blank lines. + while lines and not lines[0]: + lines.pop(0) + # make sure there is an empty line at the end + if lines and lines[-1]: + lines.append('') + return lines + + +def prepare_commentdoc(s: str) -> list[str]: + """Extract documentation comment lines (starting with #:) and return them + as a list of lines. Returns an empty list if there is no documentation. + """ + result = [] + lines = [line.strip() for line in s.expandtabs().splitlines()] + for line in lines: + if line.startswith('#:'): + line = line[2:] + # the first space after the comment is ignored + if line and line[0] == ' ': + line = line[1:] + result.append(line) + if result and result[-1]: + result.append('') + return result |