fix(uninstall): scan devDependencies.apm so --dev installs can be removed (closes #1549)#1552
Conversation
…oved apm install --dev <pkg> writes the entry under devDependencies.apm in apm.yml, but apm uninstall <pkg> only read dependencies.apm. The result was an unconditional 'not found in apm.yml' warning and the dev entry leaking forever. Uninstall now scans both sections and writes back to whichever section the package lived in. Closes #1549 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR fixes apm uninstall so packages installed with apm install --dev can be discovered and removed from devDependencies.apm, addressing issue #1549.
Changes:
- Extends uninstall dependency scanning to include both production and development APM dependency sections.
- Updates manifest write-back logic to remove matched packages from the appropriate section.
- Adds regression tests for uninstalling, dry-running, and preserving unrelated dev dependencies.
Show a summary per file
| File | Description |
|---|---|
src/apm_cli/commands/uninstall/cli.py |
Adds devDependencies.apm handling during uninstall. |
tests/unit/test_uninstall_dev_dependencies.py |
Adds regression coverage for uninstalling dev dependencies. |
CHANGELOG.md |
Documents the uninstall fix under Unreleased. |
Copilot's findings
- Files reviewed: 3/3 changed files
- Comments generated: 2
| if package in dev_deps: | ||
| dev_deps.remove(package) | ||
| elif package in prod_deps: | ||
| prod_deps.remove(package) |
|
|
||
| ### Fixed | ||
|
|
||
| - `apm uninstall <pkg>` now scans `devDependencies.apm` in addition to `dependencies.apm`, so packages added with `apm install --dev <pkg>` can actually be removed. Previously dev-only entries reported "not found in apm.yml" and leaked forever. (closes #1549, #1552) -- by @aetos382 |
APM Review Panel:
|
| Persona | B | R | N | Takeaway |
|---|---|---|---|---|
| Python Architect | 0 | 0 | 1 | Clean scoped fix; no architectural concern. The dual-section scan plus write-back-to-origin pattern is correct and minimal. |
| CLI Logging Expert | 0 | 1 | 1 | Progress message does not distinguish prod vs dev section; otherwise logging surface is clean and unchanged. |
| DevX UX Expert | 0 | 1 | 1 | Clean bugfix restoring symmetry between install --dev and uninstall; one minor UX signal gap in the progress message. |
| Supply Chain Security Expert | 0 | 0 | 0 | No supply-chain security surface touched; manifest-only YAML read/write with no download, auth, path traversal, or lockfile integrity changes. Ship. |
| OSS Growth Hacker | 0 | 1 | 2 | Solid bug-fix with good CHANGELOG entry; minor polish opportunities in release wording and story fit. |
| Doc Writer | 0 | 2 | 1 | CHANGELOG entry is accurate; reference/cli/uninstall.md Behavior step 1 is now stale and should mention devDependencies.apm. |
| Test Coverage Expert | 0 | 0 | 1 | Bug fix ships with well-targeted regression traps at integration-with-fixtures tier; one nit on the manifest-cleanup path. |
B = blocking-severity findings, R = recommended, N = nits.
Counts are signal strength, not gates. The maintainer ships.
Top 4 follow-ups
- [Doc Writer] Update docs/reference/cli/uninstall.md Behavior step 1 to cover devDependencies.apm -- The canonical user-facing description of removal behavior is now stale and will mislead readers post-merge.
- [CLI Logging Expert] Include section name (dependencies vs devDependencies) in verbose/progress removal message -- Both cli-logging-expert and devx-ux-expert flagged this independently; users debugging manifest state lose observability of where the removal happened.
- [OSS Growth Hacker] Reword CHANGELOG entry to lead with user-positive outcome -- Release notes are a conversion surface; leading with what the user gains is clearer than internal terminology.
- [Test Coverage Expert] Add assertion that uninstalling a prod dep from a manifest without devDependencies leaves no synthesized devDependencies key -- Covers the had_dev_section=False cleanup branch; low priority since it is manifest aesthetics, not correctness.
Architecture
classDiagram
direction LR
class UninstallCommand {
<<ClickCommand>>
+uninstall(ctx, packages, dry_run, verbose, global_)
}
class ManifestData {
<<YAML dict>>
+dependencies.apm
+devDependencies.apm
}
class UninstallValidation {
<<Function>>
+_validate_uninstall_packages(packages, current_deps, logger, lockfile, auth_resolver, dry_run)
}
class YamlIO {
<<Module>>
+load_yaml(path) dict
+dump_yaml(data, path)
}
UninstallCommand ..> YamlIO : reads/writes apm.yml
UninstallCommand ..> ManifestData : scans prod + dev sections
UninstallCommand ..> UninstallValidation : validates combined deps
flowchart TD
A[apm uninstall pkg] --> B[load apm.yml]
B --> C[ensure dependencies.apm]
C --> D[ensure or synthesize devDependencies.apm]
D --> E[current_deps = prod_deps + dev_deps]
E --> F[validate requested packages]
F --> G{dry run?}
G -->|yes| H[report removals only]
G -->|no| I{package in dev_deps?}
I -->|yes| J[remove from devDependencies.apm]
I -->|no| K[remove from dependencies.apm]
J --> L[drop synthesized empty dev wrapper]
K --> L
L --> M[dump apm.yml]
M --> N[continue uninstall cleanup]
Recommendation
Merge now. The fix is correct, minimal, and ships with regression tests. Track two post-merge items: (1) update the uninstall reference doc to mention devDependencies.apm scanning, and (2) consider adding section-name context to the removal progress message in a follow-up polish PR.
Full per-persona findings
Python Architect
- [nit] or-[] idiom creates detached list when source is falsy but non-None at
src/apm_cli/commands/uninstall/cli.py:103
prod_deps = data['dependencies']['apm'] or [] is safe here because the code reassigns prod_deps and dev_deps back into the data dict before dumping, but future editors may not notice that coupling.
Suggested: Optionally add a one-line comment that the local lists are reassigned back into data before dump_yaml.
CLI Logging Expert
- [recommended] logger.progress message does not tell the user which section the package was removed from at
src/apm_cli/commands/uninstall/cli.py:143
When a package lives in devDependencies.apm the message still says 'Removed from apm.yml', identical to prod removal. The section can matter for humans debugging manifest state.
Suggested: Capture whether the package came from devDependencies or dependencies before removal and include that section in verbose/progress output. - [nit] Success message says removed N package(s) without distinguishing dev vs prod counts at
src/apm_cli/commands/uninstall/cli.py:148
If a user removes a mix of prod and dev packages, the final message has no breakdown. Low priority because this pattern existed before the PR.
DevX UX Expert
- [recommended] Progress message does not indicate which section the package was removed from (prod vs dev) at
src/apm_cli/commands/uninstall/cli.py:141
Users debugging manifest state benefit from knowing whether uninstall removed from dependencies.apm or devDependencies.apm, especially now that both are scanned.
Suggested: Capture membership before removal and log the section name. - [nit] CHANGELOG placeholder was present in the first commit but fixed in the second
Cosmetic only; final diff has fix(uninstall): scan devDependencies.apm so --dev installs can be removed (closes #1549) #1552 and is fine.
Supply Chain Security Expert
No findings.
OSS Growth Hacker
- [recommended] CHANGELOG wording could lead with the user-positive outcome at
CHANGELOG.md:12
Release notes are a conversion surface; leading with 'apm uninstall now correctly removes packages installed with --dev' is clearer than maintainer-speak about leaked entries.
Suggested: Reword to lead with the positive outcome for users. - [nit] PR body How to test uses /tmp, which is less Windows-friendly
PR body only, but platform-neutral examples help contributor funnel polish. - [nit] This community bug-fix loop is worth mentioning in release communications
Community report to quick fix is a useful trust signal.
Auth Expert -- inactive
PR #1552 does not touch auth.py, token_manager.py, AuthResolver, or any credential resolution surface.
Doc Writer
- [recommended] docs/reference/cli/uninstall.md Behavior step 1 still says only dependencies.apm; update to cover devDependencies.apm too at
docs/src/content/docs/reference/cli/uninstall.md:78
After this PR, apm uninstall removes the entry from whichever of dependencies.apm or devDependencies.apm it lives in. The uninstall reference page is the canonical user-facing description of removal order.
Suggested: Mention dependencies.apm or devDependencies.apm, whichever section the package was installed to. - [recommended] CHANGELOG could note that synthesized empty devDependencies wrappers are dropped at
CHANGELOG.md:10
The PR body calls out that projects that never used --dev stay byte-identical. This user-observable invariant is not in the changelog.
Suggested: Append a short clause that no synthesized devDependencies block is left behind. - [nit] CHANGELOG credits @aetos382 while PR author is @danielmeppiel; confirm attribution is intended at
CHANGELOG.md:10
If this is shepherd convention, no change is needed; otherwise correct before release.
Test Coverage Expert
- [nit] No assertion on empty-devDependencies cleanup when section was synthesised (had_dev_section=False path) at
tests/unit/test_uninstall_dev_dependencies.py
The new branch that drops the devDependencies key when it was synthesized is not directly asserted. This is manifest aesthetics, not correctness, so nit.
Suggested: Add a test uninstalling a prod dependency from a manifest with no devDependencies key and assert devDependencies is still absent.
Proof (missing at):tests/unit/test_uninstall_dev_dependencies.py::TestUninstallDevDependencies::test_uninstall_drops_synthesised_dev_section-- proves: Projects that never used --dev keep a clean manifest after uninstalling a prod dependency [devx]
assert 'devDependencies' not in _read_apm_yml(tmp_path)
Performance Expert -- inactive
PR #1552 touches only uninstall manifest YAML read/write logic, CHANGELOG.md, and a new unit test file; no dependency download, materialization, cache, transport, ref-resolution, or lockfile hot-path code is modified.
This panel is advisory. Re-apply the panel-review label after addressing feedback to re-run.
Update uninstall documentation and changelog wording for the devDependencies.apm fix, make verbose removal logs name the manifest section, and add coverage that prod-only manifests do not synthesize devDependencies. Addresses panel follow-ups for PR #1552. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
APM Review Panel:
|
| Persona | B | R | N | Takeaway |
|---|---|---|---|---|
| Python Architect | 0 | 0 | 0 | The fix is narrow and keeps manifest mutation localized to uninstall. |
| CLI Logging Expert | 0 | 0 | 0 | Progress output now identifies dependencies.apm vs devDependencies.apm. |
| DevX UX Expert | 0 | 0 | 0 | The uninstall lifecycle now matches the --dev install mental model. |
| Supply Chain Security Expert | 0 | 0 | 0 | No new supply-chain or token surface is introduced. |
| OSS Growth Hacker | 0 | 0 | 0 | The changelog now leads with the user-visible outcome. |
| Doc Writer | 0 | 0 | 0 | The uninstall reference now documents both manifest sections. |
| Test Coverage Expert | 0 | 0 | 0 | Unit regression traps cover dev removal, dry run, preservation, and no synthesized dev section. |
B = blocking-severity findings, R = recommended, N = nits.
Counts are signal strength, not gates. The maintainer ships.
Architecture
flowchart TD
CLI[apm uninstall] --> Manifest[apm.yml]
Manifest --> Prod[dependencies.apm]
Manifest --> Dev[devDependencies.apm]
Prod --> Validate[_validate_uninstall_packages]
Dev --> Validate
Validate --> Remove[remove from owning section]
Remove --> Clean[drop synthetic empty devDependencies]
Folded in this run
- (panel) Updated
docs/src/content/docs/reference/cli/uninstall.mdBehavior step 1 to coverdevDependencies.apm-- resolved in6987f67ccefb303c84e7a8c392933fa97da140d1. - (panel) Reworded the CHANGELOG entry to lead with the user-positive outcome -- resolved in
6987f67ccefb303c84e7a8c392933fa97da140d1. - (panel) Included the manifest section name in uninstall progress output -- resolved in
6987f67ccefb303c84e7a8c392933fa97da140d1. - (panel) Added a regression assertion that prod-only manifests do not synthesize
devDependencies-- resolved in6987f67ccefb303c84e7a8c392933fa97da140d1.
Copilot signals reviewed
No inline Copilot review comments were present in the pull request comments API for this pass.
Regression-trap evidence (mutation-break gate)
tests/unit/test_uninstall_dev_dependencies.py::TestUninstallDevDependencies::test_uninstall_prod_dependency_does_not_synthesize_dev_section-- deleteddel data["devDependencies"]; test FAILED as expected; guard restored.
Lint contract
uv run --extra dev ruff check src/ tests/ and
uv run --extra dev ruff format --check src/ tests/ both silent.
CI
All 15 PR checks are green on 6987f67ccefb303c84e7a8c392933fa97da140d1 after 0 CI fix iteration(s).
Mergeability status
| PR | head SHA | CEO stance | iters | folds | defers | Copilot rounds | CI | mergeable | mergeStateStatus | notes |
|---|---|---|---|---|---|---|---|---|---|---|
| #1552 | 6987f67 |
ship_now | 2 | 4 | 0 | 1 | green | MERGEABLE | BLOCKED | pending required review |
Recommendation
Ready for maintainer review. Merge when the required human review policy is satisfied.
Full per-persona findings
No active panelist raised a remaining finding. Auth Expert was inactive because the touched files do not change token management, host classification, credential resolution, git/HTTP authorization headers, or AuthResolver behavior.
This panel is advisory. It does not block merge. Re-apply the panel-review label after addressing feedback to re-run.
* fix(tests): skip POSIX execute-bit assertions on Windows
Two unit tests asserted st_mode & 0o111 == 0o111, which fails on
Windows because NTFS does not honor POSIX execute bits. Guard both
with pytest.mark.skipif(sys.platform == 'win32'), matching the
existing convention used elsewhere in the suite.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(scripts): guard empty PYTEST_EXTRA_ARGS array under bash 3.2
scripts/test-integration.sh runs under `set -euo pipefail`. On macOS
the default /bin/bash is 3.2, where expanding an empty array with a
bare "${arr[@]}" raises an unbound-variable error. Local integration
runs (PYTEST_EXTRA_ARGS unset) aborted before pytest with
'extra_args[@]: unbound variable'. Use the ${arr[@]+"${arr[@]}"}
guard so the empty-array expansion is safe; CI behaviour (with
PYTEST_EXTRA_ARGS set) is unchanged.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* chore: release v0.16.1
Roll the [Unreleased] changelog into a dated 0.16.1 block and bump
pyproject.toml + uv.lock. Adds the previously-missing user-facing
entries for #1539 (apm doctor), #1566, #1569, #1567, #1553, #1552,
and #1538 surfaced by enumerating merged PRs since v0.16.0.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* ci: parallelise macOS release integration tests to fix 20min timeout
The v0.16.1 release pipeline failed on the macOS x86_64 (Intel) build:
the consolidated job's "Run integration tests" step hit its 20-minute
timeout at ~61% progress. The tests were passing -- the slower Intel
runner simply could not finish the full suite serially in time, and the
arm64 runner was also near the edge.
Unlike ci-integration.yml, which shards the suite across four runners,
the release workflow runs the whole integration suite on a single
scarce macOS runner. Parallelise it in-process with xdist (-n 2,
matching ci-integration's per-shard width to bound shared-PAT API
concurrency) using --dist loadgroup so the home_env xdist_group markers
keep HOME-mutating tests serialized on one worker. Also raise the step
timeout to 30 minutes for headroom on the slow Intel runner.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: danielmeppiel <danielmeppiel@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Closes #1549.
Problem
apm install --dev <pkg>writes the package underdevDependencies.apminapm.yml, butapm uninstall <pkg>only reads/writesdependencies.apm. The result is that every dev-installed package reportsand the entry leaks forever. There is no way to remove a
--devinstall short of editingapm.ymlby hand.Fix shape
apm uninstallnow scans bothdependencies.apmanddevDependencies.apmwhen looking for matches, and writes the package out of whichever section it lives in. Symmetric to install:--devinstalls put the entry indevDependencies.apm; uninstall pulls it back out of the same section. EmptydevDependencieswrappers that we synthesised purely to satisfy the read are dropped before writing, so manifests for projects that never used--devstay byte-identical to today.Tests
New regression trap at
tests/unit/test_uninstall_dev_dependencies.py:test_uninstall_removes_package_from_dev_dependencies-- the headline regression: a package recorded underdevDependencies.apmis removed end-to-end.test_uninstall_dry_run_finds_dev_dependency----dry-runlocates the dev entry and leavesapm.ymluntouched.test_uninstall_preserves_unrelated_dev_dependency-- removing one dev dep does not touch other dev or prod entries.Mutation-break gate: deleting the new
devDependencieshandling makes all three tests fail with the originalnot found in apm.ymlsymptom; restoring the fix makes them pass. The fulltests/unit/ -k 'uninstall or dev_depend'suite (227 tests) andtests/integration/uninstall coverage (23 tests) stay green.How to test
Validation evidence
uv run --extra dev ruff check src/ tests/-- silentuv run --extra dev ruff format --check src/ tests/-- silentuv run --extra dev python -m pylint --disable=all --enable=R0801 --min-similarity-lines=10 --fail-on=R0801 src/apm_cli/-- 10.00/10bash scripts/lint-auth-signals.sh-- cleanpytest tests/unit/ -k 'uninstall or dev_depend'-- 227 passedpytest tests/integration/test_uninstall_*.py tests/integration/test_wave5_e2e_coverage.py -k uninstall-- 23 passed, 2 skipped