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.
Files changed (185) hide show
  1. examples/__init__.py +1 -0
  2. examples/codegen/__init__.py +5 -0
  3. examples/codegen/models/__init__.py +13 -0
  4. examples/codegen/models/schemas.py +76 -0
  5. examples/codegen/tests/__init__.py +1 -0
  6. examples/codegen/tests/test_ai_helpers.py +235 -0
  7. examples/codegen/tests/test_ast_analysis.py +174 -0
  8. examples/codegen/tests/test_code_analysis.py +134 -0
  9. examples/codegen/tests/test_code_context.py +301 -0
  10. examples/codegen/tests/test_code_nav.py +89 -0
  11. examples/codegen/tests/test_dependency_tools.py +119 -0
  12. examples/codegen/tests/test_example_tools.py +185 -0
  13. examples/codegen/tests/test_git_tools.py +112 -0
  14. examples/codegen/tests/test_impl_agent_schemas.py +193 -0
  15. examples/codegen/tests/test_impl_agent_v4_graph.py +94 -0
  16. examples/codegen/tests/test_jedi_analysis.py +226 -0
  17. examples/codegen/tests/test_meta_tools.py +250 -0
  18. examples/codegen/tests/test_plan_discovery_prompt.py +98 -0
  19. examples/codegen/tests/test_syntax_tools.py +85 -0
  20. examples/codegen/tests/test_synthesize_prompt.py +94 -0
  21. examples/codegen/tests/test_template_tools.py +244 -0
  22. examples/codegen/tools/__init__.py +80 -0
  23. examples/codegen/tools/ai_helpers.py +420 -0
  24. examples/codegen/tools/ast_analysis.py +92 -0
  25. examples/codegen/tools/code_context.py +180 -0
  26. examples/codegen/tools/code_nav.py +52 -0
  27. examples/codegen/tools/dependency_tools.py +120 -0
  28. examples/codegen/tools/example_tools.py +188 -0
  29. examples/codegen/tools/git_tools.py +151 -0
  30. examples/codegen/tools/impl_executor.py +614 -0
  31. examples/codegen/tools/jedi_analysis.py +311 -0
  32. examples/codegen/tools/meta_tools.py +202 -0
  33. examples/codegen/tools/syntax_tools.py +26 -0
  34. examples/codegen/tools/template_tools.py +356 -0
  35. examples/fastapi_interview.py +167 -0
  36. examples/npc/api/__init__.py +1 -0
  37. examples/npc/api/app.py +100 -0
  38. examples/npc/api/routes/__init__.py +5 -0
  39. examples/npc/api/routes/encounter.py +182 -0
  40. examples/npc/api/session.py +330 -0
  41. examples/npc/demo.py +387 -0
  42. examples/npc/nodes/__init__.py +5 -0
  43. examples/npc/nodes/image_node.py +92 -0
  44. examples/npc/run_encounter.py +230 -0
  45. examples/shared/__init__.py +0 -0
  46. examples/shared/replicate_tool.py +238 -0
  47. examples/storyboard/__init__.py +1 -0
  48. examples/storyboard/generate_videos.py +335 -0
  49. examples/storyboard/nodes/__init__.py +12 -0
  50. examples/storyboard/nodes/animated_character_node.py +248 -0
  51. examples/storyboard/nodes/animated_image_node.py +138 -0
  52. examples/storyboard/nodes/character_node.py +162 -0
  53. examples/storyboard/nodes/image_node.py +118 -0
  54. examples/storyboard/nodes/replicate_tool.py +49 -0
  55. examples/storyboard/retry_images.py +118 -0
  56. scripts/demo_async_executor.py +212 -0
  57. scripts/demo_interview_e2e.py +200 -0
  58. scripts/demo_streaming.py +140 -0
  59. scripts/run_interview_demo.py +94 -0
  60. scripts/test_interrupt_fix.py +26 -0
  61. tests/__init__.py +1 -0
  62. tests/conftest.py +178 -0
  63. tests/integration/__init__.py +1 -0
  64. tests/integration/test_animated_storyboard.py +63 -0
  65. tests/integration/test_cli_commands.py +242 -0
  66. tests/integration/test_colocated_prompts.py +139 -0
  67. tests/integration/test_map_demo.py +50 -0
  68. tests/integration/test_memory_demo.py +283 -0
  69. tests/integration/test_npc_api/__init__.py +1 -0
  70. tests/integration/test_npc_api/test_routes.py +357 -0
  71. tests/integration/test_npc_api/test_session.py +216 -0
  72. tests/integration/test_pipeline_flow.py +105 -0
  73. tests/integration/test_providers.py +163 -0
  74. tests/integration/test_resume.py +75 -0
  75. tests/integration/test_subgraph_integration.py +295 -0
  76. tests/integration/test_subgraph_interrupt.py +106 -0
  77. tests/unit/__init__.py +1 -0
  78. tests/unit/test_agent_nodes.py +355 -0
  79. tests/unit/test_async_executor.py +346 -0
  80. tests/unit/test_checkpointer.py +212 -0
  81. tests/unit/test_checkpointer_factory.py +212 -0
  82. tests/unit/test_cli.py +121 -0
  83. tests/unit/test_cli_package.py +81 -0
  84. tests/unit/test_compile_graph_map.py +132 -0
  85. tests/unit/test_conditions_routing.py +253 -0
  86. tests/unit/test_config.py +93 -0
  87. tests/unit/test_conversation_memory.py +276 -0
  88. tests/unit/test_database.py +145 -0
  89. tests/unit/test_deprecation.py +104 -0
  90. tests/unit/test_executor.py +172 -0
  91. tests/unit/test_executor_async.py +179 -0
  92. tests/unit/test_export.py +149 -0
  93. tests/unit/test_expressions.py +178 -0
  94. tests/unit/test_feature_brainstorm.py +194 -0
  95. tests/unit/test_format_prompt.py +145 -0
  96. tests/unit/test_generic_report.py +200 -0
  97. tests/unit/test_graph_commands.py +327 -0
  98. tests/unit/test_graph_linter.py +627 -0
  99. tests/unit/test_graph_loader.py +357 -0
  100. tests/unit/test_graph_schema.py +193 -0
  101. tests/unit/test_inline_schema.py +151 -0
  102. tests/unit/test_interrupt_node.py +182 -0
  103. tests/unit/test_issues.py +164 -0
  104. tests/unit/test_jinja2_prompts.py +85 -0
  105. tests/unit/test_json_extract.py +134 -0
  106. tests/unit/test_langsmith.py +600 -0
  107. tests/unit/test_langsmith_tools.py +204 -0
  108. tests/unit/test_llm_factory.py +109 -0
  109. tests/unit/test_llm_factory_async.py +118 -0
  110. tests/unit/test_loops.py +403 -0
  111. tests/unit/test_map_node.py +144 -0
  112. tests/unit/test_no_backward_compat.py +56 -0
  113. tests/unit/test_node_factory.py +348 -0
  114. tests/unit/test_passthrough_node.py +126 -0
  115. tests/unit/test_prompts.py +324 -0
  116. tests/unit/test_python_nodes.py +198 -0
  117. tests/unit/test_reliability.py +298 -0
  118. tests/unit/test_result_export.py +234 -0
  119. tests/unit/test_router.py +296 -0
  120. tests/unit/test_sanitize.py +99 -0
  121. tests/unit/test_schema_loader.py +295 -0
  122. tests/unit/test_shell_tools.py +229 -0
  123. tests/unit/test_state_builder.py +331 -0
  124. tests/unit/test_state_builder_map.py +104 -0
  125. tests/unit/test_state_config.py +197 -0
  126. tests/unit/test_streaming.py +307 -0
  127. tests/unit/test_subgraph.py +596 -0
  128. tests/unit/test_template.py +190 -0
  129. tests/unit/test_tool_call_integration.py +164 -0
  130. tests/unit/test_tool_call_node.py +178 -0
  131. tests/unit/test_tool_nodes.py +129 -0
  132. tests/unit/test_websearch.py +234 -0
  133. yamlgraph/__init__.py +35 -0
  134. yamlgraph/builder.py +110 -0
  135. yamlgraph/cli/__init__.py +159 -0
  136. yamlgraph/cli/__main__.py +6 -0
  137. yamlgraph/cli/commands.py +231 -0
  138. yamlgraph/cli/deprecation.py +92 -0
  139. yamlgraph/cli/graph_commands.py +541 -0
  140. yamlgraph/cli/validators.py +37 -0
  141. yamlgraph/config.py +67 -0
  142. yamlgraph/constants.py +70 -0
  143. yamlgraph/error_handlers.py +227 -0
  144. yamlgraph/executor.py +290 -0
  145. yamlgraph/executor_async.py +288 -0
  146. yamlgraph/graph_loader.py +451 -0
  147. yamlgraph/map_compiler.py +150 -0
  148. yamlgraph/models/__init__.py +36 -0
  149. yamlgraph/models/graph_schema.py +181 -0
  150. yamlgraph/models/schemas.py +124 -0
  151. yamlgraph/models/state_builder.py +236 -0
  152. yamlgraph/node_factory.py +768 -0
  153. yamlgraph/routing.py +87 -0
  154. yamlgraph/schema_loader.py +240 -0
  155. yamlgraph/storage/__init__.py +20 -0
  156. yamlgraph/storage/checkpointer.py +72 -0
  157. yamlgraph/storage/checkpointer_factory.py +123 -0
  158. yamlgraph/storage/database.py +320 -0
  159. yamlgraph/storage/export.py +269 -0
  160. yamlgraph/tools/__init__.py +1 -0
  161. yamlgraph/tools/agent.py +320 -0
  162. yamlgraph/tools/graph_linter.py +388 -0
  163. yamlgraph/tools/langsmith_tools.py +125 -0
  164. yamlgraph/tools/nodes.py +126 -0
  165. yamlgraph/tools/python_tool.py +179 -0
  166. yamlgraph/tools/shell.py +205 -0
  167. yamlgraph/tools/websearch.py +242 -0
  168. yamlgraph/utils/__init__.py +48 -0
  169. yamlgraph/utils/conditions.py +157 -0
  170. yamlgraph/utils/expressions.py +245 -0
  171. yamlgraph/utils/json_extract.py +104 -0
  172. yamlgraph/utils/langsmith.py +416 -0
  173. yamlgraph/utils/llm_factory.py +118 -0
  174. yamlgraph/utils/llm_factory_async.py +105 -0
  175. yamlgraph/utils/logging.py +104 -0
  176. yamlgraph/utils/prompts.py +171 -0
  177. yamlgraph/utils/sanitize.py +98 -0
  178. yamlgraph/utils/template.py +102 -0
  179. yamlgraph/utils/validators.py +181 -0
  180. yamlgraph-0.3.9.dist-info/METADATA +1105 -0
  181. yamlgraph-0.3.9.dist-info/RECORD +185 -0
  182. yamlgraph-0.3.9.dist-info/WHEEL +5 -0
  183. yamlgraph-0.3.9.dist-info/entry_points.txt +2 -0
  184. yamlgraph-0.3.9.dist-info/licenses/LICENSE +33 -0
  185. 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