|
11 | 11 | import api.types |
12 | 12 |
|
13 | 13 |
|
14 | | -def extract(path: pathlib.Path) -> Mapping[str, api.Parameters]: |
15 | | - """Extracts the API from a given source file. |
16 | | -
|
17 | | - The keys will be the fully-qualified path from the root of the module, e.g. |
18 | | - * global_func |
19 | | - * ClassName.method_name |
20 | | - * ClassName.SubClassName.method_name |
21 | | - """ |
22 | | - raw_api = extract_raw(path) |
23 | | - return { |
24 | | - name: _function_def_to_parameters(function_def) |
25 | | - for name, function_def in raw_api.items() |
| 14 | +def extract(path: pathlib.Path, *, include_classes: bool = False) -> api.API: |
| 15 | + """Extracts API definitions from a given source file.""" |
| 16 | + |
| 17 | + funcs, classes = extract_raw(path, include_classes=include_classes) |
| 18 | + parameters = { |
| 19 | + name: _function_def_to_parameters(func) for name, func in funcs.items() |
26 | 20 | } |
| 21 | + return api.API(functions=parameters, classes=classes) |
| 22 | + |
27 | 23 |
|
| 24 | +def extract_raw( |
| 25 | + path: pathlib.Path, *, include_classes: bool = False |
| 26 | +) -> tuple[Mapping[str, ast.FunctionDef], Mapping[str, api.Class]]: |
| 27 | + """Extracts API as AST nodes.""" |
28 | 28 |
|
29 | | -def extract_raw(path: pathlib.Path) -> Mapping[str, ast.FunctionDef]: |
30 | | - """Extracts the API as ast.FunctionDef instances.""" |
31 | | - out: dict[str, ast.FunctionDef] = {} |
32 | | - _ContextualNodeVisitor(out, context=[]).visit( |
| 29 | + funcs: dict[str, ast.FunctionDef] = {} |
| 30 | + classes: dict[str, api.Class] = {} |
| 31 | + _ContextualNodeVisitor(funcs, classes if include_classes else None, []).visit( |
33 | 32 | ast.parse(path.read_text(), os.fspath(path)) |
34 | 33 | ) |
35 | | - return out |
| 34 | + return funcs, classes |
36 | 35 |
|
37 | 36 |
|
38 | 37 | def _function_def_to_parameters(node: ast.FunctionDef) -> api.Parameters: |
@@ -90,20 +89,69 @@ def _function_def_to_parameters(node: ast.FunctionDef) -> api.Parameters: |
90 | 89 |
|
91 | 90 |
|
92 | 91 | class _ContextualNodeVisitor(ast.NodeVisitor): |
93 | | - """NodeVisitor implementation that tracks which class, if any, it is a member of.""" |
94 | | - |
95 | | - def __init__(self, out: dict[str, ast.FunctionDef], context: Sequence[str]) -> None: |
96 | | - self._out = out |
97 | | - self._context = context |
| 92 | + """NodeVisitor that collects functions and optionally classes.""" |
| 93 | + |
| 94 | + def __init__( |
| 95 | + self, |
| 96 | + functions: dict[str, ast.FunctionDef], |
| 97 | + classes: dict[str, api.Class] | None, |
| 98 | + context: Sequence[str], |
| 99 | + ) -> None: |
| 100 | + self._functions = functions |
| 101 | + self._classes = classes |
| 102 | + self._context = list(context) |
98 | 103 |
|
99 | 104 | def visit_ClassDef(self, node: ast.ClassDef) -> None: |
100 | 105 | # Recursively visit all nodes under this class, with the given |
101 | 106 | # class name pushed onto a new context. |
| 107 | + if self._classes is not None: |
| 108 | + name = ".".join(self._context + [node.name]) |
| 109 | + is_dataclass = any( |
| 110 | + (isinstance(dec, ast.Name) and dec.id == "dataclass") |
| 111 | + or (isinstance(dec, ast.Attribute) and dec.attr == "dataclass") |
| 112 | + for dec in node.decorator_list |
| 113 | + ) |
| 114 | + fields: list[api.Field] = [] |
| 115 | + for stmt in node.body: |
| 116 | + if isinstance(stmt, ast.AnnAssign) and isinstance( |
| 117 | + stmt.target, ast.Name |
| 118 | + ): |
| 119 | + field_name = stmt.target.id |
| 120 | + if field_name.startswith("_"): |
| 121 | + continue |
| 122 | + fields.append( |
| 123 | + api.Field( |
| 124 | + name=field_name, |
| 125 | + required=stmt.value is None, |
| 126 | + line=stmt.lineno, |
| 127 | + type_annotation=api.types.annotation_to_dataclass( |
| 128 | + stmt.annotation |
| 129 | + ), |
| 130 | + ) |
| 131 | + ) |
| 132 | + elif isinstance(stmt, ast.Assign): |
| 133 | + for target in stmt.targets: |
| 134 | + if isinstance(target, ast.Name): |
| 135 | + field_name = target.id |
| 136 | + if field_name.startswith("_"): |
| 137 | + continue |
| 138 | + fields.append( |
| 139 | + api.Field( |
| 140 | + name=field_name, |
| 141 | + required=False, |
| 142 | + line=stmt.lineno, |
| 143 | + type_annotation=None, |
| 144 | + ) |
| 145 | + ) |
| 146 | + self._classes[name] = api.Class( |
| 147 | + fields=fields, line=node.lineno, dataclass=is_dataclass |
| 148 | + ) |
| 149 | + |
102 | 150 | _ContextualNodeVisitor( |
103 | | - self._out, list(self._context) + [node.name] |
| 151 | + self._functions, self._classes, self._context + [node.name] |
104 | 152 | ).generic_visit(node) |
105 | 153 |
|
106 | 154 | def visit_FunctionDef(self, node: ast.FunctionDef) -> None: |
107 | 155 | # Records this function. |
108 | | - name = ".".join(list(self._context) + [node.name]) |
109 | | - self._out[name] = node |
| 156 | + name = ".".join(self._context + [node.name]) |
| 157 | + self._functions[name] = node |
0 commit comments