Skip to content

Commit f3ce99b

Browse files
committed
refactor: address PR review comments for transitive deps fix
- Move get_lockfile_installed_paths logic into LockFile.get_installed_paths() method (foutoucour review) - Use .as_posix() for Windows-safe path normalization (copilot-reviewer) - Narrow except Exception to specific error types (copilot-reviewer) - Move inline imports to top-level in discovery.py and cli.py (foutoucour) - Remove unused imports in test file (tempfile, pytest)
1 parent 58cda2e commit f3ce99b

4 files changed

Lines changed: 50 additions & 52 deletions

File tree

src/apm_cli/cli.py

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
try:
4242
from apm_cli.deps.apm_resolver import APMDependencyResolver
4343
from apm_cli.deps.github_downloader import GitHubPackageDownloader
44+
from apm_cli.deps.lockfile import get_lockfile_installed_paths
4445
from apm_cli.models.apm_package import APMPackage, DependencyReference
4546
from apm_cli.integration import PromptIntegrator, AgentIntegrator
4647

@@ -140,8 +141,6 @@ def _check_orphaned_packages():
140141
List[str]: List of orphaned package names in org/repo or org/project/repo format
141142
"""
142143
try:
143-
from pathlib import Path
144-
145144
# Check if apm.yml exists
146145
if not Path("apm.yml").exists():
147146
return []
@@ -170,12 +169,8 @@ def _check_orphaned_packages():
170169
expected_installed.add(str(install_path))
171170

172171
# Also include transitive dependencies from apm.lock
173-
try:
174-
from apm_cli.deps.lockfile import get_lockfile_installed_paths
175-
lockfile_paths = get_lockfile_installed_paths(Path.cwd())
176-
expected_installed.update(lockfile_paths)
177-
except ImportError:
178-
pass # Lockfile module not available
172+
lockfile_paths = get_lockfile_installed_paths(Path.cwd())
173+
expected_installed.update(lockfile_paths)
179174
except Exception:
180175
return [] # If can't parse apm.yml, assume no orphans
181176

@@ -803,12 +798,8 @@ def prune(ctx, dry_run):
803798
expected_installed.add(f"{repo_parts[0]}/{repo_parts[1]}")
804799

805800
# Also include transitive dependencies from apm.lock
806-
try:
807-
from apm_cli.deps.lockfile import get_lockfile_installed_paths
808-
lockfile_paths = get_lockfile_installed_paths(Path.cwd())
809-
expected_installed.update(lockfile_paths)
810-
except ImportError:
811-
pass # Lockfile module not available
801+
lockfile_paths = get_lockfile_installed_paths(Path.cwd())
802+
expected_installed.update(lockfile_paths)
812803
except Exception as e:
813804
_rich_error(f"Failed to parse apm.yml: {e}")
814805
sys.exit(1)

src/apm_cli/deps/lockfile.py

Lines changed: 39 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,40 @@ def from_installed_packages(
200200

201201
return lock
202202

203+
def get_installed_paths(self, apm_modules_dir: Path) -> List[str]:
204+
"""Get relative installed paths for all dependencies in this lockfile.
205+
206+
Computes expected installed paths for all dependencies, including
207+
transitive ones. Used by:
208+
- Primitive discovery to find all dependency primitives
209+
- Orphan detection to avoid false positives for transitive deps
210+
211+
Args:
212+
apm_modules_dir: Path to the apm_modules directory.
213+
214+
Returns:
215+
List[str]: POSIX-style relative installed paths (e.g., ['owner/repo']),
216+
ordered by depth then repo_url (no duplicates).
217+
"""
218+
seen: set = set()
219+
paths: List[str] = []
220+
for dep in self.get_all_dependencies():
221+
dep_ref = DependencyReference(
222+
repo_url=dep.repo_url,
223+
host=dep.host,
224+
virtual_path=dep.virtual_path,
225+
is_virtual=dep.is_virtual,
226+
)
227+
install_path = dep_ref.get_install_path(apm_modules_dir)
228+
try:
229+
rel_path = install_path.relative_to(apm_modules_dir).as_posix()
230+
except ValueError:
231+
rel_path = Path(install_path).as_posix()
232+
if rel_path not in seen:
233+
seen.add(rel_path)
234+
paths.append(rel_path)
235+
return paths
236+
203237
def save(self, path: Path) -> None:
204238
"""Save lock file to disk (alias for write)."""
205239
self.write(path)
@@ -213,12 +247,9 @@ def get_lockfile_path(project_root: Path) -> Path:
213247
def get_lockfile_installed_paths(project_root: Path) -> List[str]:
214248
"""Get installed paths for all dependencies recorded in apm.lock.
215249
216-
Reads the lockfile and computes expected installed paths for all
217-
dependencies, including transitive ones. Used by:
218-
- Primitive discovery to find all dependency primitives
219-
- Orphan detection to avoid false positives for transitive deps
220-
221-
Returns an empty list if the lockfile is missing, corrupt, or unreadable.
250+
Convenience wrapper that loads the lockfile and delegates to
251+
LockFile.get_installed_paths(). Returns an empty list if the
252+
lockfile is missing, corrupt, or unreadable.
222253
223254
Args:
224255
project_root: Path to project root containing apm.lock.
@@ -232,25 +263,6 @@ def get_lockfile_installed_paths(project_root: Path) -> List[str]:
232263
lockfile = LockFile.read(lockfile_path)
233264
if not lockfile:
234265
return []
235-
236-
apm_modules_dir = project_root / "apm_modules"
237-
paths: List[str] = []
238-
seen: set = set()
239-
for dep in lockfile.get_all_dependencies():
240-
dep_ref = DependencyReference(
241-
repo_url=dep.repo_url,
242-
host=dep.host,
243-
virtual_path=dep.virtual_path,
244-
is_virtual=dep.is_virtual,
245-
)
246-
install_path = dep_ref.get_install_path(apm_modules_dir)
247-
try:
248-
rel_path = str(install_path.relative_to(apm_modules_dir))
249-
except ValueError:
250-
rel_path = str(install_path)
251-
if rel_path not in seen:
252-
seen.add(rel_path)
253-
paths.append(rel_path)
254-
return paths
255-
except Exception:
266+
return lockfile.get_installed_paths(project_root / "apm_modules")
267+
except (FileNotFoundError, yaml.YAMLError, ValueError, KeyError):
256268
return []

