Coverage for pydantic/_internal/_docs_extraction.py: 95.00%
60 statements
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-15 15:02 +0000
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-15 15:02 +0000
1"""Utilities related to attribute docstring extraction."""
3from __future__ import annotations 1abcdefuvABCDMghijklmnwxEFGHNPopqrstyzIJKLO
5import ast 1abcdefuvABCDMghijklmnwxEFGHNPopqrstyzIJKLO
6import inspect 1abcdefuvABCDMghijklmnwxEFGHNPopqrstyzIJKLO
7import sys 1abcdefuvABCDMghijklmnwxEFGHNPopqrstyzIJKLO
8import textwrap 1abcdefuvABCDMghijklmnwxEFGHNPopqrstyzIJKLO
9from typing import Any 1abcdefuvABCDMghijklmnwxEFGHNPopqrstyzIJKLO
12class DocstringVisitor(ast.NodeVisitor): 1abcdefuvABCDMghijklmnwxEFGHNPopqrstyzIJKLO
13 def __init__(self) -> None: 1abcdefuvABCDMghijklmnwxEFGHNPopqrstyzIJKLO
14 super().__init__() 1abcdefuvABCDMghijklmnwxEFGHNopqrstyzIJKLO
16 self.target: str | None = None 1abcdefuvABCDMghijklmnwxEFGHNopqrstyzIJKLO
17 self.attrs: dict[str, str] = {} 1abcdefuvABCDMghijklmnwxEFGHNopqrstyzIJKLO
18 self.previous_node_type: type[ast.AST] | None = None 1abcdefuvABCDMghijklmnwxEFGHNopqrstyzIJKLO
20 def visit(self, node: ast.AST) -> Any: 1abcdefuvABCDMghijklmnwxEFGHNPopqrstyzIJKLO
21 node_result = super().visit(node) 1abcdefuvABCDMghijklmnwxEFGHNopqrstyzIJKLO
22 self.previous_node_type = type(node) 1abcdefuvABCDMghijklmnwxEFGHNopqrstyzIJKLO
23 return node_result 1abcdefuvABCDMghijklmnwxEFGHNopqrstyzIJKLO
25 def visit_AnnAssign(self, node: ast.AnnAssign) -> Any: 1abcdefuvABCDMghijklmnwxEFGHNPopqrstyzIJKLO
26 if isinstance(node.target, ast.Name): 26 ↛ exitline 26 didn't return from function 'visit_AnnAssign' because the condition on line 26 was always true1abcdefuvABCDMghijklmnwxEFGHNopqrstyzIJKLO
27 self.target = node.target.id 1abcdefuvABCDMghijklmnwxEFGHNopqrstyzIJKLO
29 def visit_Expr(self, node: ast.Expr) -> Any: 1abcdefuvABCDMghijklmnwxEFGHNPopqrstyzIJKLO
30 if ( 1abcdefghijklmnopqrst
31 isinstance(node.value, ast.Constant)
32 and isinstance(node.value.value, str)
33 and self.previous_node_type is ast.AnnAssign
34 ):
35 docstring = inspect.cleandoc(node.value.value) 1abcdefuvABCDMghijklmnwxEFGHNopqrstyzIJKLO
36 if self.target: 36 ↛ 38line 36 didn't jump to line 38 because the condition on line 36 was always true1abcdefuvABCDMghijklmnwxEFGHNopqrstyzIJKLO
37 self.attrs[self.target] = docstring 1abcdefuvABCDMghijklmnwxEFGHNopqrstyzIJKLO
38 self.target = None 1abcdefuvABCDMghijklmnwxEFGHNopqrstyzIJKLO
41def _dedent_source_lines(source: list[str]) -> str: 1abcdefuvABCDMghijklmnwxEFGHNPopqrstyzIJKLO
42 # Required for nested class definitions, e.g. in a function block
43 dedent_source = textwrap.dedent(''.join(source)) 1abcdefuvABCDMghijklmnwxEFGHNopqrstyzIJKLO
44 if dedent_source.startswith((' ', '\t')): 1abcdefuvABCDMghijklmnwxEFGHNopqrstyzIJKLO
45 # We are in the case where there's a dedented (usually multiline) string
46 # at a lower indentation level than the class itself. We wrap our class
47 # in a function as a workaround.
48 dedent_source = f'def dedent_workaround():\n{dedent_source}' 1abcdefuvABCDMghijklmnwxEFGHNopqrstyzIJKLO
49 return dedent_source 1abcdefuvABCDMghijklmnwxEFGHNopqrstyzIJKLO
52def _extract_source_from_frame(cls: type[Any]) -> list[str] | None: 1abcdefuvABCDMghijklmnwxEFGHNPopqrstyzIJKLO
53 frame = inspect.currentframe() 1abcdefuvghijklmnwxopqrstyz
55 while frame: 1abcdefuvghijklmnwxopqrstyz
56 if inspect.getmodule(frame) is inspect.getmodule(cls): 1abcdefuvghijklmnwxopqrstyz
57 lnum = frame.f_lineno 1abcdefuvghijklmnwxopqrstyz
58 try: 1abcdefuvghijklmnwxopqrstyz
59 lines, _ = inspect.findsource(frame) 1abcdefuvghijklmnwxopqrstyz
60 except OSError: # pragma: no cover
61 # Source can't be retrieved (maybe because running in an interactive terminal),
62 # we don't want to error here.
63 pass
64 else:
65 block_lines = inspect.getblock(lines[lnum - 1 :]) 1abcdefuvghijklmnwxopqrstyz
66 dedent_source = _dedent_source_lines(block_lines) 1abcdefuvghijklmnwxopqrstyz
67 try: 1abcdefuvghijklmnwxopqrstyz
68 block_tree = ast.parse(dedent_source) 1abcdefuvghijklmnwxopqrstyz
69 except SyntaxError:
70 pass
71 else:
72 stmt = block_tree.body[0] 1abcdefuvghijklmnwxopqrstyz
73 if isinstance(stmt, ast.FunctionDef) and stmt.name == 'dedent_workaround': 1abcdefuvghijklmnwxopqrstyz
74 # `_dedent_source_lines` wrapped the class around the workaround function
75 stmt = stmt.body[0] 1abcdefuvghijklmnwxopqrstyz
76 if isinstance(stmt, ast.ClassDef) and stmt.name == cls.__name__: 1abcdefuvghijklmnwxopqrstyz
77 return block_lines 1abcdefuvghijklmnwxopqrstyz
79 frame = frame.f_back 1abcdefuvghijklmnwxopqrstyz
82def extract_docstrings_from_cls(cls: type[Any], use_inspect: bool = False) -> dict[str, str]: 1abcdefuvABCDMghijklmnwxEFGHNPopqrstyzIJKLO
83 """Map model attributes and their corresponding docstring.
85 Args:
86 cls: The class of the Pydantic model to inspect.
87 use_inspect: Whether to skip usage of frames to find the object and use
88 the `inspect` module instead.
90 Returns:
91 A mapping containing attribute names and their corresponding docstring.
92 """
93 if use_inspect or sys.version_info >= (3, 13): 1abcdefuvABCDMghijklmnwxEFGHNopqrstyzIJKLO
94 # On Python < 3.13, `inspect.getsourcelines()` might not work as expected
95 # if two classes have the same name in the same source file.
96 # On Python 3.13+, it will use the new `__firstlineno__` class attribute,
97 # making it way more robust.
98 try: 1abcdefuvABCDMghijklmnwxEFGHNopqrstyzIJKLO
99 source, _ = inspect.getsourcelines(cls) 1abcdefuvABCDMghijklmnwxEFGHNopqrstyzIJKLO
100 except OSError: # pragma: no cover 1ABCDEFGHIJKL
101 return {} 1ABCDEFGHIJKL
102 else:
103 # TODO remove this implementation when we drop support for Python 3.12:
104 source = _extract_source_from_frame(cls) 1abcdefuvghijklmnwxopqrstyz
106 if not source: 1abcdefuvABCDghijklmnwxEFGHopqrstyzIJKL
107 return {} 1abcdefuvghijklmnwxopqrstyz
109 dedent_source = _dedent_source_lines(source) 1abcdefuvABCDghijklmnwxEFGHopqrstyzIJKL
111 visitor = DocstringVisitor() 1abcdefuvABCDghijklmnwxEFGHopqrstyzIJKL
112 visitor.visit(ast.parse(dedent_source)) 1abcdefuvABCDghijklmnwxEFGHopqrstyzIJKL
113 return visitor.attrs 1abcdefuvABCDghijklmnwxEFGHopqrstyzIJKL