lyceum-cli 1.0.25__py3-none-any.whl → 1.0.27__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 (34) hide show
  1. lyceum/external/auth/login.py +18 -18
  2. lyceum/external/compute/execution/docker.py +4 -2
  3. lyceum/external/compute/execution/docker_compose.py +263 -0
  4. lyceum/external/compute/execution/notebook.py +0 -2
  5. lyceum/external/compute/execution/python.py +2 -1
  6. lyceum/external/compute/inference/batch.py +8 -10
  7. lyceum/external/vms/instances.py +301 -0
  8. lyceum/external/vms/management.py +383 -0
  9. lyceum/main.py +3 -0
  10. lyceum/shared/config.py +19 -24
  11. lyceum/shared/display.py +12 -31
  12. lyceum/shared/streaming.py +17 -45
  13. {lyceum_cli-1.0.25.dist-info → lyceum_cli-1.0.27.dist-info}/METADATA +1 -1
  14. lyceum_cli-1.0.27.dist-info/RECORD +34 -0
  15. {lyceum_cli-1.0.25.dist-info → lyceum_cli-1.0.27.dist-info}/WHEEL +1 -1
  16. {lyceum_cli-1.0.25.dist-info → lyceum_cli-1.0.27.dist-info}/top_level.txt +0 -1
  17. lyceum/external/compute/execution/docker_config.py +0 -123
  18. lyceum/external/storage/files.py +0 -273
  19. lyceum_cli-1.0.25.dist-info/RECORD +0 -46
  20. tests/__init__.py +0 -1
  21. tests/conftest.py +0 -200
  22. tests/unit/__init__.py +0 -1
  23. tests/unit/external/__init__.py +0 -1
  24. tests/unit/external/compute/__init__.py +0 -1
  25. tests/unit/external/compute/execution/__init__.py +0 -1
  26. tests/unit/external/compute/execution/test_data.py +0 -33
  27. tests/unit/external/compute/execution/test_dependency_resolver.py +0 -257
  28. tests/unit/external/compute/execution/test_python_helpers.py +0 -406
  29. tests/unit/external/compute/execution/test_python_run.py +0 -289
  30. tests/unit/shared/__init__.py +0 -1
  31. tests/unit/shared/test_config.py +0 -341
  32. tests/unit/shared/test_streaming.py +0 -259
  33. /lyceum/external/{storage → vms}/__init__.py +0 -0
  34. {lyceum_cli-1.0.25.dist-info → lyceum_cli-1.0.27.dist-info}/entry_points.txt +0 -0
