killpy 1.0.0__tar.gz → 1.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {killpy-1.0.0 → killpy-1.0.1}/PKG-INFO +4 -4
- {killpy-1.0.0 → killpy-1.0.1}/README.md +2 -2
- killpy-1.0.1/killpy/detectors/_shared_walk.py +110 -0
- {killpy-1.0.0 → killpy-1.0.1}/killpy/detectors/artifacts.py +1 -0
- {killpy-1.0.0 → killpy-1.0.1}/killpy/detectors/base.py +14 -0
- {killpy-1.0.0 → killpy-1.0.1}/killpy/detectors/cache.py +5 -0
- {killpy-1.0.0 → killpy-1.0.1}/killpy/detectors/tox.py +1 -0
- {killpy-1.0.0 → killpy-1.0.1}/killpy/detectors/venv.py +1 -0
- {killpy-1.0.0 → killpy-1.0.1}/killpy/scanner.py +69 -22
- {killpy-1.0.0 → killpy-1.0.1}/killpy.egg-info/PKG-INFO +4 -4
- {killpy-1.0.0 → killpy-1.0.1}/killpy.egg-info/SOURCES.txt +1 -0
- {killpy-1.0.0 → killpy-1.0.1}/pyproject.toml +9 -2
- {killpy-1.0.0 → killpy-1.0.1}/killpy/__init__.py +0 -0
- {killpy-1.0.0 → killpy-1.0.1}/killpy/__main__.py +0 -0
- {killpy-1.0.0 → killpy-1.0.1}/killpy/cleaner.py +0 -0
- {killpy-1.0.0 → killpy-1.0.1}/killpy/cleaners/__init__.py +0 -0
- {killpy-1.0.0 → killpy-1.0.1}/killpy/cli.py +0 -0
- {killpy-1.0.0 → killpy-1.0.1}/killpy/commands/__init__.py +0 -0
- {killpy-1.0.0 → killpy-1.0.1}/killpy/commands/_utils.py +0 -0
- {killpy-1.0.0 → killpy-1.0.1}/killpy/commands/clean.py +0 -0
- {killpy-1.0.0 → killpy-1.0.1}/killpy/commands/delete.py +0 -0
- {killpy-1.0.0 → killpy-1.0.1}/killpy/commands/doctor.py +0 -0
- {killpy-1.0.0 → killpy-1.0.1}/killpy/commands/find.py +0 -0
- {killpy-1.0.0 → killpy-1.0.1}/killpy/commands/list.py +0 -0
- {killpy-1.0.0 → killpy-1.0.1}/killpy/commands/stats.py +0 -0
- {killpy-1.0.0 → killpy-1.0.1}/killpy/detectors/__init__.py +0 -0
- {killpy-1.0.0 → killpy-1.0.1}/killpy/detectors/conda.py +0 -0
- {killpy-1.0.0 → killpy-1.0.1}/killpy/detectors/hatch.py +0 -0
- {killpy-1.0.0 → killpy-1.0.1}/killpy/detectors/pipenv.py +0 -0
- {killpy-1.0.0 → killpy-1.0.1}/killpy/detectors/pipx.py +0 -0
- {killpy-1.0.0 → killpy-1.0.1}/killpy/detectors/poetry.py +0 -0
- {killpy-1.0.0 → killpy-1.0.1}/killpy/detectors/pyenv.py +0 -0
- {killpy-1.0.0 → killpy-1.0.1}/killpy/detectors/uv.py +0 -0
- {killpy-1.0.0 → killpy-1.0.1}/killpy/files/__init__.py +0 -0
- {killpy-1.0.0 → killpy-1.0.1}/killpy/intelligence/__init__.py +0 -0
- {killpy-1.0.0 → killpy-1.0.1}/killpy/intelligence/git_analyzer.py +0 -0
- {killpy-1.0.0 → killpy-1.0.1}/killpy/intelligence/scoring.py +0 -0
- {killpy-1.0.0 → killpy-1.0.1}/killpy/intelligence/suggestions.py +0 -0
- {killpy-1.0.0 → killpy-1.0.1}/killpy/intelligence/tracker.py +0 -0
- {killpy-1.0.0 → killpy-1.0.1}/killpy/models.py +0 -0
- {killpy-1.0.0 → killpy-1.0.1}/killpy.egg-info/dependency_links.txt +0 -0
- {killpy-1.0.0 → killpy-1.0.1}/killpy.egg-info/entry_points.txt +0 -0
- {killpy-1.0.0 → killpy-1.0.1}/killpy.egg-info/requires.txt +0 -0
- {killpy-1.0.0 → killpy-1.0.1}/killpy.egg-info/top_level.txt +0 -0
- {killpy-1.0.0 → killpy-1.0.1}/setup.cfg +0 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: killpy
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.1
|
|
4
4
|
Summary: Find and delete unused Python environments (.venv, conda, poetry, pipenv, pipx, pyenv, uv) and caches to free disk space — from a fast terminal UI or a single CLI command.
|
|
5
5
|
Project-URL: Homepage, https://github.com/Tlaloc-Es/killpy
|
|
6
6
|
Project-URL: Documentation, https://tlaloc-es.github.io/killpy/
|
|
7
7
|
Project-URL: Repository, https://github.com/Tlaloc-Es/killpy
|
|
8
8
|
Project-URL: Bug Tracker, https://github.com/Tlaloc-Es/killpy/issues
|
|
9
9
|
Keywords: venv,virtualenv,conda,poetry,pipenv,pipx,pyenv,uv,cleanup,disk-space,cache,cli,tui,textual,developer-tools
|
|
10
|
-
Classifier: Development Status ::
|
|
10
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
11
11
|
Classifier: Environment :: Console
|
|
12
12
|
Classifier: Environment :: Console :: Curses
|
|
13
13
|
Classifier: Intended Audience :: Developers
|
|
@@ -603,7 +603,7 @@ ______________________________________________________________________
|
|
|
603
603
|
|
|
604
604
|
```yaml
|
|
605
605
|
- repo: https://github.com/Tlaloc-Es/KillPy
|
|
606
|
-
rev: 1.0.
|
|
606
|
+
rev: 1.0.1
|
|
607
607
|
hooks:
|
|
608
608
|
- id: killpy # remove __pycache__ on every commit
|
|
609
609
|
- id: killpy-clean-caches # also removes .mypy_cache, .pytest_cache, .ruff_cache
|
|
@@ -628,7 +628,7 @@ Typical minimal setup (safe for daily use):
|
|
|
628
628
|
|
|
629
629
|
```yaml
|
|
630
630
|
- repo: https://github.com/Tlaloc-Es/KillPy
|
|
631
|
-
rev: 1.0.
|
|
631
|
+
rev: 1.0.1
|
|
632
632
|
hooks:
|
|
633
633
|
- id: killpy
|
|
634
634
|
```
|
|
@@ -574,7 +574,7 @@ ______________________________________________________________________
|
|
|
574
574
|
|
|
575
575
|
```yaml
|
|
576
576
|
- repo: https://github.com/Tlaloc-Es/KillPy
|
|
577
|
-
rev: 1.0.
|
|
577
|
+
rev: 1.0.1
|
|
578
578
|
hooks:
|
|
579
579
|
- id: killpy # remove __pycache__ on every commit
|
|
580
580
|
- id: killpy-clean-caches # also removes .mypy_cache, .pytest_cache, .ruff_cache
|
|
@@ -599,7 +599,7 @@ Typical minimal setup (safe for daily use):
|
|
|
599
599
|
|
|
600
600
|
```yaml
|
|
601
601
|
- repo: https://github.com/Tlaloc-Es/KillPy
|
|
602
|
-
rev: 1.0.
|
|
602
|
+
rev: 1.0.1
|
|
603
603
|
hooks:
|
|
604
604
|
- id: killpy
|
|
605
605
|
```
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Single shared filesystem walk for the tree-walking detectors.
|
|
2
|
+
|
|
3
|
+
``venv`` / ``cache`` / ``artifacts`` / ``tox`` each used to run their own
|
|
4
|
+
``os.walk`` over the scan root, and ``get_total_size`` re-walked every
|
|
5
|
+
environment they found — walking a large tree several times over.
|
|
6
|
+
|
|
7
|
+
This module performs **one** traversal: each directory is classified as an
|
|
8
|
+
environment *container* (a venv, a cache dir, a build artifact, or ``.tox``);
|
|
9
|
+
a match is recorded, its size is summed once, and its subtree is pruned.
|
|
10
|
+
Environment pruning is always applied — once a venv is found, nothing inside it
|
|
11
|
+
is scanned again — which also collapses the cache/artifact double-counting that
|
|
12
|
+
used to happen inside environments.
|
|
13
|
+
|
|
14
|
+
Callers pass the set of detector names whose results they want (``active``).
|
|
15
|
+
Pruning happens on every container regardless of ``active``, so asking for a
|
|
16
|
+
subset yields exactly what running those detectors alone would.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import logging
|
|
22
|
+
import os
|
|
23
|
+
from datetime import datetime, timezone
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
from killpy.detectors.base import VCS_PRUNE_DIRS
|
|
27
|
+
from killpy.files import get_total_size
|
|
28
|
+
from killpy.models import Environment
|
|
29
|
+
|
|
30
|
+
logger = logging.getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
_CACHE_DIRS = frozenset({"__pycache__", ".mypy_cache", ".pytest_cache", ".ruff_cache"})
|
|
33
|
+
_ARTIFACT_EXACT = frozenset({"dist", "build"})
|
|
34
|
+
_ARTIFACT_SUFFIXES = (".egg-info", ".dist-info")
|
|
35
|
+
|
|
36
|
+
#: Map every ``Environment.type`` the shared walk (and the cache global scan)
|
|
37
|
+
#: can produce back to the detector that owns it — used to group results per
|
|
38
|
+
#: detector for progress callbacks.
|
|
39
|
+
TYPE_TO_DETECTOR: dict[str, str] = {
|
|
40
|
+
".venv": "venv",
|
|
41
|
+
"pyvenv.cfg": "venv",
|
|
42
|
+
"__pycache__": "cache",
|
|
43
|
+
".mypy_cache": "cache",
|
|
44
|
+
".pytest_cache": "cache",
|
|
45
|
+
".ruff_cache": "cache",
|
|
46
|
+
"pip-cache": "cache",
|
|
47
|
+
"uv-cache": "cache",
|
|
48
|
+
"artifacts": "artifacts",
|
|
49
|
+
"tox": "tox",
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _classify(basename: str, filenames: list[str]) -> tuple[str, str] | None:
|
|
54
|
+
"""Return ``(detector_name, env_type)`` if the dir is a container, else ``None``."""
|
|
55
|
+
if basename == ".venv" or "pyvenv.cfg" in filenames:
|
|
56
|
+
return ("venv", ".venv" if basename == ".venv" else "pyvenv.cfg")
|
|
57
|
+
if basename in _CACHE_DIRS:
|
|
58
|
+
return ("cache", basename)
|
|
59
|
+
if basename in _ARTIFACT_EXACT or any(
|
|
60
|
+
basename.endswith(suffix) for suffix in _ARTIFACT_SUFFIXES
|
|
61
|
+
):
|
|
62
|
+
return ("artifacts", "artifacts")
|
|
63
|
+
if basename == ".tox":
|
|
64
|
+
return ("tox", "tox")
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _make_env(path: Path, env_type: str) -> Environment | None:
|
|
69
|
+
"""Build an :class:`Environment` for *path*, summing its size once."""
|
|
70
|
+
try:
|
|
71
|
+
stat = path.stat()
|
|
72
|
+
return Environment(
|
|
73
|
+
path=path,
|
|
74
|
+
name=str(path),
|
|
75
|
+
type=env_type,
|
|
76
|
+
last_modified=datetime.fromtimestamp(stat.st_mtime, tz=timezone.utc),
|
|
77
|
+
size_bytes=get_total_size(path),
|
|
78
|
+
)
|
|
79
|
+
except (FileNotFoundError, OSError) as exc:
|
|
80
|
+
logger.debug("Skipping %s: %s", path, exc)
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def walk_environments(root: Path, active: set[str]) -> list[Environment]:
|
|
85
|
+
"""Walk *root* once and return environments for the ``active`` detector set.
|
|
86
|
+
|
|
87
|
+
Each directory that matches a container type is reported (when its detector
|
|
88
|
+
is in *active*) and its subtree pruned; VCS and ``site-packages`` trees are
|
|
89
|
+
never descended into.
|
|
90
|
+
"""
|
|
91
|
+
envs: list[Environment] = []
|
|
92
|
+
for current, dirnames, filenames in os.walk(root, topdown=True):
|
|
93
|
+
dirnames[:] = [d for d in dirnames if d not in VCS_PRUNE_DIRS]
|
|
94
|
+
current_path = Path(current)
|
|
95
|
+
if current_path != root:
|
|
96
|
+
match = _classify(current_path.name, filenames)
|
|
97
|
+
if match is not None:
|
|
98
|
+
detector_name, env_type = match
|
|
99
|
+
if detector_name in active:
|
|
100
|
+
env = _make_env(current_path, env_type)
|
|
101
|
+
if env is not None:
|
|
102
|
+
envs.append(env)
|
|
103
|
+
dirnames[:] = [] # env-pruning: never descend into a container
|
|
104
|
+
continue
|
|
105
|
+
# A bare ``site-packages`` (e.g. a conda env, which has no pyvenv.cfg) is
|
|
106
|
+
# not a container but must not be scanned for caches/artifacts. ``.venv``
|
|
107
|
+
# IS a container — detected on entry — so it is deliberately not pruned
|
|
108
|
+
# here (that would stop us from ever reporting it).
|
|
109
|
+
dirnames[:] = [d for d in dirnames if d != "site-packages"]
|
|
110
|
+
return envs
|
|
@@ -42,6 +42,7 @@ class ArtifactsDetector(AbstractDetector):
|
|
|
42
42
|
|
|
43
43
|
name = "artifacts"
|
|
44
44
|
always_available = True # pure filesystem walk
|
|
45
|
+
shared_walk = True # tree scan served by killpy.detectors._shared_walk
|
|
45
46
|
|
|
46
47
|
def detect(self, path: Path) -> list[Environment]:
|
|
47
48
|
envs: list[Environment] = []
|
|
@@ -62,6 +62,11 @@ class AbstractDetector(ABC):
|
|
|
62
62
|
required_tool: ClassVar[str | None] = None
|
|
63
63
|
#: ``True`` for detectors that always apply (pure filesystem walks).
|
|
64
64
|
always_available: ClassVar[bool] = False
|
|
65
|
+
#: ``True`` when this detector's tree scan is served by the single shared
|
|
66
|
+
#: walk (``killpy.detectors._shared_walk``) instead of its own ``os.walk``.
|
|
67
|
+
#: The :class:`~killpy.scanner.Scanner` batches all such detectors into one
|
|
68
|
+
#: traversal; ``detect`` is kept for direct use and tests.
|
|
69
|
+
shared_walk: ClassVar[bool] = False
|
|
65
70
|
|
|
66
71
|
@abstractmethod
|
|
67
72
|
def detect(self, path: Path) -> list[Environment]:
|
|
@@ -78,6 +83,15 @@ class AbstractDetector(ABC):
|
|
|
78
83
|
use it as the walk root.
|
|
79
84
|
"""
|
|
80
85
|
|
|
86
|
+
def scan_global(self, path: Path) -> list[Environment]:
|
|
87
|
+
"""Environments found outside the shared tree walk (e.g. global caches).
|
|
88
|
+
|
|
89
|
+
Empty by default; :class:`~killpy.detectors.cache.CacheDetector` overrides
|
|
90
|
+
it to report the global pip/uv caches. Called by the Scanner only for
|
|
91
|
+
detectors with ``shared_walk = True``.
|
|
92
|
+
"""
|
|
93
|
+
return []
|
|
94
|
+
|
|
81
95
|
def _candidate_dirs(self) -> tuple[Path, ...]:
|
|
82
96
|
"""Directories whose existence makes this detector applicable.
|
|
83
97
|
|
|
@@ -72,6 +72,7 @@ class CacheDetector(AbstractDetector):
|
|
|
72
72
|
|
|
73
73
|
name = "cache"
|
|
74
74
|
always_available = True # pure filesystem walk
|
|
75
|
+
shared_walk = True # local tree scan served by killpy.detectors._shared_walk
|
|
75
76
|
|
|
76
77
|
def detect(self, path: Path) -> list[Environment]:
|
|
77
78
|
envs: list[Environment] = []
|
|
@@ -79,6 +80,10 @@ class CacheDetector(AbstractDetector):
|
|
|
79
80
|
envs.extend(self._scan_global(path))
|
|
80
81
|
return envs
|
|
81
82
|
|
|
83
|
+
def scan_global(self, path: Path) -> list[Environment]:
|
|
84
|
+
"""Global pip/uv caches — the part not covered by the shared tree walk."""
|
|
85
|
+
return self._scan_global(path)
|
|
86
|
+
|
|
82
87
|
# ------------------------------------------------------------------ #
|
|
83
88
|
|
|
84
89
|
def _scan_local(self, root: Path) -> list[Environment]:
|
|
@@ -19,6 +19,7 @@ class ToxDetector(AbstractDetector):
|
|
|
19
19
|
|
|
20
20
|
name = "tox"
|
|
21
21
|
always_available = True # pure filesystem walk
|
|
22
|
+
shared_walk = True # tree scan served by killpy.detectors._shared_walk
|
|
22
23
|
|
|
23
24
|
def detect(self, path: Path) -> list[Environment]:
|
|
24
25
|
envs: list[Environment] = []
|
|
@@ -57,6 +57,7 @@ class VenvDetector(AbstractDetector):
|
|
|
57
57
|
|
|
58
58
|
name = "venv"
|
|
59
59
|
always_available = True # pure filesystem walk
|
|
60
|
+
shared_walk = True # tree scan served by killpy.detectors._shared_walk
|
|
60
61
|
|
|
61
62
|
def detect(self, path: Path) -> list[Environment]:
|
|
62
63
|
# Inner dedup: the ".venv" scan and the pyvenv.cfg scan below can both
|
|
@@ -22,6 +22,7 @@ from collections.abc import AsyncIterator, Callable
|
|
|
22
22
|
from pathlib import Path
|
|
23
23
|
|
|
24
24
|
from killpy.detectors import ALL_DETECTORS, AbstractDetector
|
|
25
|
+
from killpy.detectors._shared_walk import TYPE_TO_DETECTOR, walk_environments
|
|
25
26
|
from killpy.detectors.pyenv import _pyenv_root
|
|
26
27
|
from killpy.models import Environment
|
|
27
28
|
|
|
@@ -85,24 +86,28 @@ class Scanner:
|
|
|
85
86
|
seen: set[Path] = set()
|
|
86
87
|
results: list[Environment] = []
|
|
87
88
|
|
|
88
|
-
for
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
89
|
+
applicable = [d for d in self._detectors if d.can_handle()]
|
|
90
|
+
shared = [d for d in applicable if d.shared_walk]
|
|
91
|
+
others = [d for d in applicable if not d.shared_walk]
|
|
92
|
+
|
|
93
|
+
# One traversal shared by every filesystem-walking detector.
|
|
94
|
+
for detector, found in self._shared_walk_groups(shared, path):
|
|
95
|
+
processed = self._process(found, seen)
|
|
96
|
+
results.extend(processed)
|
|
97
|
+
if on_progress is not None:
|
|
98
|
+
on_progress(detector, processed)
|
|
99
|
+
|
|
100
|
+
# The remaining detectors scan their own global directories.
|
|
101
|
+
for detector in others:
|
|
92
102
|
try:
|
|
93
103
|
found = detector.detect(path)
|
|
94
104
|
except Exception as exc: # noqa: BLE001
|
|
95
105
|
logger.warning("Detector %s raised: %s", detector.name, exc)
|
|
96
106
|
found = []
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
deduped = self._apply_exclusions(deduped)
|
|
100
|
-
for env in deduped:
|
|
101
|
-
self._mark_system_critical(env)
|
|
102
|
-
results.extend(deduped)
|
|
103
|
-
|
|
107
|
+
processed = self._process(found, seen)
|
|
108
|
+
results.extend(processed)
|
|
104
109
|
if on_progress is not None:
|
|
105
|
-
on_progress(detector,
|
|
110
|
+
on_progress(detector, processed)
|
|
106
111
|
|
|
107
112
|
results.sort(key=lambda e: e.size_bytes, reverse=True)
|
|
108
113
|
return results
|
|
@@ -123,28 +128,70 @@ class Scanner:
|
|
|
123
128
|
table.add_row(env)
|
|
124
129
|
"""
|
|
125
130
|
applicable = [d for d in self._detectors if d.can_handle()]
|
|
131
|
+
shared = [d for d in applicable if d.shared_walk]
|
|
132
|
+
others = [d for d in applicable if not d.shared_walk]
|
|
126
133
|
seen: set[Path] = set()
|
|
127
134
|
|
|
128
|
-
async def
|
|
135
|
+
async def _run_shared() -> list[tuple[AbstractDetector, list[Environment]]]:
|
|
129
136
|
try:
|
|
130
|
-
return
|
|
137
|
+
return await asyncio.to_thread(self._shared_walk_groups, shared, path)
|
|
138
|
+
except Exception as exc: # noqa: BLE001
|
|
139
|
+
logger.warning("Shared walk raised: %s", exc)
|
|
140
|
+
return [(d, []) for d in shared]
|
|
141
|
+
|
|
142
|
+
async def _run_one(
|
|
143
|
+
detector: AbstractDetector,
|
|
144
|
+
) -> list[tuple[AbstractDetector, list[Environment]]]:
|
|
145
|
+
try:
|
|
146
|
+
return [(detector, await asyncio.to_thread(detector.detect, path))]
|
|
131
147
|
except Exception as exc: # noqa: BLE001
|
|
132
148
|
logger.warning("Detector %s raised: %s", detector.name, exc)
|
|
133
|
-
return detector, []
|
|
149
|
+
return [(detector, [])]
|
|
150
|
+
|
|
151
|
+
tasks = [asyncio.create_task(_run_one(d)) for d in others]
|
|
152
|
+
if shared:
|
|
153
|
+
tasks.append(asyncio.create_task(_run_shared()))
|
|
134
154
|
|
|
135
|
-
tasks = [asyncio.create_task(_run(d)) for d in applicable]
|
|
136
155
|
for coro in asyncio.as_completed(tasks):
|
|
137
|
-
detector, found
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
for env in deduped:
|
|
141
|
-
self._mark_system_critical(env)
|
|
142
|
-
yield detector, deduped
|
|
156
|
+
for detector, found in await coro:
|
|
157
|
+
deduped = self._process(found, seen)
|
|
158
|
+
yield detector, deduped
|
|
143
159
|
|
|
144
160
|
# ------------------------------------------------------------------ #
|
|
145
161
|
# Helpers #
|
|
146
162
|
# ------------------------------------------------------------------ #
|
|
147
163
|
|
|
164
|
+
def _shared_walk_groups(
|
|
165
|
+
self, shared: list[AbstractDetector], path: Path
|
|
166
|
+
) -> list[tuple[AbstractDetector, list[Environment]]]:
|
|
167
|
+
"""Run the one shared walk, returning ``(detector, envs)`` per detector.
|
|
168
|
+
|
|
169
|
+
The local tree is walked once for the union of *shared* detector names;
|
|
170
|
+
each detector also contributes its own global scan (pip/uv caches).
|
|
171
|
+
Results are grouped back per detector via :data:`TYPE_TO_DETECTOR` so the
|
|
172
|
+
per-detector progress contract is preserved.
|
|
173
|
+
"""
|
|
174
|
+
if not shared:
|
|
175
|
+
return []
|
|
176
|
+
active = {d.name for d in shared}
|
|
177
|
+
found = walk_environments(path, active)
|
|
178
|
+
for detector in shared:
|
|
179
|
+
found.extend(detector.scan_global(path))
|
|
180
|
+
by_name: dict[str, list[Environment]] = {d.name: [] for d in shared}
|
|
181
|
+
for env in found:
|
|
182
|
+
name = TYPE_TO_DETECTOR.get(env.type)
|
|
183
|
+
if name in by_name:
|
|
184
|
+
by_name[name].append(env)
|
|
185
|
+
return [(d, by_name[d.name]) for d in shared]
|
|
186
|
+
|
|
187
|
+
def _process(self, found: list[Environment], seen: set[Path]) -> list[Environment]:
|
|
188
|
+
"""Deduplicate, apply exclusions, and flag system-critical envs."""
|
|
189
|
+
deduped = self._deduplicate(found, seen)
|
|
190
|
+
deduped = self._apply_exclusions(deduped)
|
|
191
|
+
for env in deduped:
|
|
192
|
+
self._mark_system_critical(env)
|
|
193
|
+
return deduped
|
|
194
|
+
|
|
148
195
|
@staticmethod
|
|
149
196
|
def _deduplicate(envs: list[Environment], seen: set[Path]) -> list[Environment]:
|
|
150
197
|
"""Filter out environments whose resolved path has already been seen.
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: killpy
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.1
|
|
4
4
|
Summary: Find and delete unused Python environments (.venv, conda, poetry, pipenv, pipx, pyenv, uv) and caches to free disk space — from a fast terminal UI or a single CLI command.
|
|
5
5
|
Project-URL: Homepage, https://github.com/Tlaloc-Es/killpy
|
|
6
6
|
Project-URL: Documentation, https://tlaloc-es.github.io/killpy/
|
|
7
7
|
Project-URL: Repository, https://github.com/Tlaloc-Es/killpy
|
|
8
8
|
Project-URL: Bug Tracker, https://github.com/Tlaloc-Es/killpy/issues
|
|
9
9
|
Keywords: venv,virtualenv,conda,poetry,pipenv,pipx,pyenv,uv,cleanup,disk-space,cache,cli,tui,textual,developer-tools
|
|
10
|
-
Classifier: Development Status ::
|
|
10
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
11
11
|
Classifier: Environment :: Console
|
|
12
12
|
Classifier: Environment :: Console :: Curses
|
|
13
13
|
Classifier: Intended Audience :: Developers
|
|
@@ -603,7 +603,7 @@ ______________________________________________________________________
|
|
|
603
603
|
|
|
604
604
|
```yaml
|
|
605
605
|
- repo: https://github.com/Tlaloc-Es/KillPy
|
|
606
|
-
rev: 1.0.
|
|
606
|
+
rev: 1.0.1
|
|
607
607
|
hooks:
|
|
608
608
|
- id: killpy # remove __pycache__ on every commit
|
|
609
609
|
- id: killpy-clean-caches # also removes .mypy_cache, .pytest_cache, .ruff_cache
|
|
@@ -628,7 +628,7 @@ Typical minimal setup (safe for daily use):
|
|
|
628
628
|
|
|
629
629
|
```yaml
|
|
630
630
|
- repo: https://github.com/Tlaloc-Es/KillPy
|
|
631
|
-
rev: 1.0.
|
|
631
|
+
rev: 1.0.1
|
|
632
632
|
hooks:
|
|
633
633
|
- id: killpy
|
|
634
634
|
```
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "killpy"
|
|
3
|
-
version = "1.0.
|
|
3
|
+
version = "1.0.1"
|
|
4
4
|
description = "Find and delete unused Python environments (.venv, conda, poetry, pipenv, pipx, pyenv, uv) and caches to free disk space — from a fast terminal UI or a single CLI command."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
keywords = [
|
|
@@ -21,7 +21,7 @@ keywords = [
|
|
|
21
21
|
"developer-tools",
|
|
22
22
|
]
|
|
23
23
|
classifiers = [
|
|
24
|
-
"Development Status ::
|
|
24
|
+
"Development Status :: 5 - Production/Stable",
|
|
25
25
|
"Environment :: Console",
|
|
26
26
|
"Environment :: Console :: Curses",
|
|
27
27
|
"Intended Audience :: Developers",
|
|
@@ -75,6 +75,9 @@ package = true
|
|
|
75
75
|
[tool.pytest.ini_options]
|
|
76
76
|
testpaths = ["tests"]
|
|
77
77
|
addopts = "-v --tb=short --cov=killpy --cov-report=term-missing --cov-report=xml"
|
|
78
|
+
markers = [
|
|
79
|
+
"integration: system-level end-to-end tests (real envs + real deletion); require KILLPY_INTEGRATION=1",
|
|
80
|
+
]
|
|
78
81
|
|
|
79
82
|
[tool.coverage.run]
|
|
80
83
|
branch = true
|
|
@@ -103,5 +106,9 @@ lint = "ruff check killpy tests"
|
|
|
103
106
|
format = "ruff format killpy tests"
|
|
104
107
|
type = "mypy killpy"
|
|
105
108
|
|
|
109
|
+
[tool.poe.tasks.integration]
|
|
110
|
+
cmd = "pytest -m integration --no-cov tests/integration"
|
|
111
|
+
env = { KILLPY_INTEGRATION = "1" }
|
|
112
|
+
|
|
106
113
|
[project.scripts]
|
|
107
114
|
killpy = "killpy.__main__:cli"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|