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.
@@ -0,0 +1,6 @@
1
+ from .navigation import KnowledgeNavigator, NavigatorTools
2
+ from .storage import KnowledgeStore
3
+
4
+ __version__ = "0.1.0"
5
+
6
+ __all__ = ["KnowledgeNavigator", "NavigatorTools", "KnowledgeStore", "__version__"]
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