Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 40 additions & 1 deletion arm_cli/config.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import json
import os
import shutil
from pathlib import Path
from typing import Dict, List, Optional
Expand All @@ -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."""
Expand Down Expand Up @@ -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:
Expand Down
10 changes: 8 additions & 2 deletions arm_cli/projects/activate.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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)
Expand Down
13 changes: 11 additions & 2 deletions arm_cli/projects/info.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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:
Expand Down
5 changes: 4 additions & 1 deletion arm_cli/projects/init.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion resources/default_project_config.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@
"description": "Default ARM project configuration",
"docker_compose_file": "docker-compose.yml",
"data_directory": "/DATA",
"project_directory": "~"
"project_directory": "."
}
33 changes: 33 additions & 0 deletions tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
from pathlib import Path
from unittest.mock import patch

import pytest

from arm_cli.config import (
Config,
ProjectConfig,
Expand Down Expand Up @@ -206,3 +208,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")