@@ -1,257 +0,0 @@
1
- """Tests for DependencyResolver class."""
2
-
3
- import sys
4
- from pathlib import Path
5
- from unittest.mock import MagicMock, patch
6
-
7
- import pytest
8
-
9
- from lyceum.external.compute.execution.python import DependencyResolver
10
-
11
-
12
- class TestDependencyResolverIsStdlib:
13
- """Tests for DependencyResolver.is_stdlib() method."""
14
-
15
- def test_returns_false_for_local_path(self, sample_workspace):
16
- resolver = DependencyResolver(sample_workspace)
17
- local_file = sample_workspace / "mypackage" / "utils.py"
18
-
19
- assert resolver.is_stdlib(local_file) is False
20
-
21
- def test_returns_false_for_workspace_path(self, sample_workspace):
22
- resolver = DependencyResolver(sample_workspace)
23
- main_file = sample_workspace / "main.py"
24
-
25
- assert resolver.is_stdlib(main_file) is False
26
-
27
-
28
- class TestDependencyResolverIsStdlibModule:
29
- """Tests for DependencyResolver.is_stdlib_module() method."""
30
-
31
- def test_json_is_stdlib(self):
32
- # json has a file origin in the stdlib
33
- resolver = DependencyResolver(Path("/tmp"))
34
- assert resolver.is_stdlib_module("json") is True
35
-
36
- def test_pathlib_is_stdlib(self):
37
- resolver = DependencyResolver(Path("/tmp"))
38
- assert resolver.is_stdlib_module("pathlib") is True
39
-
40
- def test_re_is_stdlib(self):
41
- resolver = DependencyResolver(Path("/tmp"))
42
- assert resolver.is_stdlib_module("re") is True
43
-
44
- def test_collections_is_stdlib(self):
45
- resolver = DependencyResolver(Path("/tmp"))
46
- assert resolver.is_stdlib_module("collections") is True
47
-
48
- def test_nonexistent_module(self):
49
- resolver = DependencyResolver(Path("/tmp"))
50
- assert resolver.is_stdlib_module("nonexistent_module_xyz_abc_123") is False
51
-
52
-
53
- class TestDependencyResolverFindImports:
54
- """Tests for DependencyResolver.find_imports() method."""
55
-
56
- def test_finds_local_imports(self, sample_workspace):
57
- sys.path.insert(0, str(sample_workspace))
58
- try:
59
- resolver = DependencyResolver(sample_workspace)
60
- main_file = sample_workspace / "main.py"
61
- resolver.find_imports(main_file, main_file)
62
-
63
- # Should find mypackage and standalone
64
- local_files = {p.name for p in resolver.local_imports}
65
- assert "standalone.py" in local_files
66
- # At minimum, we should find some local files
67
- assert len(resolver.local_imports) > 0
68
- finally:
69
- sys.path.remove(str(sample_workspace))
70
-
71
- def test_excludes_stdlib(self, sample_workspace):
72
- sys.path.insert(0, str(sample_workspace))
73
- try:
74
- resolver = DependencyResolver(sample_workspace)
75
- main_file = sample_workspace / "main.py"
76
- resolver.find_imports(main_file, main_file)
77
-
78
- # Should not include stdlib modules like os, sys, etc.
79
- for path in resolver.local_imports:
80
- # None of the local imports should be from site-packages
81
- assert "site-packages" not in str(path)
82
- finally:
83
- sys.path.remove(str(sample_workspace))
84
-
85
- def test_handles_relative_imports(self, sample_workspace):
86
- sys.path.insert(0, str(sample_workspace))
87
- try:
88
- resolver = DependencyResolver(sample_workspace)
89
- init_file = sample_workspace / "mypackage" / "__init__.py"
90
- main_file = sample_workspace / "main.py"
91
-
92
- resolver.find_imports(init_file, main_file)
93
-
94
- # Should resolve relative import to utils
95
- paths = [str(p) for p in resolver.local_imports]
96
- assert any("utils.py" in p for p in paths)
97
- finally:
98
- sys.path.remove(str(sample_workspace))
99
-
100
- def test_handles_nonexistent_file(self, sample_workspace):
101
- resolver = DependencyResolver(sample_workspace)
102
- nonexistent = sample_workspace / "nonexistent.py"
103
-
104
- # Should not raise
105
- resolver.find_imports(nonexistent, nonexistent)
106
- assert len(resolver.local_imports) == 0
107
-
108
- def test_handles_syntax_error_file(self, tmp_path):
109
- bad_file = tmp_path / "bad_syntax.py"
110
- bad_file.write_text("def broken(:\n pass")
111
-
112
- resolver = DependencyResolver(tmp_path)
113
- # Should not raise, just skip the file
114
- resolver.find_imports(bad_file, bad_file)
115
-
116
- def test_handles_directory(self, sample_workspace):
117
- sys.path.insert(0, str(sample_workspace))
118
- try:
119
- resolver = DependencyResolver(sample_workspace)
120
- pkg_dir = sample_workspace / "mypackage"
121
-
122
- # Should follow directory to __init__.py
123
- resolver.find_imports(pkg_dir, sample_workspace / "main.py")
124
-
125
- # Should have processed the package
126
- assert len(resolver.visited) > 0
127
- finally:
128
- sys.path.remove(str(sample_workspace))
129
-
130
- def test_visited_set_prevents_infinite_recursion(self, sample_workspace):
131
- sys.path.insert(0, str(sample_workspace))
132
- try:
133
- resolver = DependencyResolver(sample_workspace)
134
- main_file = sample_workspace / "main.py"
135
-
136
- # Call find_imports multiple times on same file
137
- resolver.find_imports(main_file, main_file)
138
- visited_count = len(resolver.visited)
139
-
140
- resolver.find_imports(main_file, main_file)
141
- # Should not add more to visited
142
- assert len(resolver.visited) == visited_count
143
- finally:
144
- sys.path.remove(str(sample_workspace))
145
-
146
-
147
- class TestDependencyResolverCalculateImportPath:
148
- """Tests for DependencyResolver.calculate_import_path() method."""
149
-
150
- def test_relative_import_path(self, sample_workspace):
151
- resolver = DependencyResolver(sample_workspace)
152
- actual_path = sample_workspace / "mypackage" / "utils.py"
153
- main_file = sample_workspace / "main.py"
154
-
155
- result = resolver.calculate_import_path(
156
- actual_path.resolve(), main_file.resolve(), modname="utils", is_relative=True
157
- )
158
-
159
- assert result == "mypackage/utils.py"
160
-
161
- def test_absolute_import_path_init(self, sample_workspace):
162
- resolver = DependencyResolver(sample_workspace)
163
- actual_path = sample_workspace / "mypackage" / "__init__.py"
164
- main_file = sample_workspace / "main.py"
165
-
166
- result = resolver.calculate_import_path(
167
- actual_path.resolve(), main_file.resolve(), modname="mypackage", is_relative=False
168
- )
169
-
170
- assert result == "mypackage/__init__.py"
171
-
172
- def test_absolute_import_path_module(self, sample_workspace):
173
- resolver = DependencyResolver(sample_workspace)
174
- actual_path = sample_workspace / "standalone.py"
175
- main_file = sample_workspace / "main.py"
176
-
177
- result = resolver.calculate_import_path(
178
- actual_path.resolve(), main_file.resolve(), modname="standalone", is_relative=False
179
- )
180
-
181
- assert result == "standalone.py"
182
-
183
-
184
- class TestDependencyResolverClassifyFile:
185
- """Tests for DependencyResolver.classify_file() method."""
186
-
187
- def test_local_file_in_project(self, sample_workspace):
188
- resolver = DependencyResolver(sample_workspace)
189
- local_file = sample_workspace / "main.py"
190
-
191
- assert resolver.classify_file(local_file) == "local"
192
-
193
- def test_local_file_in_subpackage(self, sample_workspace):
194
- resolver = DependencyResolver(sample_workspace)
195
- pkg_file = sample_workspace / "mypackage" / "utils.py"
196
-
197
- assert resolver.classify_file(pkg_file) == "local"
198
-
199
-
200
- class TestDependencyResolverResolveRelativeImport:
201
- """Tests for DependencyResolver.resolve_relative_import() method."""
202
-
203
- def test_resolves_relative_module(self, sample_workspace):
204
- import ast
205
-
206
- resolver = DependencyResolver(sample_workspace)
207
- init_file = sample_workspace / "mypackage" / "__init__.py"
208
-
209
- # Create a mock ImportFrom node for "from .utils import helper_function"
210
- node = ast.ImportFrom(module="utils", names=[], level=1)
211
-
212
- result = resolver.resolve_relative_import(init_file, node, "utils")
213
-
214
- assert result is not None
215
- assert "utils.py" in str(result)
216
-
217
- def test_resolves_package_level_import(self, sample_workspace):
218
- import ast
219
-
220
- resolver = DependencyResolver(sample_workspace)
221
- utils_file = sample_workspace / "mypackage" / "utils.py"
222
-
223
- # Create a mock ImportFrom node for "from . import something" (level=1, no module)
224
- node = ast.ImportFrom(module=None, names=[], level=1)
225
-
226
- result = resolver.resolve_relative_import(utils_file, node, None)
227
-
228
- # Should resolve to __init__.py of the package
229
- if result:
230
- assert "__init__.py" in str(result)
231
-
232
-
233
- class TestDependencyResolverParseFile:
234
- """Tests for DependencyResolver.parse_file() method."""
235
-
236
- def test_parses_valid_file(self, sample_workspace):
237
- resolver = DependencyResolver(sample_workspace)
238
- main_file = sample_workspace / "main.py"
239
-
240
- result = resolver.parse_file(main_file)
241
-
242
- assert result is not None
243
-
244
- def test_returns_none_for_syntax_error(self, tmp_path):
245
- bad_file = tmp_path / "bad.py"
246
- bad_file.write_text("def broken(:\n pass")
247
-
248
- resolver = DependencyResolver(tmp_path)
249
- result = resolver.parse_file(bad_file)
250
-
251
- assert result is None
252
-
253
- def test_returns_none_for_nonexistent(self, tmp_path):
254
- resolver = DependencyResolver(tmp_path)
255
- result = resolver.parse_file(tmp_path / "nonexistent.py")
256
-
257
- assert result is None
@@ -1,406 +0,0 @@
1
- """Tests for Python execution helper functions."""
2
-
3
- import json
4
- import os
5
- import sys
6
- from pathlib import Path
7
- from unittest.mock import MagicMock, patch
8
-
9
- import pytest
10
- from click.exceptions import Exit as ClickExit
11
-
12
- from lyceum.external.compute.execution.python import (
13
- build_payload,
14
- inject_script_args,
15
- load_workspace_config,
16
- read_code_from_source,
17
- resolve_import_files,
18
- resolve_requirements,
19
- submit_execution,
20
- validate_machine_type,
21
- )
22
-
23
-
24
- class TestReadCodeFromSource:
25
- """Tests for read_code_from_source function."""
26
-
27
- def test_reads_from_file(self, tmp_path):
28
- test_file = tmp_path / "test.py"
29
- test_file.write_text("print('hello')")
30
-
31
- code, file_path, file_name = read_code_from_source(str(test_file))
32
-
33
- assert code == "print('hello')"
34
- assert file_path == test_file
35
- assert file_name == "test.py"
36
-
37
- def test_returns_inline_code(self):
38
- code, file_path, file_name = read_code_from_source("print('inline')")
39
-
40
- assert code == "print('inline')"
41
- assert file_path is None
42
- assert file_name is None
43
-
44
- def test_reads_multiline_file(self, tmp_path):
45
- test_file = tmp_path / "multi.py"
46
- test_file.write_text("line1\nline2\nline3")
47
-
48
- code, file_path, file_name = read_code_from_source(str(test_file))
49
-
50
- assert code == "line1\nline2\nline3"
51
- assert file_name == "multi.py"
52
-
53
- def test_with_status_line(self, tmp_path):
54
- test_file = tmp_path / "test.py"
55
- test_file.write_text("print('hello')")
56
-
57
- mock_status = MagicMock()
58
- code, file_path, file_name = read_code_from_source(str(test_file), status=mock_status)
59
-
60
- mock_status.update.assert_called_once()
61
- assert "test.py" in mock_status.update.call_args[0][0]
62
-
63
-
64
- class TestInjectScriptArgs:
65
- """Tests for inject_script_args function."""
66
-
67
- def test_injects_args(self):
68
- code = "print(sys.argv)"
69
- result = inject_script_args(code, ["--flag", "value"], "script.py")
70
-
71
- assert "sys.argv" in result
72
- assert "script.py" in result
73
- assert "--flag" in result
74
- assert "value" in result
75
-
76
- def test_no_injection_without_args(self):
77
- code = "print('hello')"
78
- result = inject_script_args(code, [], "script.py")
79
-
80
- assert result == code
81
-
82
- def test_uses_default_script_name(self):
83
- code = "print(sys.argv)"
84
- result = inject_script_args(code, ["arg1"], None)
85
-
86
- assert "script.py" in result
87
-
88
- def test_preserves_original_code(self):
89
- code = "original = 'code'\nprint(original)"
90
- result = inject_script_args(code, ["--test"], "test.py")
91
-
92
- assert "original = 'code'" in result
93
- assert "print(original)" in result
94
-
95
-
96
- class TestResolveRequirements:
97
- """Tests for resolve_requirements function."""
98
-
99
- def test_from_file(self, tmp_path):
100
- req_file = tmp_path / "requirements.txt"
101
- req_file.write_text("numpy==1.24.0\npandas>=2.0.0")
102
-
103
- result = resolve_requirements(str(req_file), None)
104
-
105
- assert "numpy==1.24.0" in result
106
- assert "pandas>=2.0.0" in result
107
-
108
- def test_from_string(self):
109
- result = resolve_requirements("numpy>=1.0", None)
110
-
111
- assert result == "numpy>=1.0"
112
-
113
- def test_from_workspace_config(self):
114
- workspace_config = {"dependencies": {"merged": ["numpy==1.24.0", "pandas>=2.0.0"]}}
115
-
116
- result = resolve_requirements(None, workspace_config)
117
-
118
- assert "numpy==1.24.0" in result
119
- assert "pandas>=2.0.0" in result
120
-
121
- def test_returns_none_when_empty(self):
122
- result = resolve_requirements(None, None)
123
-
124
- assert result is None
125
-
126
- def test_returns_none_for_empty_config(self):
127
- workspace_config = {"dependencies": {"merged": []}}
128
-
129
- result = resolve_requirements(None, workspace_config)
130
-
131
- assert result is None
132
-
133
- def test_returns_none_for_missing_deps(self):
134
- workspace_config = {"other_key": "value"}
135
-
136
- result = resolve_requirements(None, workspace_config)
137
-
138
- assert result is None
139
-
140
- def test_explicit_takes_precedence(self, tmp_path):
141
- req_file = tmp_path / "requirements.txt"
142
- req_file.write_text("explicit==1.0.0")
143
-
144
- workspace_config = {"dependencies": {"merged": ["config==2.0.0"]}}
145
-
146
- result = resolve_requirements(str(req_file), workspace_config)
147
-
148
- assert "explicit==1.0.0" in result
149
- assert "config==2.0.0" not in result
150
-
151
-
152
- class TestResolveImportFiles:
153
- """Tests for resolve_import_files function."""
154
-
155
- def test_resolves_local_imports(self, sample_workspace):
156
- sys.path.insert(0, str(sample_workspace))
157
- try:
158
- main_file = sample_workspace / "main.py"
159
- result = resolve_import_files(main_file, None)
160
-
161
- assert result is not None
162
- parsed = json.loads(result)
163
-
164
- # Should contain package files
165
- keys = list(parsed.keys())
166
- assert len(keys) > 0
167
- finally:
168
- sys.path.remove(str(sample_workspace))
169
-
170
- def test_returns_none_for_no_file(self):
171
- result = resolve_import_files(None, None)
172
-
173
- assert result is None
174
-
175
- def test_returns_none_for_nonexistent_file(self, tmp_path):
176
- nonexistent = tmp_path / "nonexistent.py"
177
-
178
- result = resolve_import_files(nonexistent, None)
179
-
180
- assert result is None
181
-
182
- def test_returns_none_for_no_imports(self, tmp_path):
183
- simple_file = tmp_path / "simple.py"
184
- simple_file.write_text("print('hello')")
185
-
186
- result = resolve_import_files(simple_file, None)
187
-
188
- # No local imports, so returns None
189
- assert result is None
190
-
191
- def test_uses_workspace_config_root(self, sample_workspace, sample_workspace_config):
192
- sample_workspace_config()
193
-
194
- sys.path.insert(0, str(sample_workspace))
195
- try:
196
- main_file = sample_workspace / "main.py"
197
-
198
- workspace_config = {
199
- "_config_dir": sample_workspace,
200
- "dependencies": {"merged": []},
201
- }
202
-
203
- result = resolve_import_files(main_file, workspace_config)
204
-
205
- assert result is not None
206
- finally:
207
- sys.path.remove(str(sample_workspace))
208
-
209
-
210
- class TestBuildPayload:
211
- """Tests for build_payload function."""
212
-
213
- def test_minimal_payload(self):
214
- payload = build_payload(code="print('hello')", machine_type="cpu")
215
-
216
- assert payload["code"] == "print('hello')"
217
- assert payload["execution_type"] == "cpu"
218
- assert "timeout" in payload
219
- assert payload["nbcode"] == 0
220
-
221
- def test_full_payload(self):
222
- payload = build_payload(
223
- code="print('hello')",
224
- machine_type="a100",
225
- file_name="test.py",
226
- requirements_content="numpy>=1.0",
227
- imports=["os", "sys"],
228
- import_files='{"test.py": "code"}',
229
- )
230
-
231
- assert payload["execution_type"] == "a100"
232
- assert payload["file_name"] == "test.py"
233
- assert payload["requirements_content"] == "numpy>=1.0"
234
- assert payload["prior_imports"] == ["os", "sys"]
235
- assert payload["import_files"] == '{"test.py": "code"}'
236
-
237
- def test_omits_none_values(self):
238
- payload = build_payload(
239
- code="print('hello')",
240
- machine_type="cpu",
241
- file_name=None,
242
- requirements_content=None,
243
- )
244
-
245
- assert "file_name" not in payload
246
- assert "requirements_content" not in payload
247
-
248
-
249
- class TestSubmitExecution:
250
- """Tests for submit_execution function."""
251
-
252
- @patch("lyceum.external.compute.execution.python.httpx.post")
253
- @patch("lyceum.external.compute.execution.python.config")
254
- def test_success(self, mock_config, mock_post, setup_httpx_post, mock_execution_id):
255
- mock_config.base_url = "https://api.lyceum.dev"
256
- mock_config.api_key = "test-key"
257
- mock_post.return_value = setup_httpx_post(execution_id=mock_execution_id)
258
-
259
- exec_id, stream_url = submit_execution({"code": "print('hi')"})
260
-
261
- assert exec_id == mock_execution_id
262
- assert stream_url is not None
263
-
264
- @patch("lyceum.external.compute.execution.python.httpx.post")
265
- @patch("lyceum.external.compute.execution.python.config")
266
- def test_auth_error(self, mock_config, mock_post, setup_httpx_response):
267
- mock_config.base_url = "https://api.lyceum.dev"
268
- mock_config.api_key = "invalid-key"
269
- mock_post.return_value = setup_httpx_response(status_code=401, text="Unauthorized")
270
-
271
- with pytest.raises(ClickExit):
272
- submit_execution({"code": "print('hi')"})
273
-
274
- @patch("lyceum.external.compute.execution.python.httpx.post")
275
- @patch("lyceum.external.compute.execution.python.config")
276
- def test_server_error(self, mock_config, mock_post, setup_httpx_response):
277
- mock_config.base_url = "https://api.lyceum.dev"
278
- mock_config.api_key = "test-key"
279
- mock_post.return_value = setup_httpx_response(status_code=500, text="Server Error")
280
-
281
- with pytest.raises(ClickExit):
282
- submit_execution({"code": "print('hi')"})
283
-
284
- @patch("lyceum.external.compute.execution.python.httpx.post")
285
- @patch("lyceum.external.compute.execution.python.config")
286
- def test_with_status_line(self, mock_config, mock_post, setup_httpx_post, mock_execution_id):
287
- mock_config.base_url = "https://api.lyceum.dev"
288
- mock_config.api_key = "test-key"
289
- mock_post.return_value = setup_httpx_post(execution_id=mock_execution_id)
290
-
291
- mock_status = MagicMock()
292
- exec_id, stream_url = submit_execution({"code": "print('hi')"}, status=mock_status)
293
-
294
- mock_status.update.assert_called_once_with("Submitting execution...")
295
-
296
-
297
- class TestValidateMachineType:
298
- """Tests for validate_machine_type function."""
299
-
300
- @patch("lyceum.external.compute.execution.python.get_available_machines")
301
- def test_valid_machine(self, mock_get_machines):
302
- mock_get_machines.return_value = ["cpu", "a100", "h100"]
303
-
304
- assert validate_machine_type("cpu") is True
305
- assert validate_machine_type("a100") is True
306
-
307
- @patch("lyceum.external.compute.execution.python.get_available_machines")
308
- def test_invalid_machine(self, mock_get_machines):
309
- mock_get_machines.return_value = ["cpu"]
310
-
311
- assert validate_machine_type("a100") is False
312
-
313
- @patch("lyceum.external.compute.execution.python.get_available_machines")
314
- def test_empty_available_returns_true(self, mock_get_machines):
315
- # When we can't fetch available machines, assume valid
316
- mock_get_machines.return_value = []
317
-
318
- assert validate_machine_type("any_machine") is True
319
-
320
-
321
- class TestLoadWorkspaceConfig:
322
- """Tests for load_workspace_config function."""
323
-
324
- def test_finds_config_in_workspace(self, sample_workspace, sample_workspace_config):
325
- sample_workspace_config()
326
-
327
- main_file = sample_workspace / "main.py"
328
-
329
- # Change cwd to tmp to avoid picking up real config
330
- original_cwd = os.getcwd()
331
- try:
332
- os.chdir(sample_workspace)
333
- config = load_workspace_config(main_file)
334
- finally:
335
- os.chdir(original_cwd)
336
-
337
- assert config is not None
338
- assert "dependencies" in config
339
-
340
- def test_returns_none_without_config(self, tmp_path):
341
- # Create a completely isolated workspace with no .lyceum config
342
- isolated = tmp_path / "isolated_workspace"
343
- isolated.mkdir()
344
- main_file = isolated / "main.py"
345
- main_file.write_text("print('hello')")
346
-
347
- # Change cwd to the isolated dir to avoid picking up real config
348
- original_cwd = os.getcwd()
349
- try:
350
- os.chdir(isolated)
351
- config = load_workspace_config(main_file)
352
- finally:
353
- os.chdir(original_cwd)
354
-
355
- assert config is None
356
-
357
- def test_searches_parent_directories(self, sample_workspace, sample_workspace_config):
358
- sample_workspace_config()
359
-
360
- # Create a nested file
361
- nested_dir = sample_workspace / "nested" / "deep"
362
- nested_dir.mkdir(parents=True)
363
- nested_file = nested_dir / "script.py"
364
- nested_file.write_text("print('hi')")
365
-
366
- original_cwd = os.getcwd()
367
- try:
368
- os.chdir(sample_workspace)
369
- config = load_workspace_config(nested_file)
370
- finally:
371
- os.chdir(original_cwd)
372
-
373
- assert config is not None
374
- assert "_config_dir" in config
375
-
376
- def test_returns_none_for_nonexistent_file(self, tmp_path):
377
- # Create an isolated directory
378
- isolated = tmp_path / "isolated"
379
- isolated.mkdir()
380
- nonexistent = isolated / "nonexistent.py"
381
-
382
- original_cwd = os.getcwd()
383
- try:
384
- os.chdir(isolated)
385
- config = load_workspace_config(nonexistent)
386
- finally:
387
- os.chdir(original_cwd)
388
-
389
- # Should not crash, returns None
390
- assert config is None
391
-
392
- def test_adds_config_dir_to_result(self, sample_workspace, sample_workspace_config):
393
- sample_workspace_config()
394
-
395
- main_file = sample_workspace / "main.py"
396
-
397
- original_cwd = os.getcwd()
398
- try:
399
- os.chdir(sample_workspace)
400
- config = load_workspace_config(main_file)
401
- finally:
402
- os.chdir(original_cwd)
403
-
404
- assert config is not None
405
- assert "_config_dir" in config
406
- assert config["_config_dir"] == sample_workspace