yamlgraph 0.3.9__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.
- examples/__init__.py +1 -0
- examples/codegen/__init__.py +5 -0
- examples/codegen/models/__init__.py +13 -0
- examples/codegen/models/schemas.py +76 -0
- examples/codegen/tests/__init__.py +1 -0
- examples/codegen/tests/test_ai_helpers.py +235 -0
- examples/codegen/tests/test_ast_analysis.py +174 -0
- examples/codegen/tests/test_code_analysis.py +134 -0
- examples/codegen/tests/test_code_context.py +301 -0
- examples/codegen/tests/test_code_nav.py +89 -0
- examples/codegen/tests/test_dependency_tools.py +119 -0
- examples/codegen/tests/test_example_tools.py +185 -0
- examples/codegen/tests/test_git_tools.py +112 -0
- examples/codegen/tests/test_impl_agent_schemas.py +193 -0
- examples/codegen/tests/test_impl_agent_v4_graph.py +94 -0
- examples/codegen/tests/test_jedi_analysis.py +226 -0
- examples/codegen/tests/test_meta_tools.py +250 -0
- examples/codegen/tests/test_plan_discovery_prompt.py +98 -0
- examples/codegen/tests/test_syntax_tools.py +85 -0
- examples/codegen/tests/test_synthesize_prompt.py +94 -0
- examples/codegen/tests/test_template_tools.py +244 -0
- examples/codegen/tools/__init__.py +80 -0
- examples/codegen/tools/ai_helpers.py +420 -0
- examples/codegen/tools/ast_analysis.py +92 -0
- examples/codegen/tools/code_context.py +180 -0
- examples/codegen/tools/code_nav.py +52 -0
- examples/codegen/tools/dependency_tools.py +120 -0
- examples/codegen/tools/example_tools.py +188 -0
- examples/codegen/tools/git_tools.py +151 -0
- examples/codegen/tools/impl_executor.py +614 -0
- examples/codegen/tools/jedi_analysis.py +311 -0
- examples/codegen/tools/meta_tools.py +202 -0
- examples/codegen/tools/syntax_tools.py +26 -0
- examples/codegen/tools/template_tools.py +356 -0
- examples/fastapi_interview.py +167 -0
- examples/npc/api/__init__.py +1 -0
- examples/npc/api/app.py +100 -0
- examples/npc/api/routes/__init__.py +5 -0
- examples/npc/api/routes/encounter.py +182 -0
- examples/npc/api/session.py +330 -0
- examples/npc/demo.py +387 -0
- examples/npc/nodes/__init__.py +5 -0
- examples/npc/nodes/image_node.py +92 -0
- examples/npc/run_encounter.py +230 -0
- examples/shared/__init__.py +0 -0
- examples/shared/replicate_tool.py +238 -0
- examples/storyboard/__init__.py +1 -0
- examples/storyboard/generate_videos.py +335 -0
- examples/storyboard/nodes/__init__.py +12 -0
- examples/storyboard/nodes/animated_character_node.py +248 -0
- examples/storyboard/nodes/animated_image_node.py +138 -0
- examples/storyboard/nodes/character_node.py +162 -0
- examples/storyboard/nodes/image_node.py +118 -0
- examples/storyboard/nodes/replicate_tool.py +49 -0
- examples/storyboard/retry_images.py +118 -0
- scripts/demo_async_executor.py +212 -0
- scripts/demo_interview_e2e.py +200 -0
- scripts/demo_streaming.py +140 -0
- scripts/run_interview_demo.py +94 -0
- scripts/test_interrupt_fix.py +26 -0
- tests/__init__.py +1 -0
- tests/conftest.py +178 -0
- tests/integration/__init__.py +1 -0
- tests/integration/test_animated_storyboard.py +63 -0
- tests/integration/test_cli_commands.py +242 -0
- tests/integration/test_colocated_prompts.py +139 -0
- tests/integration/test_map_demo.py +50 -0
- tests/integration/test_memory_demo.py +283 -0
- tests/integration/test_npc_api/__init__.py +1 -0
- tests/integration/test_npc_api/test_routes.py +357 -0
- tests/integration/test_npc_api/test_session.py +216 -0
- tests/integration/test_pipeline_flow.py +105 -0
- tests/integration/test_providers.py +163 -0
- tests/integration/test_resume.py +75 -0
- tests/integration/test_subgraph_integration.py +295 -0
- tests/integration/test_subgraph_interrupt.py +106 -0
- tests/unit/__init__.py +1 -0
- tests/unit/test_agent_nodes.py +355 -0
- tests/unit/test_async_executor.py +346 -0
- tests/unit/test_checkpointer.py +212 -0
- tests/unit/test_checkpointer_factory.py +212 -0
- tests/unit/test_cli.py +121 -0
- tests/unit/test_cli_package.py +81 -0
- tests/unit/test_compile_graph_map.py +132 -0
- tests/unit/test_conditions_routing.py +253 -0
- tests/unit/test_config.py +93 -0
- tests/unit/test_conversation_memory.py +276 -0
- tests/unit/test_database.py +145 -0
- tests/unit/test_deprecation.py +104 -0
- tests/unit/test_executor.py +172 -0
- tests/unit/test_executor_async.py +179 -0
- tests/unit/test_export.py +149 -0
- tests/unit/test_expressions.py +178 -0
- tests/unit/test_feature_brainstorm.py +194 -0
- tests/unit/test_format_prompt.py +145 -0
- tests/unit/test_generic_report.py +200 -0
- tests/unit/test_graph_commands.py +327 -0
- tests/unit/test_graph_linter.py +627 -0
- tests/unit/test_graph_loader.py +357 -0
- tests/unit/test_graph_schema.py +193 -0
- tests/unit/test_inline_schema.py +151 -0
- tests/unit/test_interrupt_node.py +182 -0
- tests/unit/test_issues.py +164 -0
- tests/unit/test_jinja2_prompts.py +85 -0
- tests/unit/test_json_extract.py +134 -0
- tests/unit/test_langsmith.py +600 -0
- tests/unit/test_langsmith_tools.py +204 -0
- tests/unit/test_llm_factory.py +109 -0
- tests/unit/test_llm_factory_async.py +118 -0
- tests/unit/test_loops.py +403 -0
- tests/unit/test_map_node.py +144 -0
- tests/unit/test_no_backward_compat.py +56 -0
- tests/unit/test_node_factory.py +348 -0
- tests/unit/test_passthrough_node.py +126 -0
- tests/unit/test_prompts.py +324 -0
- tests/unit/test_python_nodes.py +198 -0
- tests/unit/test_reliability.py +298 -0
- tests/unit/test_result_export.py +234 -0
- tests/unit/test_router.py +296 -0
- tests/unit/test_sanitize.py +99 -0
- tests/unit/test_schema_loader.py +295 -0
- tests/unit/test_shell_tools.py +229 -0
- tests/unit/test_state_builder.py +331 -0
- tests/unit/test_state_builder_map.py +104 -0
- tests/unit/test_state_config.py +197 -0
- tests/unit/test_streaming.py +307 -0
- tests/unit/test_subgraph.py +596 -0
- tests/unit/test_template.py +190 -0
- tests/unit/test_tool_call_integration.py +164 -0
- tests/unit/test_tool_call_node.py +178 -0
- tests/unit/test_tool_nodes.py +129 -0
- tests/unit/test_websearch.py +234 -0
- yamlgraph/__init__.py +35 -0
- yamlgraph/builder.py +110 -0
- yamlgraph/cli/__init__.py +159 -0
- yamlgraph/cli/__main__.py +6 -0
- yamlgraph/cli/commands.py +231 -0
- yamlgraph/cli/deprecation.py +92 -0
- yamlgraph/cli/graph_commands.py +541 -0
- yamlgraph/cli/validators.py +37 -0
- yamlgraph/config.py +67 -0
- yamlgraph/constants.py +70 -0
- yamlgraph/error_handlers.py +227 -0
- yamlgraph/executor.py +290 -0
- yamlgraph/executor_async.py +288 -0
- yamlgraph/graph_loader.py +451 -0
- yamlgraph/map_compiler.py +150 -0
- yamlgraph/models/__init__.py +36 -0
- yamlgraph/models/graph_schema.py +181 -0
- yamlgraph/models/schemas.py +124 -0
- yamlgraph/models/state_builder.py +236 -0
- yamlgraph/node_factory.py +768 -0
- yamlgraph/routing.py +87 -0
- yamlgraph/schema_loader.py +240 -0
- yamlgraph/storage/__init__.py +20 -0
- yamlgraph/storage/checkpointer.py +72 -0
- yamlgraph/storage/checkpointer_factory.py +123 -0
- yamlgraph/storage/database.py +320 -0
- yamlgraph/storage/export.py +269 -0
- yamlgraph/tools/__init__.py +1 -0
- yamlgraph/tools/agent.py +320 -0
- yamlgraph/tools/graph_linter.py +388 -0
- yamlgraph/tools/langsmith_tools.py +125 -0
- yamlgraph/tools/nodes.py +126 -0
- yamlgraph/tools/python_tool.py +179 -0
- yamlgraph/tools/shell.py +205 -0
- yamlgraph/tools/websearch.py +242 -0
- yamlgraph/utils/__init__.py +48 -0
- yamlgraph/utils/conditions.py +157 -0
- yamlgraph/utils/expressions.py +245 -0
- yamlgraph/utils/json_extract.py +104 -0
- yamlgraph/utils/langsmith.py +416 -0
- yamlgraph/utils/llm_factory.py +118 -0
- yamlgraph/utils/llm_factory_async.py +105 -0
- yamlgraph/utils/logging.py +104 -0
- yamlgraph/utils/prompts.py +171 -0
- yamlgraph/utils/sanitize.py +98 -0
- yamlgraph/utils/template.py +102 -0
- yamlgraph/utils/validators.py +181 -0
- yamlgraph-0.3.9.dist-info/METADATA +1105 -0
- yamlgraph-0.3.9.dist-info/RECORD +185 -0
- yamlgraph-0.3.9.dist-info/WHEEL +5 -0
- yamlgraph-0.3.9.dist-info/entry_points.txt +2 -0
- yamlgraph-0.3.9.dist-info/licenses/LICENSE +33 -0
- yamlgraph-0.3.9.dist-info/top_level.txt +4 -0
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
"""Tests for code context tools."""
|
|
2
|
+
|
|
3
|
+
import tempfile
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from examples.codegen.tools.code_context import (
|
|
7
|
+
find_related_tests,
|
|
8
|
+
read_lines,
|
|
9
|
+
search_codebase,
|
|
10
|
+
search_in_file,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TestReadLines:
|
|
15
|
+
"""Tests for read_lines function."""
|
|
16
|
+
|
|
17
|
+
def test_reads_specific_lines(self):
|
|
18
|
+
"""Reads the specified line range."""
|
|
19
|
+
# Read known lines from a project file
|
|
20
|
+
result = read_lines("yamlgraph/executor.py", 1, 10)
|
|
21
|
+
|
|
22
|
+
assert isinstance(result, str)
|
|
23
|
+
assert len(result) > 0
|
|
24
|
+
# First line should be docstring or comment
|
|
25
|
+
lines = result.split("\n")
|
|
26
|
+
assert len(lines) >= 10
|
|
27
|
+
|
|
28
|
+
def test_returns_error_for_missing_file(self):
|
|
29
|
+
"""Returns error dict for non-existent file."""
|
|
30
|
+
result = read_lines("nonexistent_file_12345.py", 1, 10)
|
|
31
|
+
|
|
32
|
+
assert isinstance(result, dict)
|
|
33
|
+
assert "error" in result
|
|
34
|
+
|
|
35
|
+
def test_handles_line_range_beyond_file(self):
|
|
36
|
+
"""Handles line numbers beyond file length gracefully."""
|
|
37
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
|
|
38
|
+
f.write("line1\nline2\nline3\n")
|
|
39
|
+
temp_path = f.name
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
result = read_lines(temp_path, 1, 100)
|
|
43
|
+
assert isinstance(result, str)
|
|
44
|
+
assert "line1" in result
|
|
45
|
+
assert "line3" in result
|
|
46
|
+
finally:
|
|
47
|
+
Path(temp_path).unlink()
|
|
48
|
+
|
|
49
|
+
def test_one_indexed_lines(self):
|
|
50
|
+
"""Line numbers are 1-indexed."""
|
|
51
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
|
|
52
|
+
f.write("first\nsecond\nthird\n")
|
|
53
|
+
temp_path = f.name
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
result = read_lines(temp_path, 2, 2)
|
|
57
|
+
assert "second" in result
|
|
58
|
+
assert "first" not in result
|
|
59
|
+
finally:
|
|
60
|
+
Path(temp_path).unlink()
|
|
61
|
+
|
|
62
|
+
def test_handles_invalid_line_range(self):
|
|
63
|
+
"""Handles start > end gracefully."""
|
|
64
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
|
|
65
|
+
f.write("line1\nline2\n")
|
|
66
|
+
temp_path = f.name
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
result = read_lines(temp_path, 5, 3)
|
|
70
|
+
# Should return empty or handle gracefully
|
|
71
|
+
assert isinstance(result, str)
|
|
72
|
+
finally:
|
|
73
|
+
Path(temp_path).unlink()
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class TestFindRelatedTests:
|
|
77
|
+
"""Tests for find_related_tests function."""
|
|
78
|
+
|
|
79
|
+
def test_finds_tests_mentioning_symbol(self):
|
|
80
|
+
"""Finds test functions that mention the symbol."""
|
|
81
|
+
result = find_related_tests("execute_prompt", "tests")
|
|
82
|
+
|
|
83
|
+
assert isinstance(result, list)
|
|
84
|
+
# Should find some tests
|
|
85
|
+
assert len(result) > 0
|
|
86
|
+
|
|
87
|
+
for test in result:
|
|
88
|
+
assert "file" in test
|
|
89
|
+
assert "line" in test
|
|
90
|
+
assert "test_name" in test
|
|
91
|
+
assert test["test_name"].startswith("test_")
|
|
92
|
+
|
|
93
|
+
def test_returns_empty_for_unknown_symbol(self):
|
|
94
|
+
"""Returns empty list for symbol not in tests."""
|
|
95
|
+
# Create a temp directory with a test file that doesn't have our symbol
|
|
96
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
97
|
+
test_file = Path(tmpdir) / "test_sample.py"
|
|
98
|
+
test_file.write_text("""
|
|
99
|
+
def test_something():
|
|
100
|
+
assert True
|
|
101
|
+
""")
|
|
102
|
+
result = find_related_tests("nonexistent", tmpdir)
|
|
103
|
+
|
|
104
|
+
assert isinstance(result, list)
|
|
105
|
+
assert len(result) == 0
|
|
106
|
+
|
|
107
|
+
def test_returns_empty_for_nonexistent_path(self):
|
|
108
|
+
"""Returns empty list for non-existent tests path."""
|
|
109
|
+
result = find_related_tests("anything", "nonexistent_tests_dir")
|
|
110
|
+
|
|
111
|
+
assert isinstance(result, list)
|
|
112
|
+
assert len(result) == 0
|
|
113
|
+
|
|
114
|
+
def test_finds_tests_in_subdirectories(self):
|
|
115
|
+
"""Finds tests in nested test directories."""
|
|
116
|
+
result = find_related_tests("graph", "tests")
|
|
117
|
+
|
|
118
|
+
# Should find tests in unit/ and possibly integration/
|
|
119
|
+
files = [t["file"] for t in result]
|
|
120
|
+
assert any("unit" in f for f in files)
|
|
121
|
+
|
|
122
|
+
def test_case_insensitive_search(self):
|
|
123
|
+
"""Symbol search is case-insensitive."""
|
|
124
|
+
# Search for something that exists in different cases
|
|
125
|
+
result_lower = find_related_tests("execute", "tests")
|
|
126
|
+
result_upper = find_related_tests("EXECUTE", "tests")
|
|
127
|
+
|
|
128
|
+
# Both should find some results
|
|
129
|
+
assert len(result_lower) > 0
|
|
130
|
+
assert len(result_upper) > 0
|
|
131
|
+
|
|
132
|
+
def test_skips_non_test_functions(self):
|
|
133
|
+
"""Only returns functions starting with test_."""
|
|
134
|
+
result = find_related_tests("execute_prompt", "tests")
|
|
135
|
+
|
|
136
|
+
for test in result:
|
|
137
|
+
assert test["test_name"].startswith("test_")
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class TestSearchInFile:
|
|
141
|
+
"""Tests for search_in_file function."""
|
|
142
|
+
|
|
143
|
+
def test_finds_matching_lines(self):
|
|
144
|
+
"""Finds lines containing the search pattern."""
|
|
145
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
|
|
146
|
+
f.write("def foo():\n timeout = 30\n return timeout\n")
|
|
147
|
+
temp_path = f.name
|
|
148
|
+
|
|
149
|
+
try:
|
|
150
|
+
result = search_in_file(temp_path, "timeout")
|
|
151
|
+
|
|
152
|
+
assert isinstance(result, list)
|
|
153
|
+
assert len(result) == 2
|
|
154
|
+
# Line 2 and 3 contain "timeout"
|
|
155
|
+
assert result[0]["line"] == 2
|
|
156
|
+
assert "timeout = 30" in result[0]["text"]
|
|
157
|
+
assert result[1]["line"] == 3
|
|
158
|
+
finally:
|
|
159
|
+
Path(temp_path).unlink()
|
|
160
|
+
|
|
161
|
+
def test_returns_error_for_missing_file(self):
|
|
162
|
+
"""Returns error dict for non-existent file."""
|
|
163
|
+
result = search_in_file("nonexistent_12345.py", "pattern")
|
|
164
|
+
|
|
165
|
+
assert isinstance(result, dict)
|
|
166
|
+
assert "error" in result
|
|
167
|
+
|
|
168
|
+
def test_returns_empty_for_no_matches(self):
|
|
169
|
+
"""Returns empty list when pattern not found."""
|
|
170
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
|
|
171
|
+
f.write("def foo():\n return 42\n")
|
|
172
|
+
temp_path = f.name
|
|
173
|
+
|
|
174
|
+
try:
|
|
175
|
+
result = search_in_file(temp_path, "nonexistent_pattern")
|
|
176
|
+
|
|
177
|
+
assert isinstance(result, list)
|
|
178
|
+
assert len(result) == 0
|
|
179
|
+
finally:
|
|
180
|
+
Path(temp_path).unlink()
|
|
181
|
+
|
|
182
|
+
def test_case_insensitive_by_default(self):
|
|
183
|
+
"""Search is case-insensitive by default."""
|
|
184
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
|
|
185
|
+
f.write("TIMEOUT = 30\ntimeout = 10\nTimeOut = 20\n")
|
|
186
|
+
temp_path = f.name
|
|
187
|
+
|
|
188
|
+
try:
|
|
189
|
+
result = search_in_file(temp_path, "timeout")
|
|
190
|
+
|
|
191
|
+
assert len(result) == 3
|
|
192
|
+
finally:
|
|
193
|
+
Path(temp_path).unlink()
|
|
194
|
+
|
|
195
|
+
def test_case_sensitive_option(self):
|
|
196
|
+
"""Can enable case-sensitive search."""
|
|
197
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
|
|
198
|
+
f.write("TIMEOUT = 30\ntimeout = 10\n")
|
|
199
|
+
temp_path = f.name
|
|
200
|
+
|
|
201
|
+
try:
|
|
202
|
+
result = search_in_file(temp_path, "timeout", case_sensitive=True)
|
|
203
|
+
|
|
204
|
+
assert len(result) == 1
|
|
205
|
+
assert result[0]["line"] == 2
|
|
206
|
+
finally:
|
|
207
|
+
Path(temp_path).unlink()
|
|
208
|
+
|
|
209
|
+
def test_limits_context(self):
|
|
210
|
+
"""Returns only matching lines, not context."""
|
|
211
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
|
|
212
|
+
f.write("line1\ntarget\nline3\n")
|
|
213
|
+
temp_path = f.name
|
|
214
|
+
|
|
215
|
+
try:
|
|
216
|
+
result = search_in_file(temp_path, "target")
|
|
217
|
+
|
|
218
|
+
assert len(result) == 1
|
|
219
|
+
assert "line1" not in result[0]["text"]
|
|
220
|
+
assert "target" in result[0]["text"]
|
|
221
|
+
finally:
|
|
222
|
+
Path(temp_path).unlink()
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
class TestSearchCodebase:
|
|
226
|
+
"""Tests for search_codebase function."""
|
|
227
|
+
|
|
228
|
+
def test_finds_matches_across_files(self):
|
|
229
|
+
"""Finds pattern matches in multiple files."""
|
|
230
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
231
|
+
# Create test files
|
|
232
|
+
(Path(tmpdir) / "file1.py").write_text("timeout = 30\n")
|
|
233
|
+
(Path(tmpdir) / "file2.py").write_text("max_timeout = 60\n")
|
|
234
|
+
(Path(tmpdir) / "file3.py").write_text("no match here\n")
|
|
235
|
+
|
|
236
|
+
result = search_codebase(tmpdir, "timeout")
|
|
237
|
+
|
|
238
|
+
assert isinstance(result, list)
|
|
239
|
+
assert len(result) == 2 # file1 and file2
|
|
240
|
+
|
|
241
|
+
# Each result should have file and matches
|
|
242
|
+
for r in result:
|
|
243
|
+
assert "file" in r
|
|
244
|
+
assert "matches" in r
|
|
245
|
+
assert len(r["matches"]) > 0
|
|
246
|
+
|
|
247
|
+
def test_respects_file_pattern(self):
|
|
248
|
+
"""Only searches files matching the pattern."""
|
|
249
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
250
|
+
(Path(tmpdir) / "code.py").write_text("timeout = 30\n")
|
|
251
|
+
(Path(tmpdir) / "config.yaml").write_text("timeout: 60\n")
|
|
252
|
+
|
|
253
|
+
result = search_codebase(tmpdir, "timeout", pattern="*.py")
|
|
254
|
+
|
|
255
|
+
assert len(result) == 1
|
|
256
|
+
assert "code.py" in result[0]["file"]
|
|
257
|
+
|
|
258
|
+
def test_returns_empty_for_no_matches(self):
|
|
259
|
+
"""Returns empty list when no matches found."""
|
|
260
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
261
|
+
(Path(tmpdir) / "file.py").write_text("def foo(): pass\n")
|
|
262
|
+
|
|
263
|
+
result = search_codebase(tmpdir, "nonexistent_xyz")
|
|
264
|
+
|
|
265
|
+
assert isinstance(result, list)
|
|
266
|
+
assert len(result) == 0
|
|
267
|
+
|
|
268
|
+
def test_searches_subdirectories(self):
|
|
269
|
+
"""Recursively searches subdirectories."""
|
|
270
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
271
|
+
subdir = Path(tmpdir) / "sub" / "dir"
|
|
272
|
+
subdir.mkdir(parents=True)
|
|
273
|
+
(subdir / "nested.py").write_text("timeout = 99\n")
|
|
274
|
+
|
|
275
|
+
result = search_codebase(tmpdir, "timeout")
|
|
276
|
+
|
|
277
|
+
assert len(result) == 1
|
|
278
|
+
assert "nested.py" in result[0]["file"]
|
|
279
|
+
|
|
280
|
+
def test_skips_pycache(self):
|
|
281
|
+
"""Skips __pycache__ directories."""
|
|
282
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
283
|
+
cache = Path(tmpdir) / "__pycache__"
|
|
284
|
+
cache.mkdir()
|
|
285
|
+
(cache / "cached.pyc").write_text("timeout = 30\n")
|
|
286
|
+
(Path(tmpdir) / "real.py").write_text("timeout = 30\n")
|
|
287
|
+
|
|
288
|
+
result = search_codebase(tmpdir, "timeout")
|
|
289
|
+
|
|
290
|
+
assert len(result) == 1
|
|
291
|
+
assert "__pycache__" not in result[0]["file"]
|
|
292
|
+
|
|
293
|
+
def test_returns_line_numbers(self):
|
|
294
|
+
"""Each match includes line number."""
|
|
295
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
296
|
+
(Path(tmpdir) / "test.py").write_text("line1\ntimeout = 30\nline3\n")
|
|
297
|
+
|
|
298
|
+
result = search_codebase(tmpdir, "timeout")
|
|
299
|
+
|
|
300
|
+
assert len(result) == 1
|
|
301
|
+
assert result[0]["matches"][0]["line"] == 2
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Tests for code navigation tools."""
|
|
2
|
+
|
|
3
|
+
import tempfile
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from examples.codegen.tools.code_nav import list_package_modules
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TestListPackageModules:
|
|
10
|
+
"""Tests for list_package_modules function."""
|
|
11
|
+
|
|
12
|
+
def test_finds_all_python_files_in_package(self):
|
|
13
|
+
"""Returns all .py files in the package."""
|
|
14
|
+
result = list_package_modules("yamlgraph/tools")
|
|
15
|
+
|
|
16
|
+
assert isinstance(result, list)
|
|
17
|
+
assert len(result) > 0
|
|
18
|
+
|
|
19
|
+
# Should find known files
|
|
20
|
+
files = [r["file"] for r in result]
|
|
21
|
+
assert any("shell.py" in f for f in files)
|
|
22
|
+
assert any("websearch.py" in f for f in files)
|
|
23
|
+
|
|
24
|
+
def test_includes_module_summaries(self):
|
|
25
|
+
"""Each module has docstring, classes, functions."""
|
|
26
|
+
result = list_package_modules("yamlgraph/tools")
|
|
27
|
+
|
|
28
|
+
for module in result:
|
|
29
|
+
assert "file" in module
|
|
30
|
+
assert "docstring" in module
|
|
31
|
+
assert "classes" in module
|
|
32
|
+
assert "functions" in module
|
|
33
|
+
assert isinstance(module["classes"], list)
|
|
34
|
+
assert isinstance(module["functions"], list)
|
|
35
|
+
|
|
36
|
+
def test_skips_files_with_syntax_errors(self):
|
|
37
|
+
"""Files with syntax errors are skipped."""
|
|
38
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
39
|
+
# Create a valid file
|
|
40
|
+
valid_file = Path(tmpdir) / "valid.py"
|
|
41
|
+
valid_file.write_text('"""Valid module."""\ndef foo(): pass')
|
|
42
|
+
|
|
43
|
+
# Create an invalid file
|
|
44
|
+
invalid_file = Path(tmpdir) / "invalid.py"
|
|
45
|
+
invalid_file.write_text("def broken(\n")
|
|
46
|
+
|
|
47
|
+
result = list_package_modules(tmpdir)
|
|
48
|
+
|
|
49
|
+
# Should have only the valid file
|
|
50
|
+
assert len(result) == 1
|
|
51
|
+
assert "valid.py" in result[0]["file"]
|
|
52
|
+
|
|
53
|
+
def test_returns_empty_list_for_nonexistent_path(self):
|
|
54
|
+
"""Returns empty list for non-existent directory."""
|
|
55
|
+
result = list_package_modules("nonexistent_directory_12345")
|
|
56
|
+
|
|
57
|
+
assert result == []
|
|
58
|
+
|
|
59
|
+
def test_returns_empty_list_for_empty_directory(self):
|
|
60
|
+
"""Returns empty list for directory with no Python files."""
|
|
61
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
62
|
+
result = list_package_modules(tmpdir)
|
|
63
|
+
assert result == []
|
|
64
|
+
|
|
65
|
+
def test_finds_nested_modules(self):
|
|
66
|
+
"""Finds Python files in subdirectories."""
|
|
67
|
+
result = list_package_modules("yamlgraph")
|
|
68
|
+
|
|
69
|
+
files = [r["file"] for r in result]
|
|
70
|
+
# Should find files in subdirectories
|
|
71
|
+
assert any("utils" in f for f in files)
|
|
72
|
+
assert any("models" in f for f in files)
|
|
73
|
+
assert any("tools" in f for f in files)
|
|
74
|
+
|
|
75
|
+
def test_class_names_are_strings(self):
|
|
76
|
+
"""Class names are extracted as strings, not dicts."""
|
|
77
|
+
result = list_package_modules("yamlgraph/models")
|
|
78
|
+
|
|
79
|
+
for module in result:
|
|
80
|
+
for cls_name in module["classes"]:
|
|
81
|
+
assert isinstance(cls_name, str)
|
|
82
|
+
|
|
83
|
+
def test_function_names_are_strings(self):
|
|
84
|
+
"""Function names are extracted as strings, not dicts."""
|
|
85
|
+
result = list_package_modules("yamlgraph/tools")
|
|
86
|
+
|
|
87
|
+
for module in result:
|
|
88
|
+
for func_name in module["functions"]:
|
|
89
|
+
assert isinstance(func_name, str)
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Tests for dependency analysis tools."""
|
|
2
|
+
|
|
3
|
+
import tempfile
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from examples.codegen.tools.dependency_tools import get_dependents, get_imports
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TestGetImports:
|
|
10
|
+
"""Tests for get_imports function."""
|
|
11
|
+
|
|
12
|
+
def test_extracts_import_statements(self):
|
|
13
|
+
"""Standard import statements are extracted."""
|
|
14
|
+
result = get_imports("yamlgraph/__init__.py")
|
|
15
|
+
|
|
16
|
+
assert "error" not in result
|
|
17
|
+
assert "imports" in result
|
|
18
|
+
assert isinstance(result["imports"], list)
|
|
19
|
+
|
|
20
|
+
def test_extracts_from_imports(self):
|
|
21
|
+
"""From X import Y statements are extracted."""
|
|
22
|
+
result = get_imports("yamlgraph/executor.py")
|
|
23
|
+
|
|
24
|
+
assert "error" not in result
|
|
25
|
+
assert "imports" in result
|
|
26
|
+
# executor.py has from imports
|
|
27
|
+
assert len(result["imports"]) > 0
|
|
28
|
+
|
|
29
|
+
def test_import_has_module_and_names(self):
|
|
30
|
+
"""Each import has module and imported names."""
|
|
31
|
+
result = get_imports("yamlgraph/executor.py")
|
|
32
|
+
|
|
33
|
+
assert "error" not in result
|
|
34
|
+
for imp in result["imports"]:
|
|
35
|
+
assert "module" in imp
|
|
36
|
+
# Names may be None for 'import X' vs 'from X import Y'
|
|
37
|
+
assert "names" in imp
|
|
38
|
+
|
|
39
|
+
def test_returns_error_for_invalid_file(self):
|
|
40
|
+
"""Returns error for non-existent file."""
|
|
41
|
+
result = get_imports("nonexistent/file.py")
|
|
42
|
+
|
|
43
|
+
assert "error" in result
|
|
44
|
+
|
|
45
|
+
def test_returns_error_for_syntax_error(self):
|
|
46
|
+
"""Returns error for file with syntax errors."""
|
|
47
|
+
with tempfile.NamedTemporaryFile(suffix=".py", delete=False, mode="w") as f:
|
|
48
|
+
f.write("def broken(\n return")
|
|
49
|
+
temp_path = f.name
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
result = get_imports(temp_path)
|
|
53
|
+
assert "error" in result
|
|
54
|
+
finally:
|
|
55
|
+
Path(temp_path).unlink()
|
|
56
|
+
|
|
57
|
+
def test_handles_aliased_imports(self):
|
|
58
|
+
"""Handles 'import X as Y' and 'from X import Y as Z'."""
|
|
59
|
+
with tempfile.NamedTemporaryFile(suffix=".py", delete=False, mode="w") as f:
|
|
60
|
+
f.write("import numpy as np\nfrom pathlib import Path as P")
|
|
61
|
+
temp_path = f.name
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
result = get_imports(temp_path)
|
|
65
|
+
assert "error" not in result
|
|
66
|
+
assert len(result["imports"]) >= 2
|
|
67
|
+
finally:
|
|
68
|
+
Path(temp_path).unlink()
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class TestGetDependents:
|
|
72
|
+
"""Tests for get_dependents function."""
|
|
73
|
+
|
|
74
|
+
def test_finds_files_that_import_module(self):
|
|
75
|
+
"""Finds files that import a given module."""
|
|
76
|
+
# yamlgraph.executor is imported by many files
|
|
77
|
+
result = get_dependents("yamlgraph.executor", "yamlgraph")
|
|
78
|
+
|
|
79
|
+
assert "error" not in result
|
|
80
|
+
assert "dependents" in result
|
|
81
|
+
assert isinstance(result["dependents"], list)
|
|
82
|
+
|
|
83
|
+
def test_returns_file_paths(self):
|
|
84
|
+
"""Returns list of file paths."""
|
|
85
|
+
result = get_dependents("yamlgraph.config", "yamlgraph")
|
|
86
|
+
|
|
87
|
+
assert "error" not in result
|
|
88
|
+
for dep in result["dependents"]:
|
|
89
|
+
assert isinstance(dep, str)
|
|
90
|
+
assert dep.endswith(".py")
|
|
91
|
+
|
|
92
|
+
def test_empty_for_unused_module(self):
|
|
93
|
+
"""Returns empty list for module with no dependents."""
|
|
94
|
+
result = get_dependents("nonexistent.module.that.nobody.imports", "yamlgraph")
|
|
95
|
+
|
|
96
|
+
assert "error" not in result
|
|
97
|
+
assert result["dependents"] == []
|
|
98
|
+
|
|
99
|
+
def test_returns_error_for_invalid_project(self):
|
|
100
|
+
"""Returns error for non-existent project path."""
|
|
101
|
+
result = get_dependents("some.module", "/nonexistent/path")
|
|
102
|
+
|
|
103
|
+
assert "error" in result
|
|
104
|
+
|
|
105
|
+
def test_finds_from_import_style(self):
|
|
106
|
+
"""Finds 'from X import Y' style imports."""
|
|
107
|
+
result = get_dependents("yamlgraph.utils.prompts", "yamlgraph")
|
|
108
|
+
|
|
109
|
+
assert "error" not in result
|
|
110
|
+
# prompts is used in multiple places
|
|
111
|
+
assert len(result["dependents"]) >= 0 # May be 0 if not imported directly
|
|
112
|
+
|
|
113
|
+
def test_finds_import_module_style(self):
|
|
114
|
+
"""Finds 'import X' style imports."""
|
|
115
|
+
# logging is imported in many places
|
|
116
|
+
result = get_dependents("logging", "yamlgraph")
|
|
117
|
+
|
|
118
|
+
# This searches for 'import logging' or 'from logging import'
|
|
119
|
+
assert "error" not in result
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"""Tests for example discovery tools."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from examples.codegen.tools.example_tools import (
|
|
6
|
+
find_error_handling,
|
|
7
|
+
find_example,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
# ============================================================================
|
|
11
|
+
# find_example tests
|
|
12
|
+
# ============================================================================
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TestFindExample:
|
|
16
|
+
"""Tests for find_example."""
|
|
17
|
+
|
|
18
|
+
def test_finds_function_usage(self, tmp_path: Path):
|
|
19
|
+
"""Find usage examples of a function."""
|
|
20
|
+
# Create a source file
|
|
21
|
+
src = tmp_path / "mymodule.py"
|
|
22
|
+
src.write_text('''
|
|
23
|
+
def greet(name: str) -> str:
|
|
24
|
+
"""Say hello."""
|
|
25
|
+
return f"Hello, {name}!"
|
|
26
|
+
''')
|
|
27
|
+
# Create a file that uses it
|
|
28
|
+
user = tmp_path / "main.py"
|
|
29
|
+
user.write_text("""
|
|
30
|
+
from mymodule import greet
|
|
31
|
+
|
|
32
|
+
def run():
|
|
33
|
+
result = greet("World")
|
|
34
|
+
print(result)
|
|
35
|
+
""")
|
|
36
|
+
result = find_example("greet", str(tmp_path))
|
|
37
|
+
|
|
38
|
+
assert "examples" in result
|
|
39
|
+
assert len(result["examples"]) >= 1
|
|
40
|
+
example = result["examples"][0]
|
|
41
|
+
assert "file" in example
|
|
42
|
+
assert "line" in example
|
|
43
|
+
assert "snippet" in example
|
|
44
|
+
assert "greet" in example["snippet"]
|
|
45
|
+
|
|
46
|
+
def test_prioritizes_test_files(self, tmp_path: Path):
|
|
47
|
+
"""Test files should appear first in examples."""
|
|
48
|
+
# Source
|
|
49
|
+
src = tmp_path / "utils.py"
|
|
50
|
+
src.write_text("def helper(): pass")
|
|
51
|
+
|
|
52
|
+
# Regular usage
|
|
53
|
+
main = tmp_path / "main.py"
|
|
54
|
+
main.write_text("from utils import helper\nhelper()")
|
|
55
|
+
|
|
56
|
+
# Test usage
|
|
57
|
+
tests = tmp_path / "tests"
|
|
58
|
+
tests.mkdir()
|
|
59
|
+
test_file = tests / "test_utils.py"
|
|
60
|
+
test_file.write_text("""
|
|
61
|
+
from utils import helper
|
|
62
|
+
|
|
63
|
+
def test_helper():
|
|
64
|
+
result = helper()
|
|
65
|
+
assert result is None
|
|
66
|
+
""")
|
|
67
|
+
result = find_example("helper", str(tmp_path))
|
|
68
|
+
|
|
69
|
+
assert "examples" in result
|
|
70
|
+
# First example should be from test file
|
|
71
|
+
if result["examples"]:
|
|
72
|
+
first = result["examples"][0]
|
|
73
|
+
assert "test" in first["file"].lower()
|
|
74
|
+
|
|
75
|
+
def test_limits_max_examples(self, tmp_path: Path):
|
|
76
|
+
"""Respects max_examples parameter."""
|
|
77
|
+
src = tmp_path / "funcs.py"
|
|
78
|
+
src.write_text("def foo(): pass")
|
|
79
|
+
|
|
80
|
+
for i in range(5):
|
|
81
|
+
f = tmp_path / f"user{i}.py"
|
|
82
|
+
f.write_text(f"from funcs import foo\nfoo() # usage {i}")
|
|
83
|
+
|
|
84
|
+
result = find_example("foo", str(tmp_path), max_examples=2)
|
|
85
|
+
|
|
86
|
+
assert "examples" in result
|
|
87
|
+
assert len(result["examples"]) <= 2
|
|
88
|
+
|
|
89
|
+
def test_handles_no_examples(self, tmp_path: Path):
|
|
90
|
+
"""Returns empty list when no examples found."""
|
|
91
|
+
src = tmp_path / "module.py"
|
|
92
|
+
src.write_text("def unused(): pass")
|
|
93
|
+
|
|
94
|
+
result = find_example("unused", str(tmp_path))
|
|
95
|
+
|
|
96
|
+
assert "examples" in result
|
|
97
|
+
assert len(result["examples"]) == 0
|
|
98
|
+
|
|
99
|
+
def test_handles_missing_project(self):
|
|
100
|
+
"""Returns error for missing project path."""
|
|
101
|
+
result = find_example("symbol", "/nonexistent/path")
|
|
102
|
+
assert "error" in result
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
# ============================================================================
|
|
106
|
+
# find_error_handling tests
|
|
107
|
+
# ============================================================================
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class TestFindErrorHandling:
|
|
111
|
+
"""Tests for find_error_handling."""
|
|
112
|
+
|
|
113
|
+
def test_finds_exception_types(self, tmp_path: Path):
|
|
114
|
+
"""Discovers exception types used in project."""
|
|
115
|
+
src = tmp_path / "handler.py"
|
|
116
|
+
src.write_text("""
|
|
117
|
+
try:
|
|
118
|
+
risky_operation()
|
|
119
|
+
except ValueError as e:
|
|
120
|
+
print(f"Value error: {e}")
|
|
121
|
+
except KeyError:
|
|
122
|
+
pass
|
|
123
|
+
except (TypeError, AttributeError):
|
|
124
|
+
pass
|
|
125
|
+
""")
|
|
126
|
+
result = find_error_handling(str(tmp_path))
|
|
127
|
+
|
|
128
|
+
assert "exceptions" in result
|
|
129
|
+
exceptions = result["exceptions"]
|
|
130
|
+
assert "ValueError" in exceptions
|
|
131
|
+
assert "KeyError" in exceptions
|
|
132
|
+
|
|
133
|
+
def test_finds_dict_error_pattern(self, tmp_path: Path):
|
|
134
|
+
"""Detects dict-based error returns."""
|
|
135
|
+
src = tmp_path / "tool.py"
|
|
136
|
+
src.write_text("""
|
|
137
|
+
def process(data):
|
|
138
|
+
if not data:
|
|
139
|
+
return {"error": "No data provided"}
|
|
140
|
+
try:
|
|
141
|
+
result = compute(data)
|
|
142
|
+
return {"result": result}
|
|
143
|
+
except Exception as e:
|
|
144
|
+
return {"error": str(e)}
|
|
145
|
+
""")
|
|
146
|
+
result = find_error_handling(str(tmp_path))
|
|
147
|
+
|
|
148
|
+
assert "patterns" in result
|
|
149
|
+
patterns = result["patterns"]
|
|
150
|
+
assert "dict_error" in patterns or any("dict" in p.lower() for p in patterns)
|
|
151
|
+
|
|
152
|
+
def test_finds_logging_patterns(self, tmp_path: Path):
|
|
153
|
+
"""Detects error logging patterns."""
|
|
154
|
+
src = tmp_path / "service.py"
|
|
155
|
+
src.write_text("""
|
|
156
|
+
import logging
|
|
157
|
+
|
|
158
|
+
logger = logging.getLogger(__name__)
|
|
159
|
+
|
|
160
|
+
def do_work():
|
|
161
|
+
try:
|
|
162
|
+
work()
|
|
163
|
+
except Exception as e:
|
|
164
|
+
logger.error(f"Failed: {e}")
|
|
165
|
+
raise
|
|
166
|
+
""")
|
|
167
|
+
result = find_error_handling(str(tmp_path))
|
|
168
|
+
|
|
169
|
+
assert "logging" in result
|
|
170
|
+
assert result["logging"]["uses_logging"] is True
|
|
171
|
+
|
|
172
|
+
def test_handles_no_error_handling(self, tmp_path: Path):
|
|
173
|
+
"""Returns empty patterns for code without error handling."""
|
|
174
|
+
src = tmp_path / "simple.py"
|
|
175
|
+
src.write_text("x = 1 + 1\nprint(x)")
|
|
176
|
+
|
|
177
|
+
result = find_error_handling(str(tmp_path))
|
|
178
|
+
|
|
179
|
+
assert "exceptions" in result
|
|
180
|
+
assert len(result["exceptions"]) == 0
|
|
181
|
+
|
|
182
|
+
def test_handles_missing_project(self):
|
|
183
|
+
"""Returns error for missing project path."""
|
|
184
|
+
result = find_error_handling("/nonexistent/path")
|
|
185
|
+
assert "error" in result
|