Skip to content

Commit 4698506

Browse files
authored
feat(cli): enable loading config from working directory (#527)
Correctly allow loading configurations from the current directory.
1 parent cd56979 commit 4698506

File tree

3 files changed

+163
-1
lines changed

3 files changed

+163
-1
lines changed

advanced_alchemy/cli.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,20 @@ def get_alchemy_group() -> "Group":
4242
@click.pass_context
4343
def alchemy_group(ctx: "click.Context", config: str) -> None:
4444
"""Advanced Alchemy CLI commands."""
45+
from pathlib import Path
46+
4547
from rich import get_console
4648

4749
from advanced_alchemy.utils import module_loader
4850

4951
console = get_console()
5052
ctx.ensure_object(dict)
53+
54+
# Add current working directory to sys.path to allow loading local config modules
55+
cwd = str(Path.cwd())
56+
if cwd not in sys.path:
57+
sys.path.insert(0, cwd)
58+
5159
try:
5260
config_instance = module_loader.import_string(config)
5361
if isinstance(config_instance, Sequence):
@@ -57,6 +65,10 @@ def alchemy_group(ctx: "click.Context", config: str) -> None:
5765
except ImportError as e:
5866
console.print(f"[red]Error loading config: {e}[/]")
5967
ctx.exit(1)
68+
finally:
69+
# Clean up: remove the cwd from sys.path if we added it
70+
if cwd in sys.path and sys.path[0] == cwd:
71+
sys.path.remove(cwd)
6072

6173
return alchemy_group
6274

tests/unit/fixtures.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,24 @@
11
from __future__ import annotations
22

3-
from advanced_alchemy.config import SQLAlchemySyncConfig
3+
from advanced_alchemy.config import SQLAlchemyAsyncConfig, SQLAlchemySyncConfig
44

5+
# Keep the original sync configs for backward compatibility
56
configs = [SQLAlchemySyncConfig(connection_string="sqlite:///:memory:")]
7+
8+
# Add async configs for new external config loading tests
9+
async_configs = [
10+
SQLAlchemyAsyncConfig(
11+
connection_string="sqlite+aiosqlite:///:memory:",
12+
bind_key="default",
13+
),
14+
SQLAlchemyAsyncConfig(
15+
connection_string="sqlite+aiosqlite:///:memory:",
16+
bind_key="secondary",
17+
),
18+
]
19+
20+
# Single config for basic tests
21+
config = SQLAlchemyAsyncConfig(
22+
connection_string="sqlite+aiosqlite:///:memory:",
23+
bind_key="default",
24+
)

tests/unit/test_cli.py

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from __future__ import annotations
22

3+
import os
4+
import tempfile
35
from collections.abc import Generator
46
from pathlib import Path
57
from typing import TYPE_CHECKING
@@ -170,3 +172,132 @@ def test_cli_group_creation() -> None:
170172
assert "drop-all" in cli_group.commands
171173
assert "dump-data" in cli_group.commands
172174
assert "stamp" in cli_group.commands
175+
176+
177+
def test_external_config_loading(cli_runner: CliRunner) -> None:
178+
"""Test loading config from external module in current working directory."""
179+
with tempfile.TemporaryDirectory() as temp_dir:
180+
temp_path = Path(temp_dir)
181+
182+
# Create an external config file in the temp directory
183+
config_file = temp_path / "external_config.py"
184+
config_file.write_text("""
185+
from advanced_alchemy.config import SQLAlchemyAsyncConfig
186+
187+
config = SQLAlchemyAsyncConfig(
188+
connection_string="sqlite+aiosqlite:///:memory:",
189+
bind_key="external",
190+
)
191+
""")
192+
193+
# Change to the temp directory
194+
original_cwd = os.getcwd()
195+
try:
196+
os.chdir(temp_dir)
197+
198+
# Test that the external config can be loaded
199+
cli_group = add_migration_commands()
200+
201+
# Use a minimal command that doesn't require database setup
202+
# but still needs the config to be loaded successfully
203+
result = cli_runner.invoke(cli_group, ["--config", "external_config.config", "--help"])
204+
205+
# Should succeed without import errors
206+
assert result.exit_code == 0
207+
assert "Error loading config" not in result.output
208+
assert "No module named" not in result.output
209+
210+
finally:
211+
os.chdir(original_cwd)
212+
213+
214+
def test_external_config_loading_multiple_configs(cli_runner: CliRunner) -> None:
215+
"""Test loading multiple configs from external module."""
216+
with tempfile.TemporaryDirectory() as temp_dir:
217+
temp_path = Path(temp_dir)
218+
219+
# Create an external config file with multiple configs
220+
config_file = temp_path / "multi_config.py"
221+
config_file.write_text("""
222+
from advanced_alchemy.config import SQLAlchemyAsyncConfig
223+
224+
configs = [
225+
SQLAlchemyAsyncConfig(
226+
connection_string="sqlite+aiosqlite:///:memory:",
227+
bind_key="primary",
228+
),
229+
SQLAlchemyAsyncConfig(
230+
connection_string="sqlite+aiosqlite:///:memory:",
231+
bind_key="secondary",
232+
),
233+
]
234+
""")
235+
236+
# Change to the temp directory
237+
original_cwd = os.getcwd()
238+
try:
239+
os.chdir(temp_dir)
240+
241+
cli_group = add_migration_commands()
242+
result = cli_runner.invoke(cli_group, ["--config", "multi_config.configs", "--help"])
243+
244+
# Should succeed without import errors
245+
assert result.exit_code == 0
246+
assert "Error loading config" not in result.output
247+
assert "No module named" not in result.output
248+
249+
finally:
250+
os.chdir(original_cwd)
251+
252+
253+
def test_external_config_loading_nonexistent_module(cli_runner: CliRunner) -> None:
254+
"""Test appropriate error when external module doesn't exist."""
255+
with tempfile.TemporaryDirectory() as temp_dir:
256+
# Change to empty temp directory
257+
original_cwd = os.getcwd()
258+
try:
259+
os.chdir(temp_dir)
260+
261+
cli_group = add_migration_commands()
262+
# Use actual command to trigger config loading, not --help
263+
result = cli_runner.invoke(cli_group, ["--config", "nonexistent_module.config", "show-current-revision"])
264+
265+
# Should fail with appropriate error
266+
assert result.exit_code == 1
267+
assert "Error loading config" in result.output
268+
assert "No module named 'nonexistent_module'" in result.output
269+
270+
finally:
271+
os.chdir(original_cwd)
272+
273+
274+
def test_external_config_loading_nonexistent_attribute(cli_runner: CliRunner) -> None:
275+
"""Test appropriate error when module exists but attribute doesn't."""
276+
with tempfile.TemporaryDirectory() as temp_dir:
277+
temp_path = Path(temp_dir)
278+
279+
# Create an external config file without the expected attribute
280+
config_file = temp_path / "bad_config.py"
281+
config_file.write_text("""
282+
# This module exists but doesn't have a 'missing_attr' attribute
283+
from advanced_alchemy.config import SQLAlchemyAsyncConfig
284+
285+
some_other_var = "not a config"
286+
""")
287+
288+
# Change to the temp directory
289+
original_cwd = os.getcwd()
290+
try:
291+
os.chdir(temp_dir)
292+
293+
cli_group = add_migration_commands()
294+
# Use actual command to trigger config loading, not --help
295+
result = cli_runner.invoke(cli_group, ["--config", "bad_config.missing_attr", "show-current-revision"])
296+
297+
# Should fail with appropriate error
298+
assert result.exit_code == 1
299+
assert "Error loading config" in result.output
300+
# The actual error message may vary, but it should indicate the attribute issue
301+
302+
finally:
303+
os.chdir(original_cwd)

0 commit comments

Comments
 (0)