summaryrefslogtreecommitdiffstats
path: root/sphinx/util/docstrings.py
blob: 6ccc5389b9ba94c41d4e21dced918a0dd8f4b151 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
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