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