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