shotgun-sh 0.4.0.dev1__py3-none-any.whl → 0.6.2__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 (135) hide show
  1. shotgun/agents/agent_manager.py +307 -8
  2. shotgun/agents/cancellation.py +103 -0
  3. shotgun/agents/common.py +12 -0
  4. shotgun/agents/config/README.md +0 -1
  5. shotgun/agents/config/manager.py +10 -7
  6. shotgun/agents/config/models.py +5 -27
  7. shotgun/agents/config/provider.py +44 -27
  8. shotgun/agents/conversation/history/token_counting/base.py +51 -9
  9. shotgun/agents/file_read.py +176 -0
  10. shotgun/agents/messages.py +15 -3
  11. shotgun/agents/models.py +24 -1
  12. shotgun/agents/router/models.py +8 -0
  13. shotgun/agents/router/tools/delegation_tools.py +55 -1
  14. shotgun/agents/router/tools/plan_tools.py +88 -7
  15. shotgun/agents/runner.py +17 -2
  16. shotgun/agents/tools/__init__.py +8 -0
  17. shotgun/agents/tools/codebase/directory_lister.py +27 -39
  18. shotgun/agents/tools/codebase/file_read.py +26 -35
  19. shotgun/agents/tools/codebase/query_graph.py +9 -0
  20. shotgun/agents/tools/codebase/retrieve_code.py +9 -0
  21. shotgun/agents/tools/file_management.py +32 -2
  22. shotgun/agents/tools/file_read_tools/__init__.py +7 -0
  23. shotgun/agents/tools/file_read_tools/multimodal_file_read.py +167 -0
  24. shotgun/agents/tools/markdown_tools/__init__.py +62 -0
  25. shotgun/agents/tools/markdown_tools/insert_section.py +148 -0
  26. shotgun/agents/tools/markdown_tools/models.py +86 -0
  27. shotgun/agents/tools/markdown_tools/remove_section.py +114 -0
  28. shotgun/agents/tools/markdown_tools/replace_section.py +119 -0
  29. shotgun/agents/tools/markdown_tools/utils.py +453 -0
  30. shotgun/agents/tools/registry.py +44 -6
  31. shotgun/agents/tools/web_search/openai.py +42 -23
  32. shotgun/attachments/__init__.py +41 -0
  33. shotgun/attachments/errors.py +60 -0
  34. shotgun/attachments/models.py +107 -0
  35. shotgun/attachments/parser.py +257 -0
  36. shotgun/attachments/processor.py +193 -0
  37. shotgun/build_constants.py +4 -7
  38. shotgun/cli/clear.py +2 -2
  39. shotgun/cli/codebase/commands.py +181 -65
  40. shotgun/cli/compact.py +2 -2
  41. shotgun/cli/context.py +2 -2
  42. shotgun/cli/error_handler.py +2 -2
  43. shotgun/cli/run.py +90 -0
  44. shotgun/cli/spec/backup.py +2 -1
  45. shotgun/codebase/__init__.py +2 -0
  46. shotgun/codebase/benchmarks/__init__.py +35 -0
  47. shotgun/codebase/benchmarks/benchmark_runner.py +309 -0
  48. shotgun/codebase/benchmarks/exporters.py +119 -0
  49. shotgun/codebase/benchmarks/formatters/__init__.py +49 -0
  50. shotgun/codebase/benchmarks/formatters/base.py +34 -0
  51. shotgun/codebase/benchmarks/formatters/json_formatter.py +106 -0
  52. shotgun/codebase/benchmarks/formatters/markdown.py +136 -0
  53. shotgun/codebase/benchmarks/models.py +129 -0
  54. shotgun/codebase/core/__init__.py +4 -0
  55. shotgun/codebase/core/call_resolution.py +91 -0
  56. shotgun/codebase/core/change_detector.py +11 -6
  57. shotgun/codebase/core/errors.py +159 -0
  58. shotgun/codebase/core/extractors/__init__.py +23 -0
  59. shotgun/codebase/core/extractors/base.py +138 -0
  60. shotgun/codebase/core/extractors/factory.py +63 -0
  61. shotgun/codebase/core/extractors/go/__init__.py +7 -0
  62. shotgun/codebase/core/extractors/go/extractor.py +122 -0
  63. shotgun/codebase/core/extractors/javascript/__init__.py +7 -0
  64. shotgun/codebase/core/extractors/javascript/extractor.py +132 -0
  65. shotgun/codebase/core/extractors/protocol.py +109 -0
  66. shotgun/codebase/core/extractors/python/__init__.py +7 -0
  67. shotgun/codebase/core/extractors/python/extractor.py +141 -0
  68. shotgun/codebase/core/extractors/rust/__init__.py +7 -0
  69. shotgun/codebase/core/extractors/rust/extractor.py +139 -0
  70. shotgun/codebase/core/extractors/types.py +15 -0
  71. shotgun/codebase/core/extractors/typescript/__init__.py +7 -0
  72. shotgun/codebase/core/extractors/typescript/extractor.py +92 -0
  73. shotgun/codebase/core/gitignore.py +252 -0
  74. shotgun/codebase/core/ingestor.py +644 -354
  75. shotgun/codebase/core/kuzu_compat.py +119 -0
  76. shotgun/codebase/core/language_config.py +239 -0
  77. shotgun/codebase/core/manager.py +256 -46
  78. shotgun/codebase/core/metrics_collector.py +310 -0
  79. shotgun/codebase/core/metrics_types.py +347 -0
  80. shotgun/codebase/core/parallel_executor.py +424 -0
  81. shotgun/codebase/core/work_distributor.py +254 -0
  82. shotgun/codebase/core/worker.py +768 -0
  83. shotgun/codebase/indexing_state.py +86 -0
  84. shotgun/codebase/models.py +94 -0
  85. shotgun/codebase/service.py +13 -0
  86. shotgun/exceptions.py +9 -9
  87. shotgun/main.py +3 -16
  88. shotgun/posthog_telemetry.py +165 -24
  89. shotgun/prompts/agents/file_read.j2 +48 -0
  90. shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +19 -47
  91. shotgun/prompts/agents/partials/content_formatting.j2 +12 -33
  92. shotgun/prompts/agents/partials/interactive_mode.j2 +9 -32
  93. shotgun/prompts/agents/partials/router_delegation_mode.j2 +21 -22
  94. shotgun/prompts/agents/plan.j2 +14 -0
  95. shotgun/prompts/agents/router.j2 +531 -258
  96. shotgun/prompts/agents/specify.j2 +14 -0
  97. shotgun/prompts/agents/state/codebase/codebase_graphs_available.j2 +14 -1
  98. shotgun/prompts/agents/state/system_state.j2 +13 -11
  99. shotgun/prompts/agents/tasks.j2 +14 -0
  100. shotgun/settings.py +49 -10
  101. shotgun/tui/app.py +149 -18
  102. shotgun/tui/commands/__init__.py +9 -1
  103. shotgun/tui/components/attachment_bar.py +87 -0
  104. shotgun/tui/components/prompt_input.py +25 -28
  105. shotgun/tui/components/status_bar.py +14 -7
  106. shotgun/tui/dependencies.py +3 -8
  107. shotgun/tui/protocols.py +18 -0
  108. shotgun/tui/screens/chat/chat.tcss +15 -0
  109. shotgun/tui/screens/chat/chat_screen.py +766 -235
  110. shotgun/tui/screens/chat/codebase_index_prompt_screen.py +8 -4
  111. shotgun/tui/screens/chat_screen/attachment_hint.py +40 -0
  112. shotgun/tui/screens/chat_screen/command_providers.py +0 -10
  113. shotgun/tui/screens/chat_screen/history/chat_history.py +54 -14
  114. shotgun/tui/screens/chat_screen/history/formatters.py +22 -0
  115. shotgun/tui/screens/chat_screen/history/user_question.py +25 -3
  116. shotgun/tui/screens/database_locked_dialog.py +219 -0
  117. shotgun/tui/screens/database_timeout_dialog.py +158 -0
  118. shotgun/tui/screens/kuzu_error_dialog.py +135 -0
  119. shotgun/tui/screens/model_picker.py +1 -3
  120. shotgun/tui/screens/models.py +11 -0
  121. shotgun/tui/state/processing_state.py +19 -0
  122. shotgun/tui/widgets/widget_coordinator.py +18 -0
  123. shotgun/utils/file_system_utils.py +4 -1
  124. {shotgun_sh-0.4.0.dev1.dist-info → shotgun_sh-0.6.2.dist-info}/METADATA +87 -34
  125. {shotgun_sh-0.4.0.dev1.dist-info → shotgun_sh-0.6.2.dist-info}/RECORD +128 -79
  126. shotgun/cli/export.py +0 -81
  127. shotgun/cli/plan.py +0 -73
  128. shotgun/cli/research.py +0 -93
  129. shotgun/cli/specify.py +0 -70
  130. shotgun/cli/tasks.py +0 -78
  131. shotgun/sentry_telemetry.py +0 -232
  132. shotgun/tui/screens/onboarding.py +0 -584
  133. {shotgun_sh-0.4.0.dev1.dist-info → shotgun_sh-0.6.2.dist-info}/WHEEL +0 -0
  134. {shotgun_sh-0.4.0.dev1.dist-info → shotgun_sh-0.6.2.dist-info}/entry_points.txt +0 -0
  135. {shotgun_sh-0.4.0.dev1.dist-info → shotgun_sh-0.6.2.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,138 @@
1
+ """Base extractor with shared extraction logic."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from abc import ABC, abstractmethod
6
+ from typing import TYPE_CHECKING
7
+
8
+ if TYPE_CHECKING:
9
+ from tree_sitter import Node
10
+
11
+ from .types import SupportedLanguage
12
+
13
+
14
+ class BaseExtractor(ABC):
15
+ """Abstract base class for language extractors.
16
+
17
+ Provides shared extraction logic that works across languages,
18
+ with abstract methods for language-specific operations.
19
+ """
20
+
21
+ @property
22
+ @abstractmethod
23
+ def language(self) -> SupportedLanguage:
24
+ """The language this extractor handles."""
25
+ ...
26
+
27
+ @abstractmethod
28
+ def extract_decorators(self, node: Node) -> list[str]:
29
+ """Extract decorators/attributes from a function or class node."""
30
+ ...
31
+
32
+ @abstractmethod
33
+ def extract_docstring(self, node: Node) -> str | None:
34
+ """Extract documentation string from a function or class node."""
35
+ ...
36
+
37
+ @abstractmethod
38
+ def extract_inheritance(self, class_node: Node) -> list[str]:
39
+ """Extract parent class names from a class definition."""
40
+ ...
41
+
42
+ @abstractmethod
43
+ def parse_call_node(self, call_node: Node) -> tuple[str | None, str | None]:
44
+ """Parse a call expression node to extract callee information."""
45
+ ...
46
+
47
+ @abstractmethod
48
+ def _class_definition_types(self) -> list[str]:
49
+ """Return node types that represent class definitions."""
50
+ ...
51
+
52
+ @abstractmethod
53
+ def _function_definition_types(self) -> list[str]:
54
+ """Return node types that represent function definitions."""
55
+ ...
56
+
57
+ def find_parent_class(self, func_node: Node, module_qn: str) -> str | None:
58
+ """Find the parent class of a function node.
59
+
60
+ Args:
61
+ func_node: The function definition AST node
62
+ module_qn: Module qualified name
63
+
64
+ Returns:
65
+ Qualified name of parent class, or None if not in a class
66
+ """
67
+ current = func_node.parent
68
+
69
+ while current:
70
+ if current.type in self._class_definition_types():
71
+ for child in current.children:
72
+ if child.type == "identifier" and child.text:
73
+ class_name = child.text.decode("utf-8")
74
+ return f"{module_qn}.{class_name}"
75
+
76
+ current = current.parent
77
+
78
+ return None
79
+
80
+ def find_containing_function(self, node: Node, module_qn: str) -> str | None:
81
+ """Find the containing function/method of a node.
82
+
83
+ Args:
84
+ node: The AST node
85
+ module_qn: Module qualified name
86
+
87
+ Returns:
88
+ Qualified name of containing function, or None
89
+ """
90
+ current = node.parent
91
+
92
+ while current:
93
+ if current.type in self._function_definition_types():
94
+ for child in current.children:
95
+ if child.type == "identifier" and child.text:
96
+ func_name = child.text.decode("utf-8")
97
+
98
+ parent_class = self.find_parent_class(current, module_qn)
99
+ if parent_class:
100
+ return f"{parent_class}.{func_name}"
101
+ else:
102
+ return f"{module_qn}.{func_name}"
103
+
104
+ current = current.parent
105
+
106
+ return None
107
+
108
+ def count_ast_nodes(self, node: Node) -> int:
109
+ """Count total AST nodes for metrics.
110
+
111
+ Args:
112
+ node: Root AST node
113
+
114
+ Returns:
115
+ Total node count
116
+ """
117
+ count = 1
118
+ for child in node.children:
119
+ count += self.count_ast_nodes(child)
120
+ return count
121
+
122
+ def _extract_full_name(self, node: Node, parts: list[str]) -> None:
123
+ """Recursively extract full qualified name from attribute access.
124
+
125
+ Args:
126
+ node: The AST node
127
+ parts: List to accumulate name parts (modified in place)
128
+ """
129
+ if node.type == "identifier" and node.text:
130
+ parts.insert(0, node.text.decode("utf-8"))
131
+ elif node.type == "attribute":
132
+ attr_node = node.child_by_field_name("attribute")
133
+ if attr_node and attr_node.text:
134
+ parts.insert(0, attr_node.text.decode("utf-8"))
135
+
136
+ obj_node = node.child_by_field_name("object")
137
+ if obj_node:
138
+ self._extract_full_name(obj_node, parts)
@@ -0,0 +1,63 @@
1
+ """Factory for creating language-specific extractors."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .protocol import LanguageExtractor
6
+ from .types import SupportedLanguage
7
+
8
+ # Cache of extractor instances (created lazily)
9
+ _extractors: dict[SupportedLanguage, LanguageExtractor] = {}
10
+
11
+
12
+ def get_extractor(language: SupportedLanguage | str) -> LanguageExtractor:
13
+ """Get the extractor for a language.
14
+
15
+ Args:
16
+ language: The language as enum or string
17
+
18
+ Returns:
19
+ The language extractor instance
20
+
21
+ Raises:
22
+ ValueError: If language is not supported
23
+ """
24
+ if isinstance(language, str):
25
+ language = SupportedLanguage(language)
26
+
27
+ if language not in _extractors:
28
+ _extractors[language] = _create_extractor(language)
29
+
30
+ return _extractors[language]
31
+
32
+
33
+ def _create_extractor(language: SupportedLanguage) -> LanguageExtractor:
34
+ """Create a new extractor instance for the language.
35
+
36
+ Uses lazy imports to avoid loading all extractors at once.
37
+ """
38
+ match language:
39
+ case SupportedLanguage.PYTHON:
40
+ from .python.extractor import PythonExtractor
41
+
42
+ return PythonExtractor()
43
+ case SupportedLanguage.JAVASCRIPT:
44
+ from .javascript.extractor import JavaScriptExtractor
45
+
46
+ return JavaScriptExtractor()
47
+ case SupportedLanguage.TYPESCRIPT:
48
+ from .typescript.extractor import TypeScriptExtractor
49
+
50
+ return TypeScriptExtractor()
51
+ case SupportedLanguage.GO:
52
+ from .go.extractor import GoExtractor
53
+
54
+ return GoExtractor()
55
+ case SupportedLanguage.RUST:
56
+ from .rust.extractor import RustExtractor
57
+
58
+ return RustExtractor()
59
+
60
+
61
+ def clear_extractor_cache() -> None:
62
+ """Clear the extractor cache (useful for testing)."""
63
+ _extractors.clear()
@@ -0,0 +1,7 @@
1
+ """Go language extractor."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .extractor import GoExtractor
6
+
7
+ __all__ = ["GoExtractor"]
@@ -0,0 +1,122 @@
1
+ """Go language extractor implementation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ from shotgun.codebase.core.extractors.base import BaseExtractor
8
+ from shotgun.codebase.core.extractors.types import SupportedLanguage
9
+
10
+ if TYPE_CHECKING:
11
+ from tree_sitter import Node
12
+
13
+
14
+ class GoExtractor(BaseExtractor):
15
+ """Extractor for Go source code."""
16
+
17
+ @property
18
+ def language(self) -> SupportedLanguage:
19
+ """The language this extractor handles."""
20
+ return SupportedLanguage.GO
21
+
22
+ def _class_definition_types(self) -> list[str]:
23
+ """Return node types that represent type definitions.
24
+
25
+ Go doesn't have classes but has type definitions and interfaces.
26
+ """
27
+ return ["type_declaration", "type_spec"]
28
+
29
+ def _function_definition_types(self) -> list[str]:
30
+ """Return node types that represent function definitions."""
31
+ return ["function_declaration", "method_declaration"]
32
+
33
+ def extract_decorators(self, node: Node) -> list[str]:
34
+ """Extract decorators from a function node.
35
+
36
+ Go doesn't have decorators. Returns empty list.
37
+
38
+ Args:
39
+ node: The AST node
40
+
41
+ Returns:
42
+ Empty list (Go has no decorators)
43
+ """
44
+ return []
45
+
46
+ def extract_docstring(self, node: Node) -> str | None:
47
+ """Extract godoc comment from a function or type node.
48
+
49
+ Args:
50
+ node: The AST node
51
+
52
+ Returns:
53
+ The godoc comment, or None if not present
54
+ """
55
+ prev_sibling = node.prev_named_sibling
56
+ if prev_sibling and prev_sibling.type == "comment":
57
+ comment_text = prev_sibling.text
58
+ if comment_text:
59
+ text = comment_text.decode("utf-8")
60
+ if text.startswith("//"):
61
+ return text[2:].strip()
62
+ return None
63
+
64
+ def extract_inheritance(self, class_node: Node) -> list[str]:
65
+ """Extract embedded types from a struct or interface.
66
+
67
+ Go uses composition instead of inheritance.
68
+ This extracts embedded type names from struct/interface definitions.
69
+
70
+ Args:
71
+ class_node: The type definition AST node
72
+
73
+ Returns:
74
+ List of embedded type names
75
+ """
76
+ embedded: list[str] = []
77
+
78
+ for child in class_node.children:
79
+ if child.type == "struct_type":
80
+ field_list = child.child_by_field_name("fields")
81
+ if field_list:
82
+ for field in field_list.children:
83
+ if field.type == "field_declaration":
84
+ if len(field.named_children) == 1:
85
+ type_node = field.named_children[0]
86
+ if (
87
+ type_node.type == "type_identifier"
88
+ and type_node.text
89
+ ):
90
+ embedded.append(type_node.text.decode("utf-8"))
91
+ elif child.type == "interface_type":
92
+ for iface_child in child.children:
93
+ if iface_child.type == "type_identifier" and iface_child.text:
94
+ embedded.append(iface_child.text.decode("utf-8"))
95
+
96
+ return embedded
97
+
98
+ def parse_call_node(self, call_node: Node) -> tuple[str | None, str | None]:
99
+ """Parse a call expression node to extract callee information.
100
+
101
+ Args:
102
+ call_node: The call expression AST node
103
+
104
+ Returns:
105
+ Tuple of (callee_name, object_name)
106
+ """
107
+ callee_name = None
108
+ object_name = None
109
+
110
+ func_node = call_node.child_by_field_name("function")
111
+ if func_node:
112
+ if func_node.type == "identifier" and func_node.text:
113
+ callee_name = func_node.text.decode("utf-8")
114
+ elif func_node.type == "selector_expression":
115
+ operand = func_node.child_by_field_name("operand")
116
+ field = func_node.child_by_field_name("field")
117
+ if operand and operand.text:
118
+ object_name = operand.text.decode("utf-8")
119
+ if field and field.text:
120
+ callee_name = field.text.decode("utf-8")
121
+
122
+ return callee_name, object_name
@@ -0,0 +1,7 @@
1
+ """JavaScript language extractor."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .extractor import JavaScriptExtractor
6
+
7
+ __all__ = ["JavaScriptExtractor"]
@@ -0,0 +1,132 @@
1
+ """JavaScript language extractor implementation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ from shotgun.codebase.core.extractors.base import BaseExtractor
8
+ from shotgun.codebase.core.extractors.types import SupportedLanguage
9
+
10
+ if TYPE_CHECKING:
11
+ from tree_sitter import Node
12
+
13
+
14
+ class JavaScriptExtractor(BaseExtractor):
15
+ """Extractor for JavaScript source code."""
16
+
17
+ @property
18
+ def language(self) -> SupportedLanguage:
19
+ """The language this extractor handles."""
20
+ return SupportedLanguage.JAVASCRIPT
21
+
22
+ def _class_definition_types(self) -> list[str]:
23
+ """Return node types that represent class definitions."""
24
+ return ["class_declaration", "class"]
25
+
26
+ def _function_definition_types(self) -> list[str]:
27
+ """Return node types that represent function definitions."""
28
+ return ["function_declaration", "method_definition", "arrow_function"]
29
+
30
+ def extract_decorators(self, node: Node) -> list[str]:
31
+ """Extract decorators from a function or class node.
32
+
33
+ JavaScript doesn't have native decorators (they're a proposal).
34
+ Returns empty list.
35
+
36
+ Args:
37
+ node: The AST node
38
+
39
+ Returns:
40
+ Empty list (JavaScript has no decorators)
41
+ """
42
+ return []
43
+
44
+ def extract_docstring(self, node: Node) -> str | None:
45
+ """Extract JSDoc comment from a function or class node.
46
+
47
+ Args:
48
+ node: The AST node
49
+
50
+ Returns:
51
+ The JSDoc content, or None if not present
52
+ """
53
+ prev_sibling = node.prev_named_sibling
54
+ if prev_sibling and prev_sibling.type == "comment":
55
+ comment_text = prev_sibling.text
56
+ if comment_text:
57
+ text = comment_text.decode("utf-8")
58
+ if text.startswith("/**"):
59
+ text = text[3:]
60
+ if text.endswith("*/"):
61
+ text = text[:-2]
62
+ return text.strip()
63
+ return None
64
+
65
+ def extract_inheritance(self, class_node: Node) -> list[str]:
66
+ """Extract parent class names from a class definition.
67
+
68
+ Args:
69
+ class_node: The class definition AST node
70
+
71
+ Returns:
72
+ List of parent class names
73
+ """
74
+ parent_names: list[str] = []
75
+
76
+ heritage = class_node.child_by_field_name("heritage")
77
+ if heritage:
78
+ for child in heritage.children:
79
+ if child.type == "identifier" and child.text:
80
+ parent_names.append(child.text.decode("utf-8"))
81
+ elif child.type == "member_expression":
82
+ parts: list[str] = []
83
+ self._extract_member_expression(child, parts)
84
+ if parts:
85
+ parent_names.append(".".join(parts))
86
+
87
+ return parent_names
88
+
89
+ def parse_call_node(self, call_node: Node) -> tuple[str | None, str | None]:
90
+ """Parse a call expression node to extract callee information.
91
+
92
+ Args:
93
+ call_node: The call expression AST node
94
+
95
+ Returns:
96
+ Tuple of (callee_name, object_name)
97
+ """
98
+ callee_name = None
99
+ object_name = None
100
+
101
+ for child in call_node.children:
102
+ if child.type == "identifier" and child.text:
103
+ callee_name = child.text.decode("utf-8")
104
+ break
105
+ elif child.type == "member_expression":
106
+ obj_node = child.child_by_field_name("object")
107
+ prop_node = child.child_by_field_name("property")
108
+ if obj_node and obj_node.text:
109
+ object_name = obj_node.text.decode("utf-8")
110
+ if prop_node and prop_node.text:
111
+ callee_name = prop_node.text.decode("utf-8")
112
+ break
113
+
114
+ return callee_name, object_name
115
+
116
+ def _extract_member_expression(self, node: Node, parts: list[str]) -> None:
117
+ """Recursively extract full name from member expression.
118
+
119
+ Args:
120
+ node: The AST node
121
+ parts: List to accumulate name parts (modified in place)
122
+ """
123
+ if node.type == "identifier" and node.text:
124
+ parts.insert(0, node.text.decode("utf-8"))
125
+ elif node.type == "member_expression":
126
+ prop_node = node.child_by_field_name("property")
127
+ if prop_node and prop_node.text:
128
+ parts.insert(0, prop_node.text.decode("utf-8"))
129
+
130
+ obj_node = node.child_by_field_name("object")
131
+ if obj_node:
132
+ self._extract_member_expression(obj_node, parts)
@@ -0,0 +1,109 @@
1
+ """Protocol definition for language extractors."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, Protocol, runtime_checkable
6
+
7
+ if TYPE_CHECKING:
8
+ from tree_sitter import Node
9
+
10
+ from .types import SupportedLanguage
11
+
12
+
13
+ @runtime_checkable
14
+ class LanguageExtractor(Protocol):
15
+ """Protocol for language-specific AST extraction.
16
+
17
+ Each language implementation provides methods to extract:
18
+ - Decorators/attributes from function/class nodes
19
+ - Docstrings/documentation
20
+ - Inheritance information from class definitions
21
+ - Call expression parsing
22
+ - Node type information for the language
23
+ """
24
+
25
+ @property
26
+ def language(self) -> SupportedLanguage:
27
+ """The language this extractor handles."""
28
+ ...
29
+
30
+ def extract_decorators(self, node: Node) -> list[str]:
31
+ """Extract decorators/attributes from a function or class node.
32
+
33
+ Args:
34
+ node: The AST node (function or class definition)
35
+
36
+ Returns:
37
+ List of decorator/attribute names
38
+ """
39
+ ...
40
+
41
+ def extract_docstring(self, node: Node) -> str | None:
42
+ """Extract documentation string from a function or class node.
43
+
44
+ Args:
45
+ node: The AST node (function or class definition)
46
+
47
+ Returns:
48
+ The docstring content, or None if not present
49
+ """
50
+ ...
51
+
52
+ def extract_inheritance(self, class_node: Node) -> list[str]:
53
+ """Extract parent class names from a class definition.
54
+
55
+ Args:
56
+ class_node: The class definition AST node
57
+
58
+ Returns:
59
+ List of parent class names (simple names, may need resolution)
60
+ """
61
+ ...
62
+
63
+ def parse_call_node(self, call_node: Node) -> tuple[str | None, str | None]:
64
+ """Parse a call expression node to extract callee information.
65
+
66
+ Args:
67
+ call_node: The call expression AST node
68
+
69
+ Returns:
70
+ Tuple of (callee_name, object_name) where:
71
+ - callee_name: The name of the function/method being called
72
+ - object_name: The object the method is called on (for method calls)
73
+ """
74
+ ...
75
+
76
+ def find_parent_class(self, func_node: Node, module_qn: str) -> str | None:
77
+ """Find the parent class of a function node.
78
+
79
+ Args:
80
+ func_node: The function definition AST node
81
+ module_qn: Module qualified name
82
+
83
+ Returns:
84
+ Qualified name of parent class, or None if not in a class
85
+ """
86
+ ...
87
+
88
+ def find_containing_function(self, node: Node, module_qn: str) -> str | None:
89
+ """Find the containing function/method of a node.
90
+
91
+ Args:
92
+ node: The AST node
93
+ module_qn: Module qualified name
94
+
95
+ Returns:
96
+ Qualified name of containing function, or None
97
+ """
98
+ ...
99
+
100
+ def count_ast_nodes(self, node: Node) -> int:
101
+ """Count total AST nodes for metrics.
102
+
103
+ Args:
104
+ node: Root AST node
105
+
106
+ Returns:
107
+ Total node count
108
+ """
109
+ ...
@@ -0,0 +1,7 @@
1
+ """Python language extractor."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .extractor import PythonExtractor
6
+
7
+ __all__ = ["PythonExtractor"]