agentic-pydoc 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agentic_pydoc/__init__.py +6 -0
- agentic_pydoc/analyzers/__init__.py +0 -0
- agentic_pydoc/analyzers/behavior.py +64 -0
- agentic_pydoc/analyzers/calls.py +66 -0
- agentic_pydoc/analyzers/imports.py +48 -0
- agentic_pydoc/analyzers/inheritance.py +69 -0
- agentic_pydoc/analyzers/symbols.py +181 -0
- agentic_pydoc/analyzers/tests.py +43 -0
- agentic_pydoc/cli.py +253 -0
- agentic_pydoc/config.py +116 -0
- agentic_pydoc/diff.py +397 -0
- agentic_pydoc/discovery.py +38 -0
- agentic_pydoc/generators/__init__.py +48 -0
- agentic_pydoc/generators/base.py +226 -0
- agentic_pydoc/generators/markdown.py +28 -0
- agentic_pydoc/generators/mdx.py +28 -0
- agentic_pydoc/git.py +336 -0
- agentic_pydoc/history.py +109 -0
- agentic_pydoc/indexer.py +430 -0
- agentic_pydoc/models.py +437 -0
- agentic_pydoc/navigation.py +581 -0
- agentic_pydoc/project_detection.py +319 -0
- agentic_pydoc/storage.py +414 -0
- agentic_pydoc-0.1.0.dist-info/METADATA +71 -0
- agentic_pydoc-0.1.0.dist-info/RECORD +27 -0
- agentic_pydoc-0.1.0.dist-info/WHEEL +4 -0
- agentic_pydoc-0.1.0.dist-info/entry_points.txt +2 -0
|
File without changes
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
from typing import override
|
|
3
|
+
|
|
4
|
+
from agentic_pydoc.models import BehaviorInfo
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class BehaviorExtractor(ast.NodeVisitor):
|
|
8
|
+
def __init__(self) -> None:
|
|
9
|
+
self.scope: list[str] = []
|
|
10
|
+
self.functions: dict[str, BehaviorInfo] = {}
|
|
11
|
+
|
|
12
|
+
@override
|
|
13
|
+
def visit_ClassDef(self, node: ast.ClassDef) -> None:
|
|
14
|
+
self.scope.append(node.name)
|
|
15
|
+
self.generic_visit(node)
|
|
16
|
+
_ = self.scope.pop()
|
|
17
|
+
|
|
18
|
+
@override
|
|
19
|
+
def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
|
|
20
|
+
self._function(node)
|
|
21
|
+
|
|
22
|
+
@override
|
|
23
|
+
def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None:
|
|
24
|
+
self._function(node)
|
|
25
|
+
|
|
26
|
+
def _function(self, node: ast.FunctionDef | ast.AsyncFunctionDef) -> None:
|
|
27
|
+
name = ".".join(self.scope + [node.name])
|
|
28
|
+
info = BehaviorInfo(
|
|
29
|
+
name=name,
|
|
30
|
+
line=node.lineno,
|
|
31
|
+
is_async=isinstance(node, ast.AsyncFunctionDef),
|
|
32
|
+
returns=[],
|
|
33
|
+
raises=[],
|
|
34
|
+
awaits=0,
|
|
35
|
+
yields=0,
|
|
36
|
+
attributes=[],
|
|
37
|
+
context_managers=[],
|
|
38
|
+
side_effects=[],
|
|
39
|
+
)
|
|
40
|
+
self.functions[name] = info
|
|
41
|
+
|
|
42
|
+
self.scope.append(node.name)
|
|
43
|
+
for child in ast.walk(node):
|
|
44
|
+
if isinstance(child, ast.Return) and child.value:
|
|
45
|
+
info.returns.append(ast.unparse(child.value))
|
|
46
|
+
elif isinstance(child, ast.Raise) and child.exc:
|
|
47
|
+
info.raises.append(ast.unparse(child.exc))
|
|
48
|
+
elif isinstance(child, ast.Await):
|
|
49
|
+
info.awaits += 1
|
|
50
|
+
elif isinstance(child, (ast.Yield, ast.YieldFrom)):
|
|
51
|
+
info.yields += 1
|
|
52
|
+
elif isinstance(child, ast.Attribute):
|
|
53
|
+
info.attributes.append(ast.unparse(child))
|
|
54
|
+
elif isinstance(child, (ast.With, ast.AsyncWith)):
|
|
55
|
+
info.context_managers.extend(
|
|
56
|
+
ast.unparse(item.context_expr) for item in child.items
|
|
57
|
+
)
|
|
58
|
+
_ = self.scope.pop()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def extract_behavior(tree: ast.AST) -> dict[str, BehaviorInfo]:
|
|
62
|
+
extractor = BehaviorExtractor()
|
|
63
|
+
extractor.visit(tree)
|
|
64
|
+
return extractor.functions
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import override
|
|
4
|
+
|
|
5
|
+
from agentic_pydoc.models import CallInfo, ResolutionStatus, SourceLocation
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class CallExtractor(ast.NodeVisitor):
|
|
9
|
+
def __init__(self, path: Path | None = None) -> None:
|
|
10
|
+
self.path: Path | None = path
|
|
11
|
+
self.scope: list[str] = []
|
|
12
|
+
self.calls: list[CallInfo] = []
|
|
13
|
+
|
|
14
|
+
@override
|
|
15
|
+
def visit_ClassDef(self, node: ast.ClassDef) -> None:
|
|
16
|
+
self.scope.append(node.name)
|
|
17
|
+
self.generic_visit(node)
|
|
18
|
+
_ = self.scope.pop()
|
|
19
|
+
|
|
20
|
+
@override
|
|
21
|
+
def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
|
|
22
|
+
self._function(node)
|
|
23
|
+
|
|
24
|
+
@override
|
|
25
|
+
def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None:
|
|
26
|
+
self._function(node)
|
|
27
|
+
|
|
28
|
+
def _function(self, node: ast.FunctionDef | ast.AsyncFunctionDef) -> None:
|
|
29
|
+
self.scope.append(node.name)
|
|
30
|
+
self.generic_visit(node)
|
|
31
|
+
_ = self.scope.pop()
|
|
32
|
+
|
|
33
|
+
@override
|
|
34
|
+
def visit_Call(self, node: ast.Call) -> None:
|
|
35
|
+
if self.scope:
|
|
36
|
+
caller_name = ".".join(self.scope)
|
|
37
|
+
callee_expr = ast.unparse(node.func)
|
|
38
|
+
loc = (
|
|
39
|
+
SourceLocation(
|
|
40
|
+
file=str(self.path),
|
|
41
|
+
line=node.lineno,
|
|
42
|
+
col_offset=node.col_offset,
|
|
43
|
+
)
|
|
44
|
+
if self.path
|
|
45
|
+
else SourceLocation(file="", line=node.lineno, col_offset=node.col_offset)
|
|
46
|
+
)
|
|
47
|
+
self.calls.append(
|
|
48
|
+
CallInfo(
|
|
49
|
+
id=f"call:{caller_name}->{callee_expr}@{node.lineno}",
|
|
50
|
+
caller=caller_name,
|
|
51
|
+
callee=callee_expr,
|
|
52
|
+
expression=callee_expr,
|
|
53
|
+
line=node.lineno,
|
|
54
|
+
column=node.col_offset,
|
|
55
|
+
location=loc,
|
|
56
|
+
confidence=1.0,
|
|
57
|
+
status=ResolutionStatus.UNRESOLVED,
|
|
58
|
+
)
|
|
59
|
+
)
|
|
60
|
+
self.generic_visit(node)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def extract_calls(tree: ast.AST, path: Path | None = None) -> list[CallInfo]:
|
|
64
|
+
extractor = CallExtractor(path)
|
|
65
|
+
extractor.visit(tree)
|
|
66
|
+
return extractor.calls
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from agentic_pydoc.models import ImportInfo, ImportType, SourceLocation
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def extract_imports(path: Path) -> list[ImportInfo]:
|
|
8
|
+
tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
|
|
9
|
+
result: list[ImportInfo] = []
|
|
10
|
+
|
|
11
|
+
for node in ast.walk(tree):
|
|
12
|
+
loc = SourceLocation(
|
|
13
|
+
file=str(path),
|
|
14
|
+
line=getattr(node, "lineno", 1),
|
|
15
|
+
end_line=getattr(node, "end_lineno", getattr(node, "lineno", 1)),
|
|
16
|
+
)
|
|
17
|
+
if isinstance(node, ast.Import):
|
|
18
|
+
for alias in node.names:
|
|
19
|
+
imp_id = f"import:{alias.name}" if alias.name else "import"
|
|
20
|
+
result.append(
|
|
21
|
+
ImportInfo(
|
|
22
|
+
id=imp_id,
|
|
23
|
+
type=ImportType.IMPORT,
|
|
24
|
+
module=alias.name,
|
|
25
|
+
alias=alias.asname,
|
|
26
|
+
line=node.lineno,
|
|
27
|
+
location=loc,
|
|
28
|
+
)
|
|
29
|
+
)
|
|
30
|
+
elif isinstance(node, ast.ImportFrom):
|
|
31
|
+
module = node.module or ""
|
|
32
|
+
for alias in node.names:
|
|
33
|
+
name = alias.name
|
|
34
|
+
imp_id = f"import:{module}.{name}" if module else f"import:{name}"
|
|
35
|
+
result.append(
|
|
36
|
+
ImportInfo(
|
|
37
|
+
id=imp_id,
|
|
38
|
+
type=ImportType.FROM,
|
|
39
|
+
module=module,
|
|
40
|
+
name=name,
|
|
41
|
+
alias=alias.asname,
|
|
42
|
+
level=node.level,
|
|
43
|
+
line=node.lineno,
|
|
44
|
+
location=loc,
|
|
45
|
+
)
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
return result
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
from collections import defaultdict
|
|
2
|
+
|
|
3
|
+
from ..models import (
|
|
4
|
+
DataKind,
|
|
5
|
+
InheritanceInfo,
|
|
6
|
+
Relationship,
|
|
7
|
+
RelationshipKind,
|
|
8
|
+
SourceLocation,
|
|
9
|
+
Symbol,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def build_inheritance(symbols: list[Symbol]) -> dict[str, list[str]]:
|
|
14
|
+
classes = {s.name for s in symbols if str(s.kind) == "class"}
|
|
15
|
+
children: dict[str, list[str]] = defaultdict(list)
|
|
16
|
+
|
|
17
|
+
for symbol in symbols:
|
|
18
|
+
if str(symbol.kind) != "class":
|
|
19
|
+
continue
|
|
20
|
+
for base in symbol.bases:
|
|
21
|
+
base_name = base.split(".")[-1]
|
|
22
|
+
if base_name in classes:
|
|
23
|
+
children[base_name].append(symbol.id)
|
|
24
|
+
|
|
25
|
+
return {k: sorted(v) for k, v in children.items()}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def build_inheritance_models(
|
|
29
|
+
symbols: list[Symbol],
|
|
30
|
+
) -> tuple[list[InheritanceInfo], list[Relationship]]:
|
|
31
|
+
class_map = {s.qualified_name: s for s in symbols if str(s.kind) == "class"}
|
|
32
|
+
name_map = {s.name: s for s in symbols if str(s.kind) == "class"}
|
|
33
|
+
|
|
34
|
+
inheritances: list[InheritanceInfo] = []
|
|
35
|
+
relationships: list[Relationship] = []
|
|
36
|
+
|
|
37
|
+
for symbol in symbols:
|
|
38
|
+
if str(symbol.kind) != "class":
|
|
39
|
+
continue
|
|
40
|
+
for base in symbol.bases:
|
|
41
|
+
parent_symbol = class_map.get(base) or name_map.get(base.split(".")[-1])
|
|
42
|
+
parent_id = parent_symbol.id if parent_symbol else base
|
|
43
|
+
resolved = parent_symbol is not None
|
|
44
|
+
loc = (
|
|
45
|
+
SourceLocation(file=symbol.file, line=symbol.line, end_line=symbol.end_line)
|
|
46
|
+
if symbol.file
|
|
47
|
+
else None
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
inheritances.append(
|
|
51
|
+
InheritanceInfo(
|
|
52
|
+
child=symbol.id,
|
|
53
|
+
parent=parent_id,
|
|
54
|
+
resolved=resolved,
|
|
55
|
+
location=loc,
|
|
56
|
+
)
|
|
57
|
+
)
|
|
58
|
+
relationships.append(
|
|
59
|
+
Relationship(
|
|
60
|
+
source=symbol.id,
|
|
61
|
+
target=parent_id,
|
|
62
|
+
kind=RelationshipKind.INHERITS,
|
|
63
|
+
location=loc,
|
|
64
|
+
confidence=1.0 if resolved else 0.5,
|
|
65
|
+
data_kind=DataKind.FACT if resolved else DataKind.INFERENCE,
|
|
66
|
+
)
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
return inheritances, relationships
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import override
|
|
4
|
+
|
|
5
|
+
from ..models import (
|
|
6
|
+
Parameter,
|
|
7
|
+
ParameterKind,
|
|
8
|
+
ReturnInfo,
|
|
9
|
+
Signature,
|
|
10
|
+
SourceLocation,
|
|
11
|
+
Symbol,
|
|
12
|
+
SymbolKind,
|
|
13
|
+
make_symbol_id,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class SymbolExtractor(ast.NodeVisitor):
|
|
18
|
+
def __init__(self, path: Path, module: str) -> None:
|
|
19
|
+
self.path: Path = path
|
|
20
|
+
self.module: str = module
|
|
21
|
+
self.scope: list[str] = []
|
|
22
|
+
self.symbols: list[Symbol] = []
|
|
23
|
+
|
|
24
|
+
@override
|
|
25
|
+
def visit_ClassDef(self, node: ast.ClassDef) -> None:
|
|
26
|
+
qualified = self._qualified(node.name)
|
|
27
|
+
symbol_id = make_symbol_id(SymbolKind.CLASS, qualified)
|
|
28
|
+
loc = SourceLocation(
|
|
29
|
+
file=str(self.path),
|
|
30
|
+
line=node.lineno,
|
|
31
|
+
end_line=node.end_lineno or node.lineno,
|
|
32
|
+
col_offset=node.col_offset,
|
|
33
|
+
end_col_offset=getattr(node, "end_col_offset", None),
|
|
34
|
+
)
|
|
35
|
+
self.symbols.append(
|
|
36
|
+
Symbol(
|
|
37
|
+
id=symbol_id,
|
|
38
|
+
name=node.name,
|
|
39
|
+
qualified_name=qualified,
|
|
40
|
+
kind=SymbolKind.CLASS,
|
|
41
|
+
module=self.module,
|
|
42
|
+
file=str(self.path),
|
|
43
|
+
line=node.lineno,
|
|
44
|
+
end_line=node.end_lineno or node.lineno,
|
|
45
|
+
location=loc,
|
|
46
|
+
docstring=ast.get_docstring(node),
|
|
47
|
+
decorators=[ast.unparse(x) for x in node.decorator_list],
|
|
48
|
+
bases=[ast.unparse(x) for x in node.bases],
|
|
49
|
+
is_public=not node.name.startswith("_"),
|
|
50
|
+
)
|
|
51
|
+
)
|
|
52
|
+
self.scope.append(node.name)
|
|
53
|
+
self.generic_visit(node)
|
|
54
|
+
_ = self.scope.pop()
|
|
55
|
+
|
|
56
|
+
@override
|
|
57
|
+
def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
|
|
58
|
+
self._function(node)
|
|
59
|
+
|
|
60
|
+
@override
|
|
61
|
+
def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None:
|
|
62
|
+
self._function(node)
|
|
63
|
+
|
|
64
|
+
def _function(self, node: ast.FunctionDef | ast.AsyncFunctionDef) -> None:
|
|
65
|
+
qualified = self._qualified(node.name)
|
|
66
|
+
kind = SymbolKind.METHOD if self.scope else SymbolKind.FUNCTION
|
|
67
|
+
symbol_id = make_symbol_id(kind, qualified)
|
|
68
|
+
parameters = self._parameters(node)
|
|
69
|
+
returns_str = ast.unparse(node.returns) if node.returns else None
|
|
70
|
+
loc = SourceLocation(
|
|
71
|
+
file=str(self.path),
|
|
72
|
+
line=node.lineno,
|
|
73
|
+
end_line=node.end_lineno or node.lineno,
|
|
74
|
+
col_offset=node.col_offset,
|
|
75
|
+
end_col_offset=getattr(node, "end_col_offset", None),
|
|
76
|
+
)
|
|
77
|
+
signature_info = Signature(
|
|
78
|
+
parameters=parameters,
|
|
79
|
+
return_type=ReturnInfo(annotation=returns_str) if returns_str else None,
|
|
80
|
+
)
|
|
81
|
+
self.symbols.append(
|
|
82
|
+
Symbol(
|
|
83
|
+
id=symbol_id,
|
|
84
|
+
name=node.name,
|
|
85
|
+
qualified_name=qualified,
|
|
86
|
+
kind=kind,
|
|
87
|
+
module=self.module,
|
|
88
|
+
file=str(self.path),
|
|
89
|
+
line=node.lineno,
|
|
90
|
+
end_line=node.end_lineno or node.lineno,
|
|
91
|
+
location=loc,
|
|
92
|
+
signature=ast.unparse(node.args),
|
|
93
|
+
signature_info=signature_info,
|
|
94
|
+
docstring=ast.get_docstring(node),
|
|
95
|
+
decorators=[ast.unparse(x) for x in node.decorator_list],
|
|
96
|
+
parameters=parameters,
|
|
97
|
+
returns=returns_str,
|
|
98
|
+
is_async=isinstance(node, ast.AsyncFunctionDef),
|
|
99
|
+
is_public=not node.name.startswith("_"),
|
|
100
|
+
)
|
|
101
|
+
)
|
|
102
|
+
self.scope.append(node.name)
|
|
103
|
+
self.generic_visit(node)
|
|
104
|
+
_ = self.scope.pop()
|
|
105
|
+
|
|
106
|
+
def _qualified(self, name: str) -> str:
|
|
107
|
+
return ".".join([self.module, *self.scope, name])
|
|
108
|
+
|
|
109
|
+
def _parameters(
|
|
110
|
+
self, node: ast.FunctionDef | ast.AsyncFunctionDef
|
|
111
|
+
) -> list[Parameter]:
|
|
112
|
+
args = node.args
|
|
113
|
+
posonly = args.posonlyargs
|
|
114
|
+
normal_args = args.args
|
|
115
|
+
|
|
116
|
+
# Match defaults from right to left across normal_args
|
|
117
|
+
all_pos = [*posonly, *normal_args]
|
|
118
|
+
defaults = [None] * (len(all_pos) - len(args.defaults)) + list(args.defaults)
|
|
119
|
+
|
|
120
|
+
result: list[Parameter] = []
|
|
121
|
+
|
|
122
|
+
for arg, default in zip(all_pos, defaults, strict=True):
|
|
123
|
+
pkind = (
|
|
124
|
+
ParameterKind.POSITIONAL_ONLY
|
|
125
|
+
if arg in posonly
|
|
126
|
+
else ParameterKind.POSITIONAL_OR_KEYWORD
|
|
127
|
+
)
|
|
128
|
+
default_str = ast.unparse(default) if default else None
|
|
129
|
+
result.append(
|
|
130
|
+
Parameter(
|
|
131
|
+
name=arg.arg,
|
|
132
|
+
kind=pkind,
|
|
133
|
+
annotation=ast.unparse(arg.annotation) if arg.annotation else None,
|
|
134
|
+
default=default_str,
|
|
135
|
+
required=default_str is None,
|
|
136
|
+
)
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
if args.vararg:
|
|
140
|
+
result.append(
|
|
141
|
+
Parameter(
|
|
142
|
+
name=args.vararg.arg,
|
|
143
|
+
kind=ParameterKind.VAR_POSITIONAL,
|
|
144
|
+
annotation=ast.unparse(args.vararg.annotation)
|
|
145
|
+
if args.vararg.annotation
|
|
146
|
+
else None,
|
|
147
|
+
required=False,
|
|
148
|
+
)
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
for arg, default in zip(args.kwonlyargs, args.kw_defaults, strict=True):
|
|
152
|
+
default_str = ast.unparse(default) if default else None
|
|
153
|
+
result.append(
|
|
154
|
+
Parameter(
|
|
155
|
+
name=arg.arg,
|
|
156
|
+
kind=ParameterKind.KEYWORD_ONLY,
|
|
157
|
+
annotation=ast.unparse(arg.annotation) if arg.annotation else None,
|
|
158
|
+
default=default_str,
|
|
159
|
+
required=default_str is None,
|
|
160
|
+
)
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
if args.kwarg:
|
|
164
|
+
result.append(
|
|
165
|
+
Parameter(
|
|
166
|
+
name=args.kwarg.arg,
|
|
167
|
+
kind=ParameterKind.VAR_KEYWORD,
|
|
168
|
+
annotation=ast.unparse(args.kwarg.annotation)
|
|
169
|
+
if args.kwarg.annotation
|
|
170
|
+
else None,
|
|
171
|
+
required=False,
|
|
172
|
+
)
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
return result
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def extract_symbols(tree: ast.AST, path: Path, module: str) -> list[Symbol]:
|
|
179
|
+
extractor = SymbolExtractor(path, module)
|
|
180
|
+
extractor.visit(tree)
|
|
181
|
+
return extractor.symbols
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from ..models import SourceLocation, TestInfo, make_test_id
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def extract_tests(path: Path, module: str) -> list[TestInfo]:
|
|
8
|
+
tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
|
|
9
|
+
result: list[TestInfo] = []
|
|
10
|
+
|
|
11
|
+
for node in ast.walk(tree):
|
|
12
|
+
if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
13
|
+
continue
|
|
14
|
+
if not node.name.startswith("test_"):
|
|
15
|
+
continue
|
|
16
|
+
|
|
17
|
+
calls = [
|
|
18
|
+
ast.unparse(child.func)
|
|
19
|
+
for child in ast.walk(node)
|
|
20
|
+
if isinstance(child, ast.Call)
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
loc = SourceLocation(
|
|
24
|
+
file=str(path),
|
|
25
|
+
line=node.lineno,
|
|
26
|
+
end_line=node.end_lineno or node.lineno,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
test_id = make_test_id(module, node.name)
|
|
30
|
+
result.append(
|
|
31
|
+
TestInfo(
|
|
32
|
+
id=test_id,
|
|
33
|
+
name=node.name,
|
|
34
|
+
file=str(path),
|
|
35
|
+
line=node.lineno,
|
|
36
|
+
location=loc,
|
|
37
|
+
is_async=isinstance(node, ast.AsyncFunctionDef),
|
|
38
|
+
calls=calls,
|
|
39
|
+
confidence=1.0,
|
|
40
|
+
)
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
return result
|