killpy 0.24.1__tar.gz → 0.24.2__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-0.24.1 → killpy-0.24.2}/PKG-INFO +2 -2
- {killpy-0.24.1 → killpy-0.24.2}/README.md +1 -1
- {killpy-0.24.1 → killpy-0.24.2}/killpy/__main__.py +12 -6
- {killpy-0.24.1 → killpy-0.24.2}/killpy/cleaner.py +19 -2
- {killpy-0.24.1 → killpy-0.24.2}/killpy/cli.py +2 -1
- {killpy-0.24.1 → killpy-0.24.2}/killpy/detectors/artifacts.py +14 -3
- {killpy-0.24.1 → killpy-0.24.2}/killpy/detectors/cache.py +40 -13
- killpy-0.24.2/killpy/detectors/uv.py +96 -0
- {killpy-0.24.1 → killpy-0.24.2}/killpy/intelligence/suggestions.py +0 -8
- {killpy-0.24.1 → killpy-0.24.2}/killpy/models.py +2 -2
- {killpy-0.24.1 → killpy-0.24.2}/killpy.egg-info/PKG-INFO +2 -2
- {killpy-0.24.1 → killpy-0.24.2}/pyproject.toml +1 -1
- killpy-0.24.1/killpy/detectors/uv.py +0 -64
- {killpy-0.24.1 → killpy-0.24.2}/killpy/__init__.py +0 -0
- {killpy-0.24.1 → killpy-0.24.2}/killpy/cleaners/__init__.py +0 -0
- {killpy-0.24.1 → killpy-0.24.2}/killpy/commands/__init__.py +0 -0
- {killpy-0.24.1 → killpy-0.24.2}/killpy/commands/_utils.py +0 -0
- {killpy-0.24.1 → killpy-0.24.2}/killpy/commands/clean.py +0 -0
- {killpy-0.24.1 → killpy-0.24.2}/killpy/commands/delete.py +0 -0
- {killpy-0.24.1 → killpy-0.24.2}/killpy/commands/doctor.py +0 -0
- {killpy-0.24.1 → killpy-0.24.2}/killpy/commands/find.py +0 -0
- {killpy-0.24.1 → killpy-0.24.2}/killpy/commands/list.py +0 -0
- {killpy-0.24.1 → killpy-0.24.2}/killpy/commands/stats.py +0 -0
- {killpy-0.24.1 → killpy-0.24.2}/killpy/detectors/__init__.py +0 -0
- {killpy-0.24.1 → killpy-0.24.2}/killpy/detectors/base.py +0 -0
- {killpy-0.24.1 → killpy-0.24.2}/killpy/detectors/conda.py +0 -0
- {killpy-0.24.1 → killpy-0.24.2}/killpy/detectors/hatch.py +0 -0
- {killpy-0.24.1 → killpy-0.24.2}/killpy/detectors/pipenv.py +0 -0
- {killpy-0.24.1 → killpy-0.24.2}/killpy/detectors/pipx.py +0 -0
- {killpy-0.24.1 → killpy-0.24.2}/killpy/detectors/poetry.py +0 -0
- {killpy-0.24.1 → killpy-0.24.2}/killpy/detectors/pyenv.py +0 -0
- {killpy-0.24.1 → killpy-0.24.2}/killpy/detectors/tox.py +0 -0
- {killpy-0.24.1 → killpy-0.24.2}/killpy/detectors/venv.py +0 -0
- {killpy-0.24.1 → killpy-0.24.2}/killpy/files/__init__.py +0 -0
- {killpy-0.24.1 → killpy-0.24.2}/killpy/intelligence/__init__.py +0 -0
- {killpy-0.24.1 → killpy-0.24.2}/killpy/intelligence/git_analyzer.py +0 -0
- {killpy-0.24.1 → killpy-0.24.2}/killpy/intelligence/scoring.py +0 -0
- {killpy-0.24.1 → killpy-0.24.2}/killpy/intelligence/tracker.py +0 -0
- {killpy-0.24.1 → killpy-0.24.2}/killpy/scanner.py +0 -0
- {killpy-0.24.1 → killpy-0.24.2}/killpy.egg-info/SOURCES.txt +0 -0
- {killpy-0.24.1 → killpy-0.24.2}/killpy.egg-info/dependency_links.txt +0 -0
- {killpy-0.24.1 → killpy-0.24.2}/killpy.egg-info/entry_points.txt +0 -0
- {killpy-0.24.1 → killpy-0.24.2}/killpy.egg-info/requires.txt +0 -0
- {killpy-0.24.1 → killpy-0.24.2}/killpy.egg-info/top_level.txt +0 -0
- {killpy-0.24.1 → killpy-0.24.2}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: killpy
|
|
3
|
-
Version: 0.24.
|
|
3
|
+
Version: 0.24.2
|
|
4
4
|
Summary: List all .venv directories and Conda environments 🐍 on your system and check how much space they are using. You can then choose which ones to delete in order to free up space 🧹.
|
|
5
5
|
Project-URL: Homepage, https://github.com/Tlaloc-Es/killpy
|
|
6
6
|
Project-URL: Documentation, https://tlaloc-es.github.io/killpy/
|
|
@@ -587,7 +587,7 @@ Typical minimal setup (safe for daily use):
|
|
|
587
587
|
|
|
588
588
|
```yaml
|
|
589
589
|
- repo: https://github.com/Tlaloc-Es/KillPy
|
|
590
|
-
rev: 0.24.
|
|
590
|
+
rev: 0.24.2
|
|
591
591
|
hooks:
|
|
592
592
|
- id: killpy
|
|
593
593
|
```
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import logging
|
|
2
|
+
import sys
|
|
2
3
|
from pathlib import Path
|
|
3
4
|
|
|
4
5
|
import click
|
|
@@ -6,7 +7,7 @@ from rich.console import Console
|
|
|
6
7
|
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
7
8
|
from rich.prompt import Confirm
|
|
8
9
|
|
|
9
|
-
from killpy.cleaner import Cleaner
|
|
10
|
+
from killpy.cleaner import Cleaner, CleanerError
|
|
10
11
|
from killpy.cli import TableApp
|
|
11
12
|
from killpy.commands.clean import clean
|
|
12
13
|
from killpy.commands.delete import delete_cmd
|
|
@@ -62,17 +63,19 @@ def _run_delete_all(path: Path, excluded: set[str], yes: bool) -> None:
|
|
|
62
63
|
|
|
63
64
|
deleted = 0
|
|
64
65
|
freed = 0
|
|
66
|
+
errors = 0
|
|
65
67
|
for env in envs:
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
freed += size
|
|
68
|
+
try:
|
|
69
|
+
freed += cleaner.delete(env)
|
|
69
70
|
deleted += 1
|
|
70
|
-
|
|
71
|
-
console.print(f"[red]Failed to delete:[/red] {env.path}")
|
|
71
|
+
except CleanerError as exc:
|
|
72
|
+
console.print(f" [red]✗ Failed to delete:[/red] {env.path}: {exc}")
|
|
73
|
+
errors += 1
|
|
72
74
|
|
|
73
75
|
console.print(
|
|
74
76
|
f"\n[bold green]Done.[/bold green] Deleted {deleted}/{len(envs)} environment(s), " # noqa: E501
|
|
75
77
|
f"freed [bold]{format_size(freed)}[/bold]."
|
|
78
|
+
+ (f" [red]{errors} error(s).[/red]" if errors else "")
|
|
76
79
|
)
|
|
77
80
|
|
|
78
81
|
# Best-effort: update the history record created above with freed bytes.
|
|
@@ -81,6 +84,9 @@ def _run_delete_all(path: Path, excluded: set[str], yes: bool) -> None:
|
|
|
81
84
|
except Exception: # noqa: BLE001
|
|
82
85
|
pass
|
|
83
86
|
|
|
87
|
+
if errors:
|
|
88
|
+
sys.exit(1)
|
|
89
|
+
|
|
84
90
|
|
|
85
91
|
@click.group(invoke_without_command=True)
|
|
86
92
|
@click.option(
|
|
@@ -11,8 +11,8 @@ Usage::
|
|
|
11
11
|
The :class:`Cleaner` dispatches to the correct removal strategy based on
|
|
12
12
|
:attr:`~killpy.models.Environment.managed_by`. Environments with
|
|
13
13
|
``managed_by="conda"`` are removed via ``conda env remove``;
|
|
14
|
-
``managed_by="pipx"`` via ``pipx uninstall``;
|
|
15
|
-
:func:`shutil.rmtree`.
|
|
14
|
+
``managed_by="pipx"`` via ``pipx uninstall``; ``managed_by="uv"`` via
|
|
15
|
+
``uv tool uninstall``; all others via :func:`shutil.rmtree`.
|
|
16
16
|
"""
|
|
17
17
|
|
|
18
18
|
from __future__ import annotations
|
|
@@ -77,6 +77,8 @@ class Cleaner:
|
|
|
77
77
|
self._remove_conda(env.name)
|
|
78
78
|
elif env.managed_by == "pipx":
|
|
79
79
|
self._remove_pipx(env.name)
|
|
80
|
+
elif env.managed_by == "uv":
|
|
81
|
+
self._remove_uv_tool(env.name)
|
|
80
82
|
else:
|
|
81
83
|
self._remove_filesystem(env.path)
|
|
82
84
|
except CleanerError:
|
|
@@ -161,3 +163,18 @@ class Cleaner:
|
|
|
161
163
|
raise CleanerError(
|
|
162
164
|
f"pipx uninstall failed for '{package_name}': {result.stderr.strip()}"
|
|
163
165
|
)
|
|
166
|
+
|
|
167
|
+
@staticmethod
|
|
168
|
+
def _remove_uv_tool(tool_name: str) -> None:
|
|
169
|
+
if shutil.which("uv") is None:
|
|
170
|
+
raise CleanerError("uv not found on PATH")
|
|
171
|
+
result = subprocess.run(
|
|
172
|
+
["uv", "tool", "uninstall", tool_name],
|
|
173
|
+
check=False,
|
|
174
|
+
capture_output=True,
|
|
175
|
+
text=True,
|
|
176
|
+
)
|
|
177
|
+
if result.returncode != 0:
|
|
178
|
+
raise CleanerError(
|
|
179
|
+
f"uv tool uninstall failed for '{tool_name}': {result.stderr.strip()}"
|
|
180
|
+
)
|
|
@@ -714,8 +714,9 @@ class TableApp(App):
|
|
|
714
714
|
)
|
|
715
715
|
self.bell()
|
|
716
716
|
|
|
717
|
-
@is_venv_tab
|
|
718
717
|
def delete_environment(self, environment: Environment) -> bool:
|
|
718
|
+
# Shared helper — no tab gating here: it is called from both the
|
|
719
|
+
# venv tab (delete actions) and the pipx tab (uninstall action).
|
|
719
720
|
try:
|
|
720
721
|
self.cleaner.delete(environment)
|
|
721
722
|
return True
|
|
@@ -14,6 +14,10 @@ from killpy.models import Environment
|
|
|
14
14
|
logger = logging.getLogger(__name__)
|
|
15
15
|
|
|
16
16
|
_PRUNED: frozenset[str] = frozenset({".git", ".hg", ".svn", "node_modules"})
|
|
17
|
+
# Environment internals are not build artifacts: every installed package has
|
|
18
|
+
# a ``site-packages/*.dist-info`` directory whose deletion corrupts the env.
|
|
19
|
+
# Environments are VenvDetector territory.
|
|
20
|
+
_ENV_DIRS: frozenset[str] = frozenset({".venv", "site-packages"})
|
|
17
21
|
_EXACT_NAMES: frozenset[str] = frozenset({"dist", "build"})
|
|
18
22
|
_SUFFIXES: tuple[str, ...] = (".egg-info", ".dist-info")
|
|
19
23
|
|
|
@@ -28,7 +32,10 @@ class ArtifactsDetector(AbstractDetector):
|
|
|
28
32
|
"""Detects Python build artifact directories.
|
|
29
33
|
|
|
30
34
|
Finds ``dist/``, ``build/``, ``*.egg-info/``, and ``*.dist-info/``
|
|
31
|
-
directories under the scan root.
|
|
35
|
+
directories under the scan root. Virtual environments (``.venv``,
|
|
36
|
+
any directory containing ``pyvenv.cfg``) and ``site-packages`` trees
|
|
37
|
+
are skipped: their ``*.dist-info`` entries are package metadata, not
|
|
38
|
+
build output.
|
|
32
39
|
"""
|
|
33
40
|
|
|
34
41
|
name = "artifacts"
|
|
@@ -38,10 +45,14 @@ class ArtifactsDetector(AbstractDetector):
|
|
|
38
45
|
|
|
39
46
|
def detect(self, path: Path) -> list[Environment]:
|
|
40
47
|
envs: list[Environment] = []
|
|
41
|
-
for current_root, directories,
|
|
48
|
+
for current_root, directories, files in os.walk(path, topdown=True):
|
|
49
|
+
if "pyvenv.cfg" in files:
|
|
50
|
+
# Inside a virtual environment (whatever its name) — skip it.
|
|
51
|
+
directories[:] = []
|
|
52
|
+
continue
|
|
42
53
|
pruned = set()
|
|
43
54
|
for d in directories:
|
|
44
|
-
if d in _PRUNED:
|
|
55
|
+
if d in _PRUNED or d in _ENV_DIRS:
|
|
45
56
|
pruned.add(d)
|
|
46
57
|
continue
|
|
47
58
|
if _is_artifact_dir(d):
|
|
@@ -5,8 +5,8 @@ Covers:
|
|
|
5
5
|
* ``.mypy_cache``
|
|
6
6
|
* ``.pytest_cache``
|
|
7
7
|
* ``.ruff_cache``
|
|
8
|
-
* Global pip cache (``~/.cache/pip``)
|
|
9
|
-
* Global uv cache (``~/.cache/uv``)
|
|
8
|
+
* Global pip cache (``~/.cache/pip``) — only when inside the scanned path
|
|
9
|
+
* Global uv cache (``~/.cache/uv``) — only when inside the scanned path
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
12
|
from __future__ import annotations
|
|
@@ -33,6 +33,11 @@ _LOCAL_CACHE_DIRS: tuple[str, ...] = (
|
|
|
33
33
|
# Directories to prune from the walk so we don't descend into them.
|
|
34
34
|
_PRUNED: frozenset[str] = frozenset({".git", ".hg", ".svn", "node_modules"})
|
|
35
35
|
|
|
36
|
+
# Caches inside an environment belong to the environment: VenvDetector
|
|
37
|
+
# already reports the whole tree, so listing them separately double-counts
|
|
38
|
+
# their size in stats/list totals.
|
|
39
|
+
_ENV_DIRS: frozenset[str] = frozenset({".venv", "site-packages"})
|
|
40
|
+
|
|
36
41
|
|
|
37
42
|
class CacheDetector(AbstractDetector):
|
|
38
43
|
"""Detects local and global Python cache directories."""
|
|
@@ -45,18 +50,26 @@ class CacheDetector(AbstractDetector):
|
|
|
45
50
|
def detect(self, path: Path) -> list[Environment]:
|
|
46
51
|
envs: list[Environment] = []
|
|
47
52
|
envs.extend(self._scan_local(path))
|
|
48
|
-
envs.extend(self._scan_global())
|
|
53
|
+
envs.extend(self._scan_global(path))
|
|
49
54
|
return envs
|
|
50
55
|
|
|
51
56
|
# ------------------------------------------------------------------ #
|
|
52
57
|
|
|
53
58
|
def _scan_local(self, root: Path) -> list[Environment]:
|
|
54
|
-
"""Walk *root* and collect all known local cache directories.
|
|
59
|
+
"""Walk *root* and collect all known local cache directories.
|
|
60
|
+
|
|
61
|
+
Virtual environments (``.venv``, any directory containing
|
|
62
|
+
``pyvenv.cfg``) and ``site-packages`` trees are skipped.
|
|
63
|
+
"""
|
|
55
64
|
results: list[Environment] = []
|
|
56
|
-
for current_root, directories,
|
|
65
|
+
for current_root, directories, files in os.walk(root, topdown=True):
|
|
66
|
+
if "pyvenv.cfg" in files:
|
|
67
|
+
# Inside a virtual environment (whatever its name) — skip it.
|
|
68
|
+
directories[:] = []
|
|
69
|
+
continue
|
|
57
70
|
prune = set()
|
|
58
71
|
for d in directories:
|
|
59
|
-
if d in _PRUNED:
|
|
72
|
+
if d in _PRUNED or d in _ENV_DIRS:
|
|
60
73
|
prune.add(d)
|
|
61
74
|
continue
|
|
62
75
|
if d in _LOCAL_CACHE_DIRS:
|
|
@@ -70,19 +83,33 @@ class CacheDetector(AbstractDetector):
|
|
|
70
83
|
directories[:] = [d for d in directories if d not in prune]
|
|
71
84
|
return results
|
|
72
85
|
|
|
73
|
-
def _scan_global(self) -> list[Environment]:
|
|
74
|
-
"""Return global pip
|
|
86
|
+
def _scan_global(self, root: Path) -> list[Environment]:
|
|
87
|
+
"""Return global pip/uv cache directories that live under *root*.
|
|
88
|
+
|
|
89
|
+
A scan scoped to a project directory (e.g. the pre-commit hooks or
|
|
90
|
+
``killpy delete --type cache --path <repo>``) must never surface —
|
|
91
|
+
and therefore never delete — caches outside that directory. The
|
|
92
|
+
global caches are still reported when the scan root contains them
|
|
93
|
+
(e.g. ``killpy --path ~``).
|
|
94
|
+
"""
|
|
95
|
+
try:
|
|
96
|
+
scan_root = root.resolve()
|
|
97
|
+
except OSError:
|
|
98
|
+
scan_root = root
|
|
75
99
|
results: list[Environment] = []
|
|
76
100
|
candidates = [
|
|
77
101
|
(Path.home() / ".cache" / "pip", "pip-cache"),
|
|
78
102
|
(Path.home() / ".cache" / "uv", "uv-cache"),
|
|
79
103
|
]
|
|
80
104
|
for cache_path, tag in candidates:
|
|
81
|
-
if cache_path.exists():
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
105
|
+
if not cache_path.exists():
|
|
106
|
+
continue
|
|
107
|
+
try:
|
|
108
|
+
if not cache_path.resolve().is_relative_to(scan_root):
|
|
109
|
+
continue
|
|
110
|
+
results.append(_make_cache_env(cache_path, tag))
|
|
111
|
+
except (FileNotFoundError, OSError) as exc:
|
|
112
|
+
logger.debug("Skipping global cache %s: %s", cache_path, exc)
|
|
86
113
|
return results
|
|
87
114
|
|
|
88
115
|
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Detector for disk space owned by uv itself (tool envs and Pythons)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
import shutil
|
|
8
|
+
from datetime import datetime, timezone
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from killpy.detectors.base import AbstractDetector
|
|
12
|
+
from killpy.files import get_total_size
|
|
13
|
+
from killpy.models import Environment
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _uv_data_dir() -> Path:
|
|
19
|
+
xdg = os.environ.get("XDG_DATA_HOME")
|
|
20
|
+
base = Path(xdg) if xdg else Path.home() / ".local" / "share"
|
|
21
|
+
return base / "uv"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _uv_tools_dir() -> Path:
|
|
25
|
+
override = os.environ.get("UV_TOOL_DIR")
|
|
26
|
+
return Path(override) if override else _uv_data_dir() / "tools"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _uv_python_dir() -> Path:
|
|
30
|
+
override = os.environ.get("UV_PYTHON_INSTALL_DIR")
|
|
31
|
+
return Path(override) if override else _uv_data_dir() / "python"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class UvDetector(AbstractDetector):
|
|
35
|
+
"""Detects environments managed by uv itself.
|
|
36
|
+
|
|
37
|
+
Covers:
|
|
38
|
+
|
|
39
|
+
* Tool environments installed with ``uv tool install`` (uv's pipx
|
|
40
|
+
equivalent), stored under ``~/.local/share/uv/tools``. Marked
|
|
41
|
+
``managed_by="uv"`` so deletion goes through ``uv tool uninstall``
|
|
42
|
+
and the ``~/.local/bin`` shims are cleaned up too.
|
|
43
|
+
* Python versions installed with ``uv python install``, stored under
|
|
44
|
+
``~/.local/share/uv/python``.
|
|
45
|
+
|
|
46
|
+
``UV_TOOL_DIR``, ``UV_PYTHON_INSTALL_DIR`` and ``XDG_DATA_HOME`` are
|
|
47
|
+
honoured. Project virtualenvs created by ``uv venv`` / ``uv sync``
|
|
48
|
+
are regular ``pyvenv.cfg`` environments reported by
|
|
49
|
+
:class:`~killpy.detectors.venv.VenvDetector`, and the global uv cache
|
|
50
|
+
is reported by the cache detector. The scan *path* argument is
|
|
51
|
+
ignored.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
name = "uv"
|
|
55
|
+
|
|
56
|
+
def can_handle(self) -> bool:
|
|
57
|
+
return (
|
|
58
|
+
shutil.which("uv") is not None
|
|
59
|
+
or _uv_tools_dir().exists()
|
|
60
|
+
or _uv_python_dir().exists()
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
def detect(self, path: Path) -> list[Environment]: # noqa: ARG002
|
|
64
|
+
envs: list[Environment] = []
|
|
65
|
+
envs.extend(self._scan_dir(_uv_tools_dir(), managed_by="uv"))
|
|
66
|
+
envs.extend(self._scan_dir(_uv_python_dir(), managed_by=None))
|
|
67
|
+
return envs
|
|
68
|
+
|
|
69
|
+
@staticmethod
|
|
70
|
+
def _scan_dir(root: Path, managed_by: str | None) -> list[Environment]:
|
|
71
|
+
if not root.exists():
|
|
72
|
+
return []
|
|
73
|
+
envs: list[Environment] = []
|
|
74
|
+
try:
|
|
75
|
+
for env_dir in root.iterdir():
|
|
76
|
+
if not env_dir.is_dir():
|
|
77
|
+
continue
|
|
78
|
+
try:
|
|
79
|
+
stat = env_dir.stat()
|
|
80
|
+
size = get_total_size(env_dir)
|
|
81
|
+
mtime = datetime.fromtimestamp(stat.st_mtime, tz=timezone.utc)
|
|
82
|
+
envs.append(
|
|
83
|
+
Environment(
|
|
84
|
+
path=env_dir,
|
|
85
|
+
name=env_dir.name,
|
|
86
|
+
type="uv",
|
|
87
|
+
last_accessed=mtime,
|
|
88
|
+
size_bytes=size,
|
|
89
|
+
managed_by=managed_by,
|
|
90
|
+
)
|
|
91
|
+
)
|
|
92
|
+
except (FileNotFoundError, OSError) as exc:
|
|
93
|
+
logger.debug("Skipping uv entry %s: %s", env_dir, exc)
|
|
94
|
+
except OSError as exc:
|
|
95
|
+
logger.error("Cannot inspect uv dir %s: %s", root, exc)
|
|
96
|
+
return envs
|
|
@@ -9,7 +9,6 @@ from killpy.models import ScoredEnvironment, Suggestion
|
|
|
9
9
|
|
|
10
10
|
# Age thresholds in days.
|
|
11
11
|
_HIGH_AGE_ORPHAN_THRESHOLD = 180 # orphan + age >= this → HIGH
|
|
12
|
-
_HIGH_AGE_NO_PROJECT_THRESHOLD = 365 # no project files + age >= this → HIGH
|
|
13
12
|
_MEDIUM_AGE_THRESHOLD = 120 # age >= this → MEDIUM
|
|
14
13
|
_LOW_AGE_THRESHOLD = 120 # age < this → LOW
|
|
15
14
|
|
|
@@ -73,7 +72,6 @@ class SuggestionEngine:
|
|
|
73
72
|
reasons: list[str],
|
|
74
73
|
) -> tuple[Literal["HIGH", "MEDIUM", "LOW"], str]:
|
|
75
74
|
is_orphan = scored.is_orphan
|
|
76
|
-
has_project_files = scored.has_project_files
|
|
77
75
|
is_active_git = (
|
|
78
76
|
scored.git_info is not None
|
|
79
77
|
and scored.git_info.is_git_repo
|
|
@@ -86,12 +84,6 @@ class SuggestionEngine:
|
|
|
86
84
|
reasons.append(f"Not used in {age_days} days")
|
|
87
85
|
return "HIGH", _ACTION_HIGH
|
|
88
86
|
|
|
89
|
-
# ── Rule 1b: HIGH — no project files and very old ──────────────────────
|
|
90
|
-
if not has_project_files and age_days >= _HIGH_AGE_NO_PROJECT_THRESHOLD:
|
|
91
|
-
reasons.append("Orphan — no project files found nearby")
|
|
92
|
-
reasons.append(f"Not used in {age_days} days")
|
|
93
|
-
return "HIGH", _ACTION_HIGH
|
|
94
|
-
|
|
95
87
|
# ── Rule 2: LOW — active git or very recent ─────────────────────────────
|
|
96
88
|
if is_active_git or age_days < _LOW_AGE_THRESHOLD:
|
|
97
89
|
if is_active_git:
|
|
@@ -39,8 +39,8 @@ class Environment:
|
|
|
39
39
|
Total size in bytes (recursive directory sum).
|
|
40
40
|
managed_by:
|
|
41
41
|
If not ``None``, the external tool that manages deletion. Supported
|
|
42
|
-
values: ``"conda"`` and ``"
|
|
43
|
-
performed via :func:`shutil.rmtree`.
|
|
42
|
+
values: ``"conda"``, ``"pipx"`` and ``"uv"`` (uv tool environments).
|
|
43
|
+
When ``None`` deletion is performed via :func:`shutil.rmtree`.
|
|
44
44
|
"""
|
|
45
45
|
|
|
46
46
|
path: Path
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: killpy
|
|
3
|
-
Version: 0.24.
|
|
3
|
+
Version: 0.24.2
|
|
4
4
|
Summary: List all .venv directories and Conda environments 🐍 on your system and check how much space they are using. You can then choose which ones to delete in order to free up space 🧹.
|
|
5
5
|
Project-URL: Homepage, https://github.com/Tlaloc-Es/killpy
|
|
6
6
|
Project-URL: Documentation, https://tlaloc-es.github.io/killpy/
|
|
@@ -587,7 +587,7 @@ Typical minimal setup (safe for daily use):
|
|
|
587
587
|
|
|
588
588
|
```yaml
|
|
589
589
|
- repo: https://github.com/Tlaloc-Es/KillPy
|
|
590
|
-
rev: 0.24.
|
|
590
|
+
rev: 0.24.2
|
|
591
591
|
hooks:
|
|
592
592
|
- id: killpy
|
|
593
593
|
```
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "killpy"
|
|
3
|
-
version = "0.24.
|
|
3
|
+
version = "0.24.2"
|
|
4
4
|
description = "List all .venv directories and Conda environments 🐍 on your system and check how much space they are using. You can then choose which ones to delete in order to free up space 🧹."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
classifiers = [
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
"""Detector for uv-managed virtual environments."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import logging
|
|
6
|
-
import os
|
|
7
|
-
import shutil
|
|
8
|
-
from datetime import datetime, timezone
|
|
9
|
-
from pathlib import Path
|
|
10
|
-
|
|
11
|
-
from killpy.detectors.base import AbstractDetector
|
|
12
|
-
from killpy.files import get_total_size
|
|
13
|
-
from killpy.models import Environment
|
|
14
|
-
|
|
15
|
-
logger = logging.getLogger(__name__)
|
|
16
|
-
|
|
17
|
-
_PRUNED: frozenset[str] = frozenset({".git", ".hg", ".svn", "node_modules"})
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
class UvDetector(AbstractDetector):
|
|
21
|
-
"""Detects ``.uv`` directories created by `uv venv` inside projects.
|
|
22
|
-
|
|
23
|
-
Also reports the global uv cache at ``~/.cache/uv`` – but only when
|
|
24
|
-
the cache detector is *not* included in the same scan (the scanner
|
|
25
|
-
handles deduplication by path).
|
|
26
|
-
"""
|
|
27
|
-
|
|
28
|
-
name = "uv"
|
|
29
|
-
|
|
30
|
-
def can_handle(self) -> bool:
|
|
31
|
-
return shutil.which("uv") is not None
|
|
32
|
-
|
|
33
|
-
def detect(self, path: Path) -> list[Environment]:
|
|
34
|
-
envs: list[Environment] = []
|
|
35
|
-
for current_root, directories, _ in os.walk(path, topdown=True):
|
|
36
|
-
pruned = set()
|
|
37
|
-
for d in directories:
|
|
38
|
-
if d in _PRUNED:
|
|
39
|
-
pruned.add(d)
|
|
40
|
-
continue
|
|
41
|
-
if d == ".uv":
|
|
42
|
-
uv_path = Path(current_root) / d
|
|
43
|
-
try:
|
|
44
|
-
stat = uv_path.stat()
|
|
45
|
-
size = get_total_size(uv_path)
|
|
46
|
-
mtime = datetime.fromtimestamp(
|
|
47
|
-
stat.st_mtime,
|
|
48
|
-
tz=timezone.utc,
|
|
49
|
-
)
|
|
50
|
-
envs.append(
|
|
51
|
-
Environment(
|
|
52
|
-
path=uv_path,
|
|
53
|
-
name=str(uv_path),
|
|
54
|
-
type="uv",
|
|
55
|
-
last_accessed=mtime,
|
|
56
|
-
size_bytes=size,
|
|
57
|
-
)
|
|
58
|
-
)
|
|
59
|
-
except (FileNotFoundError, OSError) as exc:
|
|
60
|
-
logger.debug("Skipping %s: %s", uv_path, exc)
|
|
61
|
-
pruned.add(d)
|
|
62
|
-
directories[:] = [d for d in directories if d not in pruned]
|
|
63
|
-
|
|
64
|
-
return envs
|
|
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
|