From 93d1aabadbacc1caf6212533b5e9388e3392fec3 Mon Sep 17 00:00:00 2001 From: Matthew Powelson Date: Fri, 15 Aug 2025 09:57:10 -0400 Subject: [PATCH 1/3] Allow relative paths in config --- arm_cli/config.py | 41 +++++++++++++++++++++++++++++++++++- arm_cli/projects/activate.py | 10 +++++++-- arm_cli/projects/info.py | 13 ++++++++++-- arm_cli/projects/init.py | 5 ++++- tests/test_config.py | 31 +++++++++++++++++++++++++++ 5 files changed, 94 insertions(+), 6 deletions(-) diff --git a/arm_cli/config.py b/arm_cli/config.py index f51c39a..8d08adc 100644 --- a/arm_cli/config.py +++ b/arm_cli/config.py @@ -1,4 +1,5 @@ import json +import os import shutil from pathlib import Path from typing import Dict, List, Optional @@ -16,6 +17,38 @@ class ProjectConfig(BaseModel): docker_compose_file: Optional[str] = None data_directory: Optional[str] = None + def get_resolved_project_directory( + self, config_file_path: Optional[Path] = None + ) -> Optional[str]: + """Get the project directory resolved to an absolute path. + + Args: + config_file_path: Path to the config file. If None, uses current working directory. + + Returns: + Absolute path to the project directory, or None if project_directory is None. + """ + if self.project_directory is None: + return None + + # Handle tilde expansion + expanded_path = os.path.expanduser(self.project_directory) + project_path = Path(expanded_path) + + # If it's already absolute, return as is + if project_path.is_absolute(): + return str(project_path) + + # If it's relative, resolve it relative to the config file location + if config_file_path is None: + raise ValueError( + "config_file_path must be provided to resolve relative project_directory" + ) + base_path = config_file_path.parent + + resolved_path = (base_path / project_path).resolve() + return str(resolved_path) + class AvailableProject(BaseModel): """Schema for available project entry.""" @@ -99,7 +132,13 @@ def load_project_config(project_path: str) -> ProjectConfig: with open(config_path, "r") as f: data = json.load(f) - return ProjectConfig(**data) + + project_config = ProjectConfig(**data) + + # Store the config file path for resolving relative project_directory + project_config._config_file_path = config_path + + return project_config def add_project_to_list(config: Config, project_path: str, project_name: str) -> None: diff --git a/arm_cli/projects/activate.py b/arm_cli/projects/activate.py index 242ca91..0787aa9 100644 --- a/arm_cli/projects/activate.py +++ b/arm_cli/projects/activate.py @@ -25,7 +25,10 @@ def _activate(ctx, project: Optional[str] = None): project_config = get_active_project_config(config) if project_config: print(f"Activated default project: {project_config.name}") - print(f"Project directory: {project_config.project_directory}") + resolved_dir = project_config.get_resolved_project_directory( + getattr(project_config, "_config_file_path", None) + ) + print(f"Project directory: {resolved_dir}") else: print("Failed to set up default project.") print_no_projects_message() @@ -65,7 +68,10 @@ def _activate(ctx, project: Optional[str] = None): if project_config: print(f"Activated project: {project_config.name}") - print(f"Project directory: {project_config.project_directory}") + resolved_dir = project_config.get_resolved_project_directory( + getattr(project_config, "_config_file_path", None) + ) + print(f"Project directory: {resolved_dir}") else: print(f"Project '{project}' not found in available projects") print_available_projects(config) diff --git a/arm_cli/projects/info.py b/arm_cli/projects/info.py index 96f1877..b64d43a 100644 --- a/arm_cli/projects/info.py +++ b/arm_cli/projects/info.py @@ -26,7 +26,13 @@ def _info(ctx, field): ] if field in available_attrs: - value = getattr(project_config, field) + if field == "project_directory": + # Use resolved project directory for the cdp alias + value = project_config.get_resolved_project_directory( + getattr(project_config, "_config_file_path", None) + ) + else: + value = getattr(project_config, field) if value: print(value) else: @@ -44,7 +50,10 @@ def _info(ctx, field): if project_config.description: print(f"Description: {project_config.description}") if project_config.project_directory: - print(f"Project Directory: {project_config.project_directory}") + resolved_dir = project_config.get_resolved_project_directory( + getattr(project_config, "_config_file_path", None) + ) + print(f"Project Directory: {resolved_dir}") if project_config.docker_compose_file: print(f"Docker Compose File: {project_config.docker_compose_file}") if project_config.data_directory: diff --git a/arm_cli/projects/init.py b/arm_cli/projects/init.py index b00f3bf..9ddb1b9 100644 --- a/arm_cli/projects/init.py +++ b/arm_cli/projects/init.py @@ -136,7 +136,10 @@ def _init(ctx, project_path: str, name: Optional[str] = None): save_config(config) print(f"Project '{project_config.name}' initialized and set as active") - print(f"Project directory: {project_config.project_directory}") + resolved_dir = project_config.get_resolved_project_directory( + getattr(project_config, "_config_file_path", None) + ) + print(f"Project directory: {resolved_dir}") # Create the command object diff --git a/tests/test_config.py b/tests/test_config.py index a204c6a..4f7eb9b 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -206,3 +206,34 @@ class StrictProjectConfig(ProjectConfig): # Just verify it loaded successfully (the JSON is the source of truth for values) assert isinstance(project_config, ProjectConfig) assert project_config.name is not None + + def test_get_resolved_project_directory_absolute(self): + """Test that absolute project_directory paths are returned as-is.""" + config = ProjectConfig(name="test-project", project_directory="/absolute/path") + resolved = config.get_resolved_project_directory(Path("/dummy/config.json")) + assert resolved == "/absolute/path" + + def test_get_resolved_project_directory_relative(self): + """Test that relative project_directory paths are resolved correctly.""" + config = ProjectConfig(name="test-project", project_directory="relative/path") + config_file_path = Path("/config/location/config.json") + resolved = config.get_resolved_project_directory(config_file_path) + assert resolved == str(Path("/config/location/relative/path").resolve()) + + def test_get_resolved_project_directory_none(self): + """Test that None project_directory returns None.""" + config = ProjectConfig(name="test-project") + resolved = config.get_resolved_project_directory(Path("/dummy/config.json")) + assert resolved is None + + def test_get_resolved_project_directory_no_config_path(self): + """Test that relative paths raise an error when no config path provided.""" + config = ProjectConfig(name="test-project", project_directory="relative/path") + with pytest.raises(ValueError, match="config_file_path must be provided"): + config.get_resolved_project_directory() + + def test_get_resolved_project_directory_tilde(self): + """Test that tilde (~) is expanded to home directory.""" + config = ProjectConfig(name="test-project", project_directory="~/projects") + resolved = config.get_resolved_project_directory(Path("/dummy/config.json")) + assert resolved == str(Path.home() / "projects") From 626494becfcb01a137e678b5d6cd43cf41fc169e Mon Sep 17 00:00:00 2001 From: Matthew Powelson Date: Fri, 15 Aug 2025 10:00:41 -0400 Subject: [PATCH 2/3] Change default project directory to . This will make less sense if you use the default all the time but more sense if you initialize your project to defaults (which should be encouraged) --- resources/default_project_config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/default_project_config.json b/resources/default_project_config.json index ce1cc4c..64674f8 100644 --- a/resources/default_project_config.json +++ b/resources/default_project_config.json @@ -3,5 +3,5 @@ "description": "Default ARM project configuration", "docker_compose_file": "docker-compose.yml", "data_directory": "/DATA", - "project_directory": "~" + "project_directory": "." } From 36a42b44b89afd9b35cc8f04710e5c7dd63c3865 Mon Sep 17 00:00:00 2001 From: Matthew Powelson Date: Fri, 15 Aug 2025 10:13:40 -0400 Subject: [PATCH 3/3] Fix the tests --- tests/test_config.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_config.py b/tests/test_config.py index 4f7eb9b..033b00d 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -3,6 +3,8 @@ from pathlib import Path from unittest.mock import patch +import pytest + from arm_cli.config import ( Config, ProjectConfig,