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.
Files changed (187) hide show
  1. emdash_core/__init__.py +3 -0
  2. emdash_core/agent/__init__.py +37 -0
  3. emdash_core/agent/agents.py +225 -0
  4. emdash_core/agent/code_reviewer.py +476 -0
  5. emdash_core/agent/compaction.py +143 -0
  6. emdash_core/agent/context_manager.py +140 -0
  7. emdash_core/agent/events.py +338 -0
  8. emdash_core/agent/handlers.py +224 -0
  9. emdash_core/agent/inprocess_subagent.py +377 -0
  10. emdash_core/agent/mcp/__init__.py +50 -0
  11. emdash_core/agent/mcp/client.py +346 -0
  12. emdash_core/agent/mcp/config.py +302 -0
  13. emdash_core/agent/mcp/manager.py +496 -0
  14. emdash_core/agent/mcp/tool_factory.py +213 -0
  15. emdash_core/agent/prompts/__init__.py +38 -0
  16. emdash_core/agent/prompts/main_agent.py +104 -0
  17. emdash_core/agent/prompts/subagents.py +131 -0
  18. emdash_core/agent/prompts/workflow.py +136 -0
  19. emdash_core/agent/providers/__init__.py +34 -0
  20. emdash_core/agent/providers/base.py +143 -0
  21. emdash_core/agent/providers/factory.py +80 -0
  22. emdash_core/agent/providers/models.py +220 -0
  23. emdash_core/agent/providers/openai_provider.py +463 -0
  24. emdash_core/agent/providers/transformers_provider.py +217 -0
  25. emdash_core/agent/research/__init__.py +81 -0
  26. emdash_core/agent/research/agent.py +143 -0
  27. emdash_core/agent/research/controller.py +254 -0
  28. emdash_core/agent/research/critic.py +428 -0
  29. emdash_core/agent/research/macros.py +469 -0
  30. emdash_core/agent/research/planner.py +449 -0
  31. emdash_core/agent/research/researcher.py +436 -0
  32. emdash_core/agent/research/state.py +523 -0
  33. emdash_core/agent/research/synthesizer.py +594 -0
  34. emdash_core/agent/reviewer_profile.py +475 -0
  35. emdash_core/agent/rules.py +123 -0
  36. emdash_core/agent/runner.py +601 -0
  37. emdash_core/agent/session.py +262 -0
  38. emdash_core/agent/spec_schema.py +66 -0
  39. emdash_core/agent/specification.py +479 -0
  40. emdash_core/agent/subagent.py +397 -0
  41. emdash_core/agent/subagent_prompts.py +13 -0
  42. emdash_core/agent/toolkit.py +482 -0
  43. emdash_core/agent/toolkits/__init__.py +64 -0
  44. emdash_core/agent/toolkits/base.py +96 -0
  45. emdash_core/agent/toolkits/explore.py +47 -0
  46. emdash_core/agent/toolkits/plan.py +55 -0
  47. emdash_core/agent/tools/__init__.py +141 -0
  48. emdash_core/agent/tools/analytics.py +436 -0
  49. emdash_core/agent/tools/base.py +131 -0
  50. emdash_core/agent/tools/coding.py +484 -0
  51. emdash_core/agent/tools/github_mcp.py +592 -0
  52. emdash_core/agent/tools/history.py +13 -0
  53. emdash_core/agent/tools/modes.py +153 -0
  54. emdash_core/agent/tools/plan.py +206 -0
  55. emdash_core/agent/tools/plan_write.py +135 -0
  56. emdash_core/agent/tools/search.py +412 -0
  57. emdash_core/agent/tools/spec.py +341 -0
  58. emdash_core/agent/tools/task.py +262 -0
  59. emdash_core/agent/tools/task_output.py +204 -0
  60. emdash_core/agent/tools/tasks.py +454 -0
  61. emdash_core/agent/tools/traversal.py +588 -0
  62. emdash_core/agent/tools/web.py +179 -0
  63. emdash_core/analytics/__init__.py +5 -0
  64. emdash_core/analytics/engine.py +1286 -0
  65. emdash_core/api/__init__.py +5 -0
  66. emdash_core/api/agent.py +308 -0
  67. emdash_core/api/agents.py +154 -0
  68. emdash_core/api/analyze.py +264 -0
  69. emdash_core/api/auth.py +173 -0
  70. emdash_core/api/context.py +77 -0
  71. emdash_core/api/db.py +121 -0
  72. emdash_core/api/embed.py +131 -0
  73. emdash_core/api/feature.py +143 -0
  74. emdash_core/api/health.py +93 -0
  75. emdash_core/api/index.py +162 -0
  76. emdash_core/api/plan.py +110 -0
  77. emdash_core/api/projectmd.py +210 -0
  78. emdash_core/api/query.py +320 -0
  79. emdash_core/api/research.py +122 -0
  80. emdash_core/api/review.py +161 -0
  81. emdash_core/api/router.py +76 -0
  82. emdash_core/api/rules.py +116 -0
  83. emdash_core/api/search.py +119 -0
  84. emdash_core/api/spec.py +99 -0
  85. emdash_core/api/swarm.py +223 -0
  86. emdash_core/api/tasks.py +109 -0
  87. emdash_core/api/team.py +120 -0
  88. emdash_core/auth/__init__.py +17 -0
  89. emdash_core/auth/github.py +389 -0
  90. emdash_core/config.py +74 -0
  91. emdash_core/context/__init__.py +52 -0
  92. emdash_core/context/models.py +50 -0
  93. emdash_core/context/providers/__init__.py +11 -0
  94. emdash_core/context/providers/base.py +74 -0
  95. emdash_core/context/providers/explored_areas.py +183 -0
  96. emdash_core/context/providers/touched_areas.py +360 -0
  97. emdash_core/context/registry.py +73 -0
  98. emdash_core/context/reranker.py +199 -0
  99. emdash_core/context/service.py +260 -0
  100. emdash_core/context/session.py +352 -0
  101. emdash_core/core/__init__.py +104 -0
  102. emdash_core/core/config.py +454 -0
  103. emdash_core/core/exceptions.py +55 -0
  104. emdash_core/core/models.py +265 -0
  105. emdash_core/core/review_config.py +57 -0
  106. emdash_core/db/__init__.py +67 -0
  107. emdash_core/db/auth.py +134 -0
  108. emdash_core/db/models.py +91 -0
  109. emdash_core/db/provider.py +222 -0
  110. emdash_core/db/providers/__init__.py +5 -0
  111. emdash_core/db/providers/supabase.py +452 -0
  112. emdash_core/embeddings/__init__.py +24 -0
  113. emdash_core/embeddings/indexer.py +534 -0
  114. emdash_core/embeddings/models.py +192 -0
  115. emdash_core/embeddings/providers/__init__.py +7 -0
  116. emdash_core/embeddings/providers/base.py +112 -0
  117. emdash_core/embeddings/providers/fireworks.py +141 -0
  118. emdash_core/embeddings/providers/openai.py +104 -0
  119. emdash_core/embeddings/registry.py +146 -0
  120. emdash_core/embeddings/service.py +215 -0
  121. emdash_core/graph/__init__.py +26 -0
  122. emdash_core/graph/builder.py +134 -0
  123. emdash_core/graph/connection.py +692 -0
  124. emdash_core/graph/schema.py +416 -0
  125. emdash_core/graph/writer.py +667 -0
  126. emdash_core/ingestion/__init__.py +7 -0
  127. emdash_core/ingestion/change_detector.py +150 -0
  128. emdash_core/ingestion/git/__init__.py +5 -0
  129. emdash_core/ingestion/git/commit_analyzer.py +196 -0
  130. emdash_core/ingestion/github/__init__.py +6 -0
  131. emdash_core/ingestion/github/pr_fetcher.py +296 -0
  132. emdash_core/ingestion/github/task_extractor.py +100 -0
  133. emdash_core/ingestion/orchestrator.py +540 -0
  134. emdash_core/ingestion/parsers/__init__.py +10 -0
  135. emdash_core/ingestion/parsers/base_parser.py +66 -0
  136. emdash_core/ingestion/parsers/call_graph_builder.py +121 -0
  137. emdash_core/ingestion/parsers/class_extractor.py +154 -0
  138. emdash_core/ingestion/parsers/function_extractor.py +202 -0
  139. emdash_core/ingestion/parsers/import_analyzer.py +119 -0
  140. emdash_core/ingestion/parsers/python_parser.py +123 -0
  141. emdash_core/ingestion/parsers/registry.py +72 -0
  142. emdash_core/ingestion/parsers/ts_ast_parser.js +313 -0
  143. emdash_core/ingestion/parsers/typescript_parser.py +278 -0
  144. emdash_core/ingestion/repository.py +346 -0
  145. emdash_core/models/__init__.py +38 -0
  146. emdash_core/models/agent.py +68 -0
  147. emdash_core/models/index.py +77 -0
  148. emdash_core/models/query.py +113 -0
  149. emdash_core/planning/__init__.py +7 -0
  150. emdash_core/planning/agent_api.py +413 -0
  151. emdash_core/planning/context_builder.py +265 -0
  152. emdash_core/planning/feature_context.py +232 -0
  153. emdash_core/planning/feature_expander.py +646 -0
  154. emdash_core/planning/llm_explainer.py +198 -0
  155. emdash_core/planning/similarity.py +509 -0
  156. emdash_core/planning/team_focus.py +821 -0
  157. emdash_core/server.py +153 -0
  158. emdash_core/sse/__init__.py +5 -0
  159. emdash_core/sse/stream.py +196 -0
  160. emdash_core/swarm/__init__.py +17 -0
  161. emdash_core/swarm/merge_agent.py +383 -0
  162. emdash_core/swarm/session_manager.py +274 -0
  163. emdash_core/swarm/swarm_runner.py +226 -0
  164. emdash_core/swarm/task_definition.py +137 -0
  165. emdash_core/swarm/worker_spawner.py +319 -0
  166. emdash_core/swarm/worktree_manager.py +278 -0
  167. emdash_core/templates/__init__.py +10 -0
  168. emdash_core/templates/defaults/agent-builder.md.template +82 -0
  169. emdash_core/templates/defaults/focus.md.template +115 -0
  170. emdash_core/templates/defaults/pr-review-enhanced.md.template +309 -0
  171. emdash_core/templates/defaults/pr-review.md.template +80 -0
  172. emdash_core/templates/defaults/project.md.template +85 -0
  173. emdash_core/templates/defaults/research_critic.md.template +112 -0
  174. emdash_core/templates/defaults/research_planner.md.template +85 -0
  175. emdash_core/templates/defaults/research_synthesizer.md.template +128 -0
  176. emdash_core/templates/defaults/reviewer.md.template +81 -0
  177. emdash_core/templates/defaults/spec.md.template +41 -0
  178. emdash_core/templates/defaults/tasks.md.template +78 -0
  179. emdash_core/templates/loader.py +296 -0
  180. emdash_core/utils/__init__.py +45 -0
  181. emdash_core/utils/git.py +84 -0
  182. emdash_core/utils/image.py +502 -0
  183. emdash_core/utils/logger.py +51 -0
  184. emdash_core-0.1.7.dist-info/METADATA +35 -0
  185. emdash_core-0.1.7.dist-info/RECORD +187 -0
  186. emdash_core-0.1.7.dist-info/WHEEL +4 -0
  187. 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