src/apm_cli/primitives/discovery.py

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from .models import PrimitiveCollection
99
from .parser import parse_primitive_file, parse_skill_file
1010
from ..models.apm_package import APMPackage
11+
from ..deps.lockfile import get_lockfile_installed_paths
1112

1213

1314
# Common primitive patterns for local discovery (with recursive search)
@@ -251,15 +252,11 @@ def get_dependency_declaration_order(base_dir: str) -> List[str]:
251252

252253
# Include transitive dependencies from apm.lock
253254
# Direct deps from apm.yml have priority; transitive deps are appended
254-
try:
255-
from ..deps.lockfile import get_lockfile_installed_paths
256-
lockfile_paths = get_lockfile_installed_paths(Path(base_dir))
257-
direct_set = set(dependency_names)
258-
for path in lockfile_paths:
259-
if path not in direct_set:
260-
dependency_names.append(path)
261-
except ImportError:
262-
pass # Lockfile module not available
255+
lockfile_paths = get_lockfile_installed_paths(Path(base_dir))
256+
direct_set = set(dependency_names)
257+
for path in lockfile_paths:
258+
if path not in direct_set:
259+
dependency_names.append(path)
263260

264261
return dependency_names
265262

tests/unit/test_transitive_deps.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,8 @@
66
- get_dependency_declaration_order() includes transitive deps from lockfile
77
"""
88

9-
import tempfile
109
from pathlib import Path
1110

12-
import pytest
1311
import yaml
1412

1513
from apm_cli.deps.lockfile import (

0 commit comments

Comments
 (0)