emdash-core 0.1.7__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.
- emdash_core/__init__.py +3 -0
- emdash_core/agent/__init__.py +37 -0
- emdash_core/agent/agents.py +225 -0
- emdash_core/agent/code_reviewer.py +476 -0
- emdash_core/agent/compaction.py +143 -0
- emdash_core/agent/context_manager.py +140 -0
- emdash_core/agent/events.py +338 -0
- emdash_core/agent/handlers.py +224 -0
- emdash_core/agent/inprocess_subagent.py +377 -0
- emdash_core/agent/mcp/__init__.py +50 -0
- emdash_core/agent/mcp/client.py +346 -0
- emdash_core/agent/mcp/config.py +302 -0
- emdash_core/agent/mcp/manager.py +496 -0
- emdash_core/agent/mcp/tool_factory.py +213 -0
- emdash_core/agent/prompts/__init__.py +38 -0
- emdash_core/agent/prompts/main_agent.py +104 -0
- emdash_core/agent/prompts/subagents.py +131 -0
- emdash_core/agent/prompts/workflow.py +136 -0
- emdash_core/agent/providers/__init__.py +34 -0
- emdash_core/agent/providers/base.py +143 -0
- emdash_core/agent/providers/factory.py +80 -0
- emdash_core/agent/providers/models.py +220 -0
- emdash_core/agent/providers/openai_provider.py +463 -0
- emdash_core/agent/providers/transformers_provider.py +217 -0
- emdash_core/agent/research/__init__.py +81 -0
- emdash_core/agent/research/agent.py +143 -0
- emdash_core/agent/research/controller.py +254 -0
- emdash_core/agent/research/critic.py +428 -0
- emdash_core/agent/research/macros.py +469 -0
- emdash_core/agent/research/planner.py +449 -0
- emdash_core/agent/research/researcher.py +436 -0
- emdash_core/agent/research/state.py +523 -0
- emdash_core/agent/research/synthesizer.py +594 -0
- emdash_core/agent/reviewer_profile.py +475 -0
- emdash_core/agent/rules.py +123 -0
- emdash_core/agent/runner.py +601 -0
- emdash_core/agent/session.py +262 -0
- emdash_core/agent/spec_schema.py +66 -0
- emdash_core/agent/specification.py +479 -0
- emdash_core/agent/subagent.py +397 -0
- emdash_core/agent/subagent_prompts.py +13 -0
- emdash_core/agent/toolkit.py +482 -0
- emdash_core/agent/toolkits/__init__.py +64 -0
- emdash_core/agent/toolkits/base.py +96 -0
- emdash_core/agent/toolkits/explore.py +47 -0
- emdash_core/agent/toolkits/plan.py +55 -0
- emdash_core/agent/tools/__init__.py +141 -0
- emdash_core/agent/tools/analytics.py +436 -0
- emdash_core/agent/tools/base.py +131 -0
- emdash_core/agent/tools/coding.py +484 -0
- emdash_core/agent/tools/github_mcp.py +592 -0
- emdash_core/agent/tools/history.py +13 -0
- emdash_core/agent/tools/modes.py +153 -0
- emdash_core/agent/tools/plan.py +206 -0
- emdash_core/agent/tools/plan_write.py +135 -0
- emdash_core/agent/tools/search.py +412 -0
- emdash_core/agent/tools/spec.py +341 -0
- emdash_core/agent/tools/task.py +262 -0
- emdash_core/agent/tools/task_output.py +204 -0
- emdash_core/agent/tools/tasks.py +454 -0
- emdash_core/agent/tools/traversal.py +588 -0
- emdash_core/agent/tools/web.py +179 -0
- emdash_core/analytics/__init__.py +5 -0
- emdash_core/analytics/engine.py +1286 -0
- emdash_core/api/__init__.py +5 -0
- emdash_core/api/agent.py +308 -0
- emdash_core/api/agents.py +154 -0
- emdash_core/api/analyze.py +264 -0
- emdash_core/api/auth.py +173 -0
- emdash_core/api/context.py +77 -0
- emdash_core/api/db.py +121 -0
- emdash_core/api/embed.py +131 -0
- emdash_core/api/feature.py +143 -0
- emdash_core/api/health.py +93 -0
- emdash_core/api/index.py +162 -0
- emdash_core/api/plan.py +110 -0
- emdash_core/api/projectmd.py +210 -0
- emdash_core/api/query.py +320 -0
- emdash_core/api/research.py +122 -0
- emdash_core/api/review.py +161 -0
- emdash_core/api/router.py +76 -0
- emdash_core/api/rules.py +116 -0
- emdash_core/api/search.py +119 -0
- emdash_core/api/spec.py +99 -0
- emdash_core/api/swarm.py +223 -0
- emdash_core/api/tasks.py +109 -0
- emdash_core/api/team.py +120 -0
- emdash_core/auth/__init__.py +17 -0
- emdash_core/auth/github.py +389 -0
- emdash_core/config.py +74 -0
- emdash_core/context/__init__.py +52 -0
- emdash_core/context/models.py +50 -0
- emdash_core/context/providers/__init__.py +11 -0
- emdash_core/context/providers/base.py +74 -0
- emdash_core/context/providers/explored_areas.py +183 -0
- emdash_core/context/providers/touched_areas.py +360 -0
- emdash_core/context/registry.py +73 -0
- emdash_core/context/reranker.py +199 -0
- emdash_core/context/service.py +260 -0
- emdash_core/context/session.py +352 -0
- emdash_core/core/__init__.py +104 -0
- emdash_core/core/config.py +454 -0
- emdash_core/core/exceptions.py +55 -0
- emdash_core/core/models.py +265 -0
- emdash_core/core/review_config.py +57 -0
- emdash_core/db/__init__.py +67 -0
- emdash_core/db/auth.py +134 -0
- emdash_core/db/models.py +91 -0
- emdash_core/db/provider.py +222 -0
- emdash_core/db/providers/__init__.py +5 -0
- emdash_core/db/providers/supabase.py +452 -0
- emdash_core/embeddings/__init__.py +24 -0
- emdash_core/embeddings/indexer.py +534 -0
- emdash_core/embeddings/models.py +192 -0
- emdash_core/embeddings/providers/__init__.py +7 -0
- emdash_core/embeddings/providers/base.py +112 -0
- emdash_core/embeddings/providers/fireworks.py +141 -0
- emdash_core/embeddings/providers/openai.py +104 -0
- emdash_core/embeddings/registry.py +146 -0
- emdash_core/embeddings/service.py +215 -0
- emdash_core/graph/__init__.py +26 -0
- emdash_core/graph/builder.py +134 -0
- emdash_core/graph/connection.py +692 -0
- emdash_core/graph/schema.py +416 -0
- emdash_core/graph/writer.py +667 -0
- emdash_core/ingestion/__init__.py +7 -0
- emdash_core/ingestion/change_detector.py +150 -0
- emdash_core/ingestion/git/__init__.py +5 -0
- emdash_core/ingestion/git/commit_analyzer.py +196 -0
- emdash_core/ingestion/github/__init__.py +6 -0
- emdash_core/ingestion/github/pr_fetcher.py +296 -0
- emdash_core/ingestion/github/task_extractor.py +100 -0
- emdash_core/ingestion/orchestrator.py +540 -0
- emdash_core/ingestion/parsers/__init__.py +10 -0
- emdash_core/ingestion/parsers/base_parser.py +66 -0
- emdash_core/ingestion/parsers/call_graph_builder.py +121 -0
- emdash_core/ingestion/parsers/class_extractor.py +154 -0
- emdash_core/ingestion/parsers/function_extractor.py +202 -0
- emdash_core/ingestion/parsers/import_analyzer.py +119 -0
- emdash_core/ingestion/parsers/python_parser.py +123 -0
- emdash_core/ingestion/parsers/registry.py +72 -0
- emdash_core/ingestion/parsers/ts_ast_parser.js +313 -0
- emdash_core/ingestion/parsers/typescript_parser.py +278 -0
- emdash_core/ingestion/repository.py +346 -0
- emdash_core/models/__init__.py +38 -0
- emdash_core/models/agent.py +68 -0
- emdash_core/models/index.py +77 -0
- emdash_core/models/query.py +113 -0
- emdash_core/planning/__init__.py +7 -0
- emdash_core/planning/agent_api.py +413 -0
- emdash_core/planning/context_builder.py +265 -0
- emdash_core/planning/feature_context.py +232 -0
- emdash_core/planning/feature_expander.py +646 -0
- emdash_core/planning/llm_explainer.py +198 -0
- emdash_core/planning/similarity.py +509 -0
- emdash_core/planning/team_focus.py +821 -0
- emdash_core/server.py +153 -0
- emdash_core/sse/__init__.py +5 -0
- emdash_core/sse/stream.py +196 -0
- emdash_core/swarm/__init__.py +17 -0
- emdash_core/swarm/merge_agent.py +383 -0
- emdash_core/swarm/session_manager.py +274 -0
- emdash_core/swarm/swarm_runner.py +226 -0
- emdash_core/swarm/task_definition.py +137 -0
- emdash_core/swarm/worker_spawner.py +319 -0
- emdash_core/swarm/worktree_manager.py +278 -0
- emdash_core/templates/__init__.py +10 -0
- emdash_core/templates/defaults/agent-builder.md.template +82 -0
- emdash_core/templates/defaults/focus.md.template +115 -0
- emdash_core/templates/defaults/pr-review-enhanced.md.template +309 -0
- emdash_core/templates/defaults/pr-review.md.template +80 -0
- emdash_core/templates/defaults/project.md.template +85 -0
- emdash_core/templates/defaults/research_critic.md.template +112 -0
- emdash_core/templates/defaults/research_planner.md.template +85 -0
- emdash_core/templates/defaults/research_synthesizer.md.template +128 -0
- emdash_core/templates/defaults/reviewer.md.template +81 -0
- emdash_core/templates/defaults/spec.md.template +41 -0
- emdash_core/templates/defaults/tasks.md.template +78 -0
- emdash_core/templates/loader.py +296 -0
- emdash_core/utils/__init__.py +45 -0
- emdash_core/utils/git.py +84 -0
- emdash_core/utils/image.py +502 -0
- emdash_core/utils/logger.py +51 -0
- emdash_core-0.1.7.dist-info/METADATA +35 -0
- emdash_core-0.1.7.dist-info/RECORD +187 -0
- emdash_core-0.1.7.dist-info/WHEEL +4 -0
- emdash_core-0.1.7.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Abstract base class for language-specific parsers."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import List, Optional
|
|
6
|
+
|
|
7
|
+
from ...core.models import FileEntities
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class BaseLanguageParser(ABC):
|
|
11
|
+
"""Abstract base class for language-specific parsers."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, file_path: Path, repo_root: Optional[Path] = None):
|
|
14
|
+
"""Initialize parser.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
file_path: Path to source file
|
|
18
|
+
repo_root: Root directory of repository (for module name calculation)
|
|
19
|
+
"""
|
|
20
|
+
self.file_path = file_path
|
|
21
|
+
self.repo_root = repo_root or file_path.parent
|
|
22
|
+
self.module_name = self._calculate_module_name()
|
|
23
|
+
|
|
24
|
+
@abstractmethod
|
|
25
|
+
def parse(self) -> FileEntities:
|
|
26
|
+
"""Parse file and extract code entities.
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
FileEntities containing classes, functions, imports, etc.
|
|
30
|
+
"""
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
@classmethod
|
|
34
|
+
@abstractmethod
|
|
35
|
+
def get_supported_extensions(cls) -> List[str]:
|
|
36
|
+
"""Return list of file extensions this parser handles.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
List of file extensions (e.g., ['.py'] or ['.ts', '.tsx', '.js', '.jsx'])
|
|
40
|
+
"""
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
def _calculate_module_name(self) -> str:
|
|
44
|
+
"""Calculate module name from file path (language-agnostic logic).
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
Module name like "src.module.submodule"
|
|
48
|
+
"""
|
|
49
|
+
try:
|
|
50
|
+
# Get relative path from repo root
|
|
51
|
+
relative_path = self.file_path.relative_to(self.repo_root)
|
|
52
|
+
|
|
53
|
+
# Convert path to module name
|
|
54
|
+
parts = list(relative_path.parts[:-1]) # Exclude filename
|
|
55
|
+
filename = relative_path.stem # Get filename without extension
|
|
56
|
+
|
|
57
|
+
# Skip index files (index.ts, __init__.py)
|
|
58
|
+
if filename not in ["__init__", "index"]:
|
|
59
|
+
parts.append(filename)
|
|
60
|
+
|
|
61
|
+
module_name = ".".join(parts) if parts else filename
|
|
62
|
+
return module_name
|
|
63
|
+
|
|
64
|
+
except ValueError:
|
|
65
|
+
# File is outside repo root
|
|
66
|
+
return self.file_path.stem
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""Build call graph by analyzing function calls in Python AST."""
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from typing import List
|
|
5
|
+
|
|
6
|
+
from ...core.models import FunctionEntity
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class CallGraphBuilder:
|
|
10
|
+
"""Builds call relationships between functions."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, tree: ast.AST, module_name: str):
|
|
13
|
+
"""Initialize call graph builder.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
tree: Python AST
|
|
17
|
+
module_name: Module name
|
|
18
|
+
"""
|
|
19
|
+
self.tree = tree
|
|
20
|
+
self.module_name = module_name
|
|
21
|
+
|
|
22
|
+
def build(self, functions: List[FunctionEntity]):
|
|
23
|
+
"""Build call graph for all functions.
|
|
24
|
+
|
|
25
|
+
Mutates the functions list by populating the `calls` attribute.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
functions: List of FunctionEntity objects to analyze
|
|
29
|
+
"""
|
|
30
|
+
# Create a mapping from function name to qualified name
|
|
31
|
+
function_map = {func.name: func.qualified_name for func in functions}
|
|
32
|
+
|
|
33
|
+
# For each function, analyze its body for calls
|
|
34
|
+
for func in functions:
|
|
35
|
+
func.calls = self._extract_calls(func, function_map)
|
|
36
|
+
|
|
37
|
+
def _extract_calls(
|
|
38
|
+
self,
|
|
39
|
+
function: FunctionEntity,
|
|
40
|
+
function_map: dict
|
|
41
|
+
) -> List[str]:
|
|
42
|
+
"""Extract function calls from a function body.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
function: FunctionEntity to analyze
|
|
46
|
+
function_map: Mapping of function names to qualified names
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
List of qualified names of called functions
|
|
50
|
+
"""
|
|
51
|
+
calls = []
|
|
52
|
+
|
|
53
|
+
# Find the function node in the AST
|
|
54
|
+
for node in ast.walk(self.tree):
|
|
55
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
56
|
+
if node.lineno == function.line_start:
|
|
57
|
+
# Found the function node
|
|
58
|
+
calls = self._find_calls_in_node(node, function_map)
|
|
59
|
+
break
|
|
60
|
+
|
|
61
|
+
return calls
|
|
62
|
+
|
|
63
|
+
def _find_calls_in_node(
|
|
64
|
+
self,
|
|
65
|
+
node: ast.AST,
|
|
66
|
+
function_map: dict
|
|
67
|
+
) -> List[str]:
|
|
68
|
+
"""Find all function calls within a node.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
node: AST node to analyze
|
|
72
|
+
function_map: Mapping of function names to qualified names
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
List of called function qualified names
|
|
76
|
+
"""
|
|
77
|
+
calls = []
|
|
78
|
+
|
|
79
|
+
for child in ast.walk(node):
|
|
80
|
+
if isinstance(child, ast.Call):
|
|
81
|
+
called_name = self._get_call_name(child.func)
|
|
82
|
+
|
|
83
|
+
if called_name:
|
|
84
|
+
# Try to resolve to qualified name
|
|
85
|
+
if called_name in function_map:
|
|
86
|
+
calls.append(function_map[called_name])
|
|
87
|
+
else:
|
|
88
|
+
# Keep the raw name even if we can't resolve it
|
|
89
|
+
# In a second pass, we could resolve these across files
|
|
90
|
+
calls.append(called_name)
|
|
91
|
+
|
|
92
|
+
return list(set(calls)) # Remove duplicates
|
|
93
|
+
|
|
94
|
+
def _get_call_name(self, node: ast.expr) -> str:
|
|
95
|
+
"""Get the name of a called function from a Call node.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
node: Function expression node
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
Function name or empty string
|
|
102
|
+
"""
|
|
103
|
+
if isinstance(node, ast.Name):
|
|
104
|
+
# Direct call: foo()
|
|
105
|
+
return node.id
|
|
106
|
+
|
|
107
|
+
elif isinstance(node, ast.Attribute):
|
|
108
|
+
# Method call: obj.foo()
|
|
109
|
+
# We'll track these as "Type.method" if possible
|
|
110
|
+
value_name = self._get_call_name(node.value)
|
|
111
|
+
if value_name:
|
|
112
|
+
return f"{value_name}.{node.attr}"
|
|
113
|
+
else:
|
|
114
|
+
return node.attr
|
|
115
|
+
|
|
116
|
+
elif isinstance(node, ast.Call):
|
|
117
|
+
# Chained call: foo()()
|
|
118
|
+
return self._get_call_name(node.func)
|
|
119
|
+
|
|
120
|
+
else:
|
|
121
|
+
return ""
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""Extract class definitions from Python AST."""
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import List
|
|
6
|
+
|
|
7
|
+
from ...core.models import ClassEntity
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ClassExtractor:
|
|
11
|
+
"""Extracts class definitions from Python AST."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, tree: ast.AST, file_path: Path, module_name: str):
|
|
14
|
+
"""Initialize class extractor.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
tree: Python AST
|
|
18
|
+
file_path: Path to source file
|
|
19
|
+
module_name: Module name (e.g., "src.module")
|
|
20
|
+
"""
|
|
21
|
+
self.tree = tree
|
|
22
|
+
self.file_path = file_path
|
|
23
|
+
self.module_name = module_name
|
|
24
|
+
|
|
25
|
+
def extract(self) -> List[ClassEntity]:
|
|
26
|
+
"""Extract all class definitions.
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
List of ClassEntity objects
|
|
30
|
+
"""
|
|
31
|
+
classes = []
|
|
32
|
+
|
|
33
|
+
for node in ast.walk(self.tree):
|
|
34
|
+
if isinstance(node, ast.ClassDef):
|
|
35
|
+
class_entity = self._extract_class(node)
|
|
36
|
+
if class_entity:
|
|
37
|
+
classes.append(class_entity)
|
|
38
|
+
|
|
39
|
+
return classes
|
|
40
|
+
|
|
41
|
+
def _extract_class(self, node: ast.ClassDef) -> ClassEntity:
|
|
42
|
+
"""Extract a single class definition.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
node: ClassDef AST node
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
ClassEntity
|
|
49
|
+
"""
|
|
50
|
+
# Qualified name
|
|
51
|
+
qualified_name = f"{self.module_name}.{node.name}" if self.module_name else node.name
|
|
52
|
+
|
|
53
|
+
# Docstring
|
|
54
|
+
docstring = ast.get_docstring(node)
|
|
55
|
+
|
|
56
|
+
# Check if abstract
|
|
57
|
+
is_abstract = self._is_abstract(node)
|
|
58
|
+
|
|
59
|
+
# Decorators
|
|
60
|
+
decorators = [self._get_decorator_name(dec) for dec in node.decorator_list]
|
|
61
|
+
|
|
62
|
+
# Base classes
|
|
63
|
+
base_classes = []
|
|
64
|
+
for base in node.bases:
|
|
65
|
+
base_name = self._get_name(base)
|
|
66
|
+
if base_name:
|
|
67
|
+
base_classes.append(base_name)
|
|
68
|
+
|
|
69
|
+
# Extract attributes (class variables)
|
|
70
|
+
attributes = []
|
|
71
|
+
for item in node.body:
|
|
72
|
+
if isinstance(item, ast.AnnAssign) and isinstance(item.target, ast.Name):
|
|
73
|
+
attributes.append(item.target.id)
|
|
74
|
+
elif isinstance(item, ast.Assign):
|
|
75
|
+
for target in item.targets:
|
|
76
|
+
if isinstance(target, ast.Name):
|
|
77
|
+
attributes.append(target.id)
|
|
78
|
+
|
|
79
|
+
# Extract method names (will be populated later with qualified names)
|
|
80
|
+
methods = []
|
|
81
|
+
for item in node.body:
|
|
82
|
+
if isinstance(item, ast.FunctionDef) or isinstance(item, ast.AsyncFunctionDef):
|
|
83
|
+
method_qualified_name = f"{qualified_name}.{item.name}"
|
|
84
|
+
methods.append(method_qualified_name)
|
|
85
|
+
|
|
86
|
+
return ClassEntity(
|
|
87
|
+
name=node.name,
|
|
88
|
+
qualified_name=qualified_name,
|
|
89
|
+
file_path=str(self.file_path),
|
|
90
|
+
line_start=node.lineno,
|
|
91
|
+
line_end=node.end_lineno or node.lineno,
|
|
92
|
+
docstring=docstring,
|
|
93
|
+
is_abstract=is_abstract,
|
|
94
|
+
decorators=decorators,
|
|
95
|
+
base_classes=base_classes,
|
|
96
|
+
attributes=attributes,
|
|
97
|
+
methods=methods,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
def _is_abstract(self, node: ast.ClassDef) -> bool:
|
|
101
|
+
"""Check if a class is abstract (has ABC or abstractmethod)."""
|
|
102
|
+
# Check if inherits from ABC
|
|
103
|
+
for base in node.bases:
|
|
104
|
+
base_name = self._get_name(base)
|
|
105
|
+
if base_name in ["ABC", "abc.ABC"]:
|
|
106
|
+
return True
|
|
107
|
+
|
|
108
|
+
# Check if any method has @abstractmethod
|
|
109
|
+
for item in node.body:
|
|
110
|
+
if isinstance(item, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
111
|
+
for dec in item.decorator_list:
|
|
112
|
+
dec_name = self._get_decorator_name(dec)
|
|
113
|
+
if "abstractmethod" in dec_name:
|
|
114
|
+
return True
|
|
115
|
+
|
|
116
|
+
return False
|
|
117
|
+
|
|
118
|
+
def _get_decorator_name(self, decorator: ast.expr) -> str:
|
|
119
|
+
"""Get the name of a decorator.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
decorator: Decorator AST node
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
Decorator name as string
|
|
126
|
+
"""
|
|
127
|
+
if isinstance(decorator, ast.Name):
|
|
128
|
+
return decorator.id
|
|
129
|
+
elif isinstance(decorator, ast.Attribute):
|
|
130
|
+
return f"{self._get_name(decorator.value)}.{decorator.attr}"
|
|
131
|
+
elif isinstance(decorator, ast.Call):
|
|
132
|
+
return self._get_decorator_name(decorator.func)
|
|
133
|
+
else:
|
|
134
|
+
return "unknown"
|
|
135
|
+
|
|
136
|
+
def _get_name(self, node: ast.expr) -> str:
|
|
137
|
+
"""Get the name from an expression node.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
node: AST expression node
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
Name as string
|
|
144
|
+
"""
|
|
145
|
+
if isinstance(node, ast.Name):
|
|
146
|
+
return node.id
|
|
147
|
+
elif isinstance(node, ast.Attribute):
|
|
148
|
+
value_name = self._get_name(node.value)
|
|
149
|
+
return f"{value_name}.{node.attr}" if value_name else node.attr
|
|
150
|
+
elif isinstance(node, ast.Subscript):
|
|
151
|
+
# For generics like List[int]
|
|
152
|
+
return self._get_name(node.value)
|
|
153
|
+
else:
|
|
154
|
+
return ""
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""Extract function and method definitions from Python AST."""
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import List, Optional
|
|
6
|
+
|
|
7
|
+
from ...core.models import FunctionEntity
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class FunctionExtractor:
|
|
11
|
+
"""Extracts function and method definitions from Python AST."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, tree: ast.AST, file_path: Path, module_name: str):
|
|
14
|
+
"""Initialize function extractor.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
tree: Python AST
|
|
18
|
+
file_path: Path to source file
|
|
19
|
+
module_name: Module name
|
|
20
|
+
"""
|
|
21
|
+
self.tree = tree
|
|
22
|
+
self.file_path = file_path
|
|
23
|
+
self.module_name = module_name
|
|
24
|
+
self.current_class: Optional[str] = None
|
|
25
|
+
|
|
26
|
+
def extract(self) -> List[FunctionEntity]:
|
|
27
|
+
"""Extract all function and method definitions.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
List of FunctionEntity objects
|
|
31
|
+
"""
|
|
32
|
+
functions = []
|
|
33
|
+
|
|
34
|
+
for node in ast.walk(self.tree):
|
|
35
|
+
if isinstance(node, ast.ClassDef):
|
|
36
|
+
# Track current class for method extraction
|
|
37
|
+
self._extract_methods(node, functions)
|
|
38
|
+
elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
39
|
+
# Only extract top-level functions (not methods)
|
|
40
|
+
# Methods are extracted via _extract_methods
|
|
41
|
+
if not self._is_method(node):
|
|
42
|
+
function_entity = self._extract_function(node, is_method=False)
|
|
43
|
+
if function_entity:
|
|
44
|
+
functions.append(function_entity)
|
|
45
|
+
|
|
46
|
+
return functions
|
|
47
|
+
|
|
48
|
+
def _extract_methods(self, class_node: ast.ClassDef, functions: List[FunctionEntity]):
|
|
49
|
+
"""Extract methods from a class.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
class_node: ClassDef AST node
|
|
53
|
+
functions: List to append extracted methods to
|
|
54
|
+
"""
|
|
55
|
+
class_qualified_name = f"{self.module_name}.{class_node.name}" if self.module_name else class_node.name
|
|
56
|
+
|
|
57
|
+
for item in class_node.body:
|
|
58
|
+
if isinstance(item, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
59
|
+
function_entity = self._extract_function(
|
|
60
|
+
item,
|
|
61
|
+
is_method=True,
|
|
62
|
+
parent_class=class_qualified_name
|
|
63
|
+
)
|
|
64
|
+
if function_entity:
|
|
65
|
+
functions.append(function_entity)
|
|
66
|
+
|
|
67
|
+
def _is_method(self, node: ast.FunctionDef) -> bool:
|
|
68
|
+
"""Check if a function node is a method (inside a class).
|
|
69
|
+
|
|
70
|
+
This is a simplification - in practice we extract methods separately.
|
|
71
|
+
"""
|
|
72
|
+
return False
|
|
73
|
+
|
|
74
|
+
def _extract_function(
|
|
75
|
+
self,
|
|
76
|
+
node: ast.FunctionDef,
|
|
77
|
+
is_method: bool = False,
|
|
78
|
+
parent_class: Optional[str] = None
|
|
79
|
+
) -> FunctionEntity:
|
|
80
|
+
"""Extract a single function or method.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
node: FunctionDef or AsyncFunctionDef AST node
|
|
84
|
+
is_method: Whether this is a class method
|
|
85
|
+
parent_class: Qualified name of parent class (if method)
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
FunctionEntity
|
|
89
|
+
"""
|
|
90
|
+
# Qualified name
|
|
91
|
+
if is_method and parent_class:
|
|
92
|
+
qualified_name = f"{parent_class}.{node.name}"
|
|
93
|
+
else:
|
|
94
|
+
qualified_name = f"{self.module_name}.{node.name}" if self.module_name else node.name
|
|
95
|
+
|
|
96
|
+
# Docstring
|
|
97
|
+
docstring = ast.get_docstring(node)
|
|
98
|
+
|
|
99
|
+
# Parameters
|
|
100
|
+
parameters = self._extract_parameters(node.args)
|
|
101
|
+
|
|
102
|
+
# Return annotation
|
|
103
|
+
return_annotation = None
|
|
104
|
+
if node.returns:
|
|
105
|
+
return_annotation = ast.unparse(node.returns)
|
|
106
|
+
|
|
107
|
+
# Check if async
|
|
108
|
+
is_async = isinstance(node, ast.AsyncFunctionDef)
|
|
109
|
+
|
|
110
|
+
# Decorators
|
|
111
|
+
decorators = []
|
|
112
|
+
is_static = False
|
|
113
|
+
is_classmethod = False
|
|
114
|
+
|
|
115
|
+
for dec in node.decorator_list:
|
|
116
|
+
dec_name = self._get_decorator_name(dec)
|
|
117
|
+
decorators.append(dec_name)
|
|
118
|
+
|
|
119
|
+
if dec_name == "staticmethod":
|
|
120
|
+
is_static = True
|
|
121
|
+
elif dec_name == "classmethod":
|
|
122
|
+
is_classmethod = True
|
|
123
|
+
|
|
124
|
+
# Cyclomatic complexity (simplified - count branches)
|
|
125
|
+
complexity = self._calculate_complexity(node)
|
|
126
|
+
|
|
127
|
+
return FunctionEntity(
|
|
128
|
+
name=node.name,
|
|
129
|
+
qualified_name=qualified_name,
|
|
130
|
+
file_path=str(self.file_path),
|
|
131
|
+
line_start=node.lineno,
|
|
132
|
+
line_end=node.end_lineno or node.lineno,
|
|
133
|
+
docstring=docstring,
|
|
134
|
+
parameters=parameters,
|
|
135
|
+
return_annotation=return_annotation,
|
|
136
|
+
is_async=is_async,
|
|
137
|
+
is_method=is_method,
|
|
138
|
+
is_static=is_static,
|
|
139
|
+
is_classmethod=is_classmethod,
|
|
140
|
+
decorators=decorators,
|
|
141
|
+
cyclomatic_complexity=complexity,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
def _extract_parameters(self, args: ast.arguments) -> List[str]:
|
|
145
|
+
"""Extract parameter names from function arguments.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
args: arguments AST node
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
List of parameter names with optional type annotations
|
|
152
|
+
"""
|
|
153
|
+
parameters = []
|
|
154
|
+
|
|
155
|
+
# Regular args
|
|
156
|
+
for arg in args.args:
|
|
157
|
+
param = arg.arg
|
|
158
|
+
if arg.annotation:
|
|
159
|
+
param += f": {ast.unparse(arg.annotation)}"
|
|
160
|
+
parameters.append(param)
|
|
161
|
+
|
|
162
|
+
# *args
|
|
163
|
+
if args.vararg:
|
|
164
|
+
param = f"*{args.vararg.arg}"
|
|
165
|
+
if args.vararg.annotation:
|
|
166
|
+
param += f": {ast.unparse(args.vararg.annotation)}"
|
|
167
|
+
parameters.append(param)
|
|
168
|
+
|
|
169
|
+
# **kwargs
|
|
170
|
+
if args.kwarg:
|
|
171
|
+
param = f"**{args.kwarg.arg}"
|
|
172
|
+
if args.kwarg.annotation:
|
|
173
|
+
param += f": {ast.unparse(args.kwarg.annotation)}"
|
|
174
|
+
parameters.append(param)
|
|
175
|
+
|
|
176
|
+
return parameters
|
|
177
|
+
|
|
178
|
+
def _get_decorator_name(self, decorator: ast.expr) -> str:
|
|
179
|
+
"""Get the name of a decorator."""
|
|
180
|
+
if isinstance(decorator, ast.Name):
|
|
181
|
+
return decorator.id
|
|
182
|
+
elif isinstance(decorator, ast.Attribute):
|
|
183
|
+
return f"{ast.unparse(decorator.value)}.{decorator.attr}"
|
|
184
|
+
elif isinstance(decorator, ast.Call):
|
|
185
|
+
return self._get_decorator_name(decorator.func)
|
|
186
|
+
else:
|
|
187
|
+
return "unknown"
|
|
188
|
+
|
|
189
|
+
def _calculate_complexity(self, node: ast.FunctionDef) -> int:
|
|
190
|
+
"""Calculate cyclomatic complexity (simplified).
|
|
191
|
+
|
|
192
|
+
Counts decision points: if, while, for, except, with, and, or
|
|
193
|
+
"""
|
|
194
|
+
complexity = 1 # Base complexity
|
|
195
|
+
|
|
196
|
+
for child in ast.walk(node):
|
|
197
|
+
if isinstance(child, (ast.If, ast.While, ast.For, ast.ExceptHandler, ast.With)):
|
|
198
|
+
complexity += 1
|
|
199
|
+
elif isinstance(child, (ast.And, ast.Or)):
|
|
200
|
+
complexity += 1
|
|
201
|
+
|
|
202
|
+
return complexity
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Analyze import statements from Python AST."""
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import List, Tuple
|
|
6
|
+
|
|
7
|
+
from ...core.models import ImportStatement, ModuleEntity
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ImportAnalyzer:
|
|
11
|
+
"""Analyzes import statements and builds module dependency graph."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, tree: ast.AST, file_path: Path, module_name: str):
|
|
14
|
+
"""Initialize import analyzer.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
tree: Python AST
|
|
18
|
+
file_path: Path to source file
|
|
19
|
+
module_name: Module name
|
|
20
|
+
"""
|
|
21
|
+
self.tree = tree
|
|
22
|
+
self.file_path = file_path
|
|
23
|
+
self.module_name = module_name
|
|
24
|
+
|
|
25
|
+
def extract(self) -> Tuple[List[ImportStatement], List[ModuleEntity]]:
|
|
26
|
+
"""Extract all import statements and create module entities.
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
Tuple of (import_statements, module_entities)
|
|
30
|
+
"""
|
|
31
|
+
import_statements = []
|
|
32
|
+
modules = {} # Dict to avoid duplicates
|
|
33
|
+
|
|
34
|
+
for node in ast.walk(self.tree):
|
|
35
|
+
if isinstance(node, ast.Import):
|
|
36
|
+
for alias in node.names:
|
|
37
|
+
stmt = ImportStatement(
|
|
38
|
+
file_path=str(self.file_path),
|
|
39
|
+
line_number=node.lineno,
|
|
40
|
+
module=alias.name,
|
|
41
|
+
imported_names=[alias.name],
|
|
42
|
+
alias=alias.asname,
|
|
43
|
+
import_type="import",
|
|
44
|
+
)
|
|
45
|
+
import_statements.append(stmt)
|
|
46
|
+
|
|
47
|
+
# Create module entity
|
|
48
|
+
if alias.name not in modules:
|
|
49
|
+
modules[alias.name] = self._create_module_entity(alias.name)
|
|
50
|
+
|
|
51
|
+
elif isinstance(node, ast.ImportFrom):
|
|
52
|
+
if node.module:
|
|
53
|
+
imported_names = [alias.name for alias in node.names]
|
|
54
|
+
|
|
55
|
+
stmt = ImportStatement(
|
|
56
|
+
file_path=str(self.file_path),
|
|
57
|
+
line_number=node.lineno,
|
|
58
|
+
module=node.module,
|
|
59
|
+
imported_names=imported_names,
|
|
60
|
+
alias=None,
|
|
61
|
+
import_type="from_import",
|
|
62
|
+
)
|
|
63
|
+
import_statements.append(stmt)
|
|
64
|
+
|
|
65
|
+
# Create module entity
|
|
66
|
+
if node.module not in modules:
|
|
67
|
+
modules[node.module] = self._create_module_entity(node.module)
|
|
68
|
+
|
|
69
|
+
return import_statements, list(modules.values())
|
|
70
|
+
|
|
71
|
+
def _create_module_entity(self, module_name: str) -> ModuleEntity:
|
|
72
|
+
"""Create a module entity from an import.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
module_name: Name of imported module
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
ModuleEntity
|
|
79
|
+
"""
|
|
80
|
+
# Determine if module is external (stdlib or third-party)
|
|
81
|
+
is_external = self._is_external_module(module_name)
|
|
82
|
+
|
|
83
|
+
# Extract package name (first component)
|
|
84
|
+
package = module_name.split('.')[0] if '.' in module_name else module_name
|
|
85
|
+
|
|
86
|
+
return ModuleEntity(
|
|
87
|
+
name=module_name,
|
|
88
|
+
import_path=module_name,
|
|
89
|
+
is_external=is_external,
|
|
90
|
+
package=package,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
def _is_external_module(self, module_name: str) -> bool:
|
|
94
|
+
"""Check if a module is external (not part of current codebase).
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
module_name: Module name
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
True if external, False if internal
|
|
101
|
+
"""
|
|
102
|
+
# Simple heuristic: if the module starts with a well-known package, it's external
|
|
103
|
+
# In a more sophisticated version, we'd check against the actual codebase structure
|
|
104
|
+
|
|
105
|
+
common_stdlib = {
|
|
106
|
+
'os', 'sys', 'json', 're', 'math', 'datetime', 'collections',
|
|
107
|
+
'itertools', 'functools', 'pathlib', 'typing', 'abc', 'asyncio',
|
|
108
|
+
'unittest', 'logging', 'argparse', 'dataclasses', 'enum',
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
common_third_party = {
|
|
112
|
+
'numpy', 'pandas', 'requests', 'flask', 'django', 'fastapi',
|
|
113
|
+
'sqlalchemy', 'pydantic', 'pytest', 'click', 'rich', 'boto3',
|
|
114
|
+
'tensorflow', 'torch', 'sklearn', 'matplotlib', 'seaborn',
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
root_package = module_name.split('.')[0]
|
|
118
|
+
|
|
119
|
+
return root_package in common_stdlib or root_package in common_third_party
|