Skip to content

gh-300: Improve purge handling of unrecognized files - #389

Merged
zooba merged 2 commits into
python:mainfrom
Punisheroot:feat/js/improve-purge-cleanup
Jul 31, 2026
Merged

gh-300: Improve purge handling of unrecognized files#389
zooba merged 2 commits into
python:mainfrom
Punisheroot:feat/js/improve-purge-cleanup

Conversation

@Punisheroot

Copy link
Copy Markdown
Contributor

Summary

  • Remove unrecognized files and directories left under the install directory by uninstall --purge.
  • Preserve recognized runtimes when their atomic removal reports that files are still in use.
  • Avoid traversing directory symlinks and Windows junctions while recursively deleting unknown content.

Context

uninstall --purge currently removes recognized installs and known managed directories, but unrecognized content can remain behind. This includes files from incomplete or corrupted installations that are no longer returned by install discovery.

The final cleanup cannot blindly remove everything that remains. Recognized runtimes still use the existing atomic-unlink path so that a runtime with locked executable files is not partially deleted. Unknown content may also contain a reparse point whose target is outside the managed install directory.

This change addresses the unrecognized-file cleanup described in #300. It intentionally does not add the optional less-destructive purge mode that would keep every recognized install; that interface and its exact preservation semantics can be considered separately.

Changes

Purge handling

The purge flow now records the prefixes of recognized installs whose removal raises FilesInUseError. After the existing cleanup of recognized installs, cached downloads, global configuration, and shortcuts, it enumerates the remaining entries in the install directory and removes them except for those protected prefixes.

This preserves the existing atomic-removal behavior for known runtimes while allowing broken or otherwise unrecognized content to be cleaned up.

Safe recursive deletion

rmtree now treats directory symlinks and Windows junctions as leaf entries rather than traversing them. The link or junction itself is removed while its target is left untouched.

The public rglob behavior remains unchanged because link following is still its default; only the deletion path opts into the non-following traversal.

Tests

Added coverage for:

  • removing an unknown file and an unrecognized broken-runtime directory;
  • preserving a recognized runtime when atomic removal reports files in use;
  • continuing to remove unrelated unknown content when a runtime is preserved;
  • removing a top-level Windows junction without deleting its target;
  • removing a tree containing a nested junction without deleting the external target.

Validation

Performed on Windows with Python 3.14.6 and pytest 9.1.1:

  • tests/test_uninstall_command.py tests/test_fsutils.py: PASS (11 passed);
  • full test suite: 403 passed, 1 failed;
  • git diff --check: PASS.

The full-suite failure is the unrelated tests/test_shellext.py::test_RegReadStr: the local HKCU\\Environment\\PATH value is REG_SZ, while that environment-specific test expects REG_EXPAND_SZ.

Compatibility and risk

The additional install-directory scan runs only for uninstall --purge and is linear in the number of direct entries. Normal install and uninstall operations are unchanged.

Unknown files are deliberately removed by this destructive command, but recognized prefixes that could not be atomically removed are excluded from the final sweep. Junction behavior is exercised with real Windows junctions in temporary directories; creating a directory symlink was unavailable under the local Windows privilege configuration.

@Punisheroot
Punisheroot marked this pull request as ready for review July 31, 2026 10:51
Comment thread tests/test_fsutils.py Outdated
@zooba

zooba commented Jul 31, 2026

Copy link
Copy Markdown
Member

Looks great, thanks!

@zooba
zooba merged commit 3d4e3f9 into python:main Jul 31, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants