Fix --erase ignore precedence under default-ignored ancestors#123
Conversation
Agent-Logs-Url: https://github.com/bittner/pyclean/sessions/c2f57d0d-02cf-462a-981c-183b0cec384d Co-authored-by: bittner <665072+bittner@users.noreply.github.com>
Agent-Logs-Url: https://github.com/bittner/pyclean/sessions/c2f57d0d-02cf-462a-981c-183b0cec384d Co-authored-by: bittner <665072+bittner@users.noreply.github.com>
--erase matches in ignored directories--erase ignore precedence under default-ignored ancestors
Code Review — Fix
|
| Command | Expected | PR behavior |
|---|---|---|
pyclean DIR --erase '**/*' |
deletes everything in DIR |
explicit_ignore=[] → ignore_patterns=[] → if ignore_patterns: false → no filtering ✅ |
pyclean DIR --erase '**/*' --ignore FOO |
deletes all except FOO |
filters by ['FOO'] only ✅ |
pyclean DIR --erase '**/*' --ignore DIR |
no-op | every match has DIR as ancestor → all filtered ✅ |
Other things I checked and confirmed sound:
- Traversal is untouched.
Runner.ignoreis stilldefaults + explicit, so bytecode/debris/folders/git-clean keep avoiding.git/.tox/etc. — no regression there (the issue's biggest worry). None-fallback equivalence. Whenignore_patterns is None,path_is_ignored(n, Runner.ignore)is identical to the oldRunner.is_ignored(n). Debris's directdelete_filesystem_objects(directory, pattern)call (debris.py:96) is unaffected. ✅argparsedefault=None+action='extend'. Correct:_copy_items(None)yields[], so an explicit--ignoreproduces just the user's list;dict.fromkeysthen rebuildsargs.ignoredefaults-first, deduped, order-preserving. ✅from __future__ import annotationsinerase.pyis required, not cosmetic —directory: Path | str(PEP 604) would raiseTypeErrorat def-time on Python 3.9, whichtoxstill targets. The PR got this right.- All 58 tests pass, plus
lint,format, andtytype checks — all green.
UX / behavior-change assessment
I reproduced the one behavior change that affects existing users, in /tmp:
pyclean proj --erase '**/*' --yes # proj/ contains .git/, .venv/, src/
main (v3.6.0): Total 1 files, 1 directories removed (.git + .venv survive)
PR #123: Total 4 files, 5 directories removed (.git + .venv wiped)
So --erase no longer gets the default-ignore safety net — pyclean . --erase '**/*' now wipes .git too.
This is intended — #122 explicitly designs it ("traverse DIR for better or worse and delete everything therein") — and arguably more intuitive, not less. The v3.6.0 behavior is the real surprise: --erase '**/*' reports success while silently leaving directories in place — that is #122. Making **/* mean **/* is the more logical contract, and the README already warns prominently that --erase is "potentially dangerous."
That said, it's a real change for everyone, not just the .tox-ancestor case — worth a conscious nod in the release notes.
Suggested follow-ups (none blocking)
-
Fix --ignore flag not suppressing --erase targets #119 regression test no longer guards what it was written for.
test_erase_ignored_dir_produces_no_outputcallsremove_freeform_targets(...)withoutexplicit_ignore_patterns, so it now exercises theNone→Runner.ignorefallback — a pathmain.pynever takes (it always passesexplicit_ignore_patterns=). The test passes, but it would no longer catch a break in the explicit-ignore wiring. Passexplicit_ignore_patterns=['foo']to test the production path. (Coverage isn't lost overall — the newtest_erase_applies_explicit_ignore_on_top_of_defaultsexercises the real path viapyclean.main.pyclean— but the old test is now misleading.) -
Nonevs[]dual semantics is a latent footgun.explicit_ignore_patterns=Nonemeans "fall back to defaults+explicit";[]means "no filtering." Only test code relies onNone. Consider defaultingremove_freeform_targets's parameter to[](matching production reality) and confining theNone-fallback todelete_filesystem_objectsfor debris — this resolves debpython breaks Python 2<->3 compatibility #1 cleanly too. -
Add a test for issue case 3 (
--erase+--ignore DIR→ full no-op). The PR covers cases 1 and 2 but not the "user can still protect the named directory" guarantee. -
Docs:
remove_freeform_targets's docstring wasn't updated for the newexplicit_ignore_patternsparameter (delete_filesystem_objects's was). The README--erasesection (lines 220–243) doesn't mention--ignoreinteraction at all — a one-liner ("--eraseis filtered only by explicit--ignorepatterns; the built-in default ignore list does not restrict--erase") would set the right expectation. -
Minor: the 8-element default ignore list is hardcoded in three new test spots. The two erase tests don't actually depend on its exact contents — they only need
.toxas an ancestor andexplicit_ignore=[]— so thoseignore=[...]blocks could be trimmed. (test_ignore_defaults_without_optionlegitimately asserts the full list.)
Bottom line: the implementation is correct, faithfully matches the issue's design, preserves traversal behavior, and passes all checks. The follow-ups are about test fidelity and documentation, not correctness — safe to merge with item 1 addressed.
- Default `remove_freeform_targets`'s `explicit_ignore_patterns` to `[]` internally so production callers (`main.py`) and tests both exercise the same path. The `None`-fallback in `delete_filesystem_objects` remains for direct debris call sites only. - Update the #119 regression test to pass `explicit_ignore_patterns=['foo']` so it exercises the production wiring instead of the `Runner.ignore` fallback. - Add a regression test for case 3 of issue #122's precedence table — `pyclean DIR --erase '**/*' --ignore DIR` is a no-op. - Trim the hardcoded default ignore list to the patterns that actually matter in the two new erase tests (`.tox`, `keep.txt`). - Document the erase/ignore interaction in `remove_freeform_targets`'s docstring and in the README `--erase` section. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Fixes --erase behavior so explicit erase targets aren’t accidentally suppressed by default ignore ancestors (e.g. .tox, .venv), while preserving default ignore behavior for traversal/debris cleanup.
Changes:
- Split ignore handling into
explicit_ignore(user-provided only) vsignore(defaults + explicit, deduped) in the CLI. - Adjust erase filtering so
--eraseuses only explicit ignore patterns (and not built-in defaults) when deciding what erase matches to drop. - Add regression tests covering erase under default-ignored ancestors and precedence interactions with
--ignore.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
pyclean/cli.py |
Builds args.explicit_ignore and recomputes args.ignore as defaults + explicit patterns. |
pyclean/main.py |
Wires explicit_ignore_patterns into remove_freeform_targets() for erase semantics. |
pyclean/erase.py |
Adds ignore_patterns plumbing and applies explicit-ignore-only filtering to erase matches. |
README.rst |
Documents that --erase filtering is constrained only by explicit --ignore patterns. |
tests/test_cli.py |
Verifies explicit_ignore and default ignore behavior when --ignore is omitted. |
tests/test_erase.py |
Adds regression coverage for erase precedence under default-ignored ancestors and explicit ignores. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
--erasematches were being dropped when the target directory lived under default-ignored ancestors (e.g..tox,.venv) because erase filtering reused ancestor-aware global ignore rules. This made explicit targets silently no-op for patterns like**/*.Separate explicit vs default ignore state
--ignorenow parses withdefault=None.args.explicit_ignorefor user-provided ignore patterns only.args.ignoreremains the traversal/debris list: defaults + explicit patterns (deduplicated, order-preserving).Apply erase-specific filtering semantics
remove_freeform_targets()now passes onlyexplicit_ignore_patternsto erase filtering.delete_filesystem_objects()acceptsignore_patterns; when provided, erase filtering uses that set instead of global defaults.ignore_patternsis omitted (fallback toRunner.ignore).Regression coverage for the reported behavior
.tox.--ignorestill excludes specific erase matches.--ignoreis not passed.