killpy 0.22.0__tar.gz → 0.24.0__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.22.0 → killpy-0.24.0}/PKG-INFO +37 -3
- {killpy-0.22.0 → killpy-0.24.0}/README.md +35 -2
- {killpy-0.22.0 → killpy-0.24.0}/killpy/__main__.py +2 -0
- killpy-0.24.0/killpy/commands/find.py +197 -0
- {killpy-0.22.0 → killpy-0.24.0}/killpy/commands/list.py +76 -30
- {killpy-0.22.0 → killpy-0.24.0}/killpy.egg-info/PKG-INFO +37 -3
- {killpy-0.22.0 → killpy-0.24.0}/killpy.egg-info/SOURCES.txt +1 -0
- {killpy-0.22.0 → killpy-0.24.0}/killpy.egg-info/requires.txt +1 -0
- {killpy-0.22.0 → killpy-0.24.0}/pyproject.toml +7 -2
- {killpy-0.22.0 → killpy-0.24.0}/killpy/__init__.py +0 -0
- {killpy-0.22.0 → killpy-0.24.0}/killpy/cleaner.py +0 -0
- {killpy-0.22.0 → killpy-0.24.0}/killpy/cleaners/__init__.py +0 -0
- {killpy-0.22.0 → killpy-0.24.0}/killpy/cli.py +0 -0
- {killpy-0.22.0 → killpy-0.24.0}/killpy/commands/__init__.py +0 -0
- {killpy-0.22.0 → killpy-0.24.0}/killpy/commands/_utils.py +0 -0
- {killpy-0.22.0 → killpy-0.24.0}/killpy/commands/clean.py +0 -0
- {killpy-0.22.0 → killpy-0.24.0}/killpy/commands/delete.py +0 -0
- {killpy-0.22.0 → killpy-0.24.0}/killpy/commands/doctor.py +0 -0
- {killpy-0.22.0 → killpy-0.24.0}/killpy/commands/stats.py +0 -0
- {killpy-0.22.0 → killpy-0.24.0}/killpy/detectors/__init__.py +0 -0
- {killpy-0.22.0 → killpy-0.24.0}/killpy/detectors/artifacts.py +0 -0
- {killpy-0.22.0 → killpy-0.24.0}/killpy/detectors/base.py +0 -0
- {killpy-0.22.0 → killpy-0.24.0}/killpy/detectors/cache.py +0 -0
- {killpy-0.22.0 → killpy-0.24.0}/killpy/detectors/conda.py +0 -0
- {killpy-0.22.0 → killpy-0.24.0}/killpy/detectors/hatch.py +0 -0
- {killpy-0.22.0 → killpy-0.24.0}/killpy/detectors/pipenv.py +0 -0
- {killpy-0.22.0 → killpy-0.24.0}/killpy/detectors/pipx.py +0 -0
- {killpy-0.22.0 → killpy-0.24.0}/killpy/detectors/poetry.py +0 -0
- {killpy-0.22.0 → killpy-0.24.0}/killpy/detectors/pyenv.py +0 -0
- {killpy-0.22.0 → killpy-0.24.0}/killpy/detectors/tox.py +0 -0
- {killpy-0.22.0 → killpy-0.24.0}/killpy/detectors/uv.py +0 -0
- {killpy-0.22.0 → killpy-0.24.0}/killpy/detectors/venv.py +0 -0
- {killpy-0.22.0 → killpy-0.24.0}/killpy/files/__init__.py +0 -0
- {killpy-0.22.0 → killpy-0.24.0}/killpy/intelligence/__init__.py +0 -0
- {killpy-0.22.0 → killpy-0.24.0}/killpy/intelligence/git_analyzer.py +0 -0
- {killpy-0.22.0 → killpy-0.24.0}/killpy/intelligence/scoring.py +0 -0
- {killpy-0.22.0 → killpy-0.24.0}/killpy/intelligence/suggestions.py +0 -0
- {killpy-0.22.0 → killpy-0.24.0}/killpy/intelligence/tracker.py +0 -0
- {killpy-0.22.0 → killpy-0.24.0}/killpy/models.py +0 -0
- {killpy-0.22.0 → killpy-0.24.0}/killpy/scanner.py +0 -0
- {killpy-0.22.0 → killpy-0.24.0}/killpy.egg-info/dependency_links.txt +0 -0
- {killpy-0.22.0 → killpy-0.24.0}/killpy.egg-info/entry_points.txt +0 -0
- {killpy-0.22.0 → killpy-0.24.0}/killpy.egg-info/top_level.txt +0 -0
- {killpy-0.22.0 → killpy-0.24.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: killpy
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.24.0
|
|
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/
|
|
@@ -15,6 +15,7 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
15
15
|
Requires-Python: >=3.10
|
|
16
16
|
Description-Content-Type: text/markdown
|
|
17
17
|
Requires-Dist: click>=8.1.8
|
|
18
|
+
Requires-Dist: packaging>=24.0
|
|
18
19
|
Requires-Dist: rich>=13.9.4
|
|
19
20
|
Requires-Dist: textual>=1.0.0
|
|
20
21
|
|
|
@@ -46,6 +47,31 @@ uvx killpy --path ~
|
|
|
46
47
|
|
|
47
48
|
______________________________________________________________________
|
|
48
49
|
|
|
50
|
+
## Table of Contents
|
|
51
|
+
|
|
52
|
+
- [The Problem](#the-problem)
|
|
53
|
+
- [What killpy detects](#what-killpy-detects)
|
|
54
|
+
- [Quickstart](#quickstart)
|
|
55
|
+
- [Interactive TUI](#interactive-tui)
|
|
56
|
+
- [Keyboard shortcuts](#keyboard-shortcuts)
|
|
57
|
+
- [Search / filter](#search--filter)
|
|
58
|
+
- [Multi-select mode](#multi-select-mode)
|
|
59
|
+
- [CLI reference](#cli-reference)
|
|
60
|
+
- [`killpy` — launch TUI or headless delete](#killpy--launch-tui-or-headless-delete)
|
|
61
|
+
- [`killpy list` — inspect environments](#killpy-list--inspect-environments)
|
|
62
|
+
- [`killpy delete` — remove environments](#killpy-delete--remove-environments)
|
|
63
|
+
- [`killpy stats` — disk usage summary](#killpy-stats--disk-usage-summary)
|
|
64
|
+
- [`killpy clean` — remove cache directories](#killpy-clean--remove-cache-directories)
|
|
65
|
+
- [`killpy doctor` — smart health report](#killpy-doctor--smart-health-report)
|
|
66
|
+
- [killpy vs alternatives](#killpy-vs-alternatives)
|
|
67
|
+
- [FAQ](#faq)
|
|
68
|
+
- [Pre-commit hooks](#pre-commit-hooks)
|
|
69
|
+
- [Safety](#safety)
|
|
70
|
+
- [Roadmap](#roadmap)
|
|
71
|
+
- [Contributing](#contributing)
|
|
72
|
+
|
|
73
|
+
______________________________________________________________________
|
|
74
|
+
|
|
49
75
|
## The Problem
|
|
50
76
|
|
|
51
77
|
If you have been writing Python for more than a year, your disk is probably full of environments you forgot about.
|
|
@@ -226,8 +252,11 @@ killpy list --type venv --type conda # filter by type (repeatable)
|
|
|
226
252
|
killpy list --older-than 90 # not accessed in the last 90 days
|
|
227
253
|
killpy list --json # output as a JSON array
|
|
228
254
|
killpy list --json-stream # stream as NDJSON — one line per env
|
|
255
|
+
killpy list --quiet # suppress progress output (scripts/CI)
|
|
229
256
|
```
|
|
230
257
|
|
|
258
|
+
While scanning, `killpy list` shows a live progress indicator on **stderr** so you can see which detector is running. Stdout receives only the final output (table, JSON, or NDJSON), so pipes and redirections are never polluted. Use `--quiet` / `-q` to silence the progress indicator entirely.
|
|
259
|
+
|
|
231
260
|
`--json` example output:
|
|
232
261
|
|
|
233
262
|
```json
|
|
@@ -429,9 +458,12 @@ Deletes every `.venv` / `pyvenv.cfg` env not accessed in the last 90 days, witho
|
|
|
429
458
|
**How do I use killpy in a CI pipeline or script?**
|
|
430
459
|
|
|
431
460
|
```bash
|
|
432
|
-
# List as machine-readable JSON
|
|
461
|
+
# List as machine-readable JSON (progress goes to stderr, JSON to stdout)
|
|
433
462
|
killpy list --json
|
|
434
463
|
|
|
464
|
+
# Suppress progress output entirely with --quiet / -q
|
|
465
|
+
killpy list --json --quiet | jq '.[] | .size_human'
|
|
466
|
+
|
|
435
467
|
# Stream results as NDJSON in real time
|
|
436
468
|
killpy list --json-stream | jq '.size_bytes'
|
|
437
469
|
|
|
@@ -439,6 +471,8 @@ killpy list --json-stream | jq '.size_bytes'
|
|
|
439
471
|
killpy --path ./build_artifacts --delete-all --yes
|
|
440
472
|
```
|
|
441
473
|
|
|
474
|
+
Progress messages always go to **stderr**, so stdout is clean for piping even without `--quiet`. Use `-q` when you want no output at all on stderr.
|
|
475
|
+
|
|
442
476
|
**How do I skip certain directories?**
|
|
443
477
|
|
|
444
478
|
```bash
|
|
@@ -550,7 +584,7 @@ Typical minimal setup (safe for daily use):
|
|
|
550
584
|
|
|
551
585
|
```yaml
|
|
552
586
|
- repo: https://github.com/Tlaloc-Es/KillPy
|
|
553
|
-
rev: 0.
|
|
587
|
+
rev: 0.24.0
|
|
554
588
|
hooks:
|
|
555
589
|
- id: killpy
|
|
556
590
|
```
|
|
@@ -26,6 +26,31 @@ uvx killpy --path ~
|
|
|
26
26
|
|
|
27
27
|
______________________________________________________________________
|
|
28
28
|
|
|
29
|
+
## Table of Contents
|
|
30
|
+
|
|
31
|
+
- [The Problem](#the-problem)
|
|
32
|
+
- [What killpy detects](#what-killpy-detects)
|
|
33
|
+
- [Quickstart](#quickstart)
|
|
34
|
+
- [Interactive TUI](#interactive-tui)
|
|
35
|
+
- [Keyboard shortcuts](#keyboard-shortcuts)
|
|
36
|
+
- [Search / filter](#search--filter)
|
|
37
|
+
- [Multi-select mode](#multi-select-mode)
|
|
38
|
+
- [CLI reference](#cli-reference)
|
|
39
|
+
- [`killpy` — launch TUI or headless delete](#killpy--launch-tui-or-headless-delete)
|
|
40
|
+
- [`killpy list` — inspect environments](#killpy-list--inspect-environments)
|
|
41
|
+
- [`killpy delete` — remove environments](#killpy-delete--remove-environments)
|
|
42
|
+
- [`killpy stats` — disk usage summary](#killpy-stats--disk-usage-summary)
|
|
43
|
+
- [`killpy clean` — remove cache directories](#killpy-clean--remove-cache-directories)
|
|
44
|
+
- [`killpy doctor` — smart health report](#killpy-doctor--smart-health-report)
|
|
45
|
+
- [killpy vs alternatives](#killpy-vs-alternatives)
|
|
46
|
+
- [FAQ](#faq)
|
|
47
|
+
- [Pre-commit hooks](#pre-commit-hooks)
|
|
48
|
+
- [Safety](#safety)
|
|
49
|
+
- [Roadmap](#roadmap)
|
|
50
|
+
- [Contributing](#contributing)
|
|
51
|
+
|
|
52
|
+
______________________________________________________________________
|
|
53
|
+
|
|
29
54
|
## The Problem
|
|
30
55
|
|
|
31
56
|
If you have been writing Python for more than a year, your disk is probably full of environments you forgot about.
|
|
@@ -206,8 +231,11 @@ killpy list --type venv --type conda # filter by type (repeatable)
|
|
|
206
231
|
killpy list --older-than 90 # not accessed in the last 90 days
|
|
207
232
|
killpy list --json # output as a JSON array
|
|
208
233
|
killpy list --json-stream # stream as NDJSON — one line per env
|
|
234
|
+
killpy list --quiet # suppress progress output (scripts/CI)
|
|
209
235
|
```
|
|
210
236
|
|
|
237
|
+
While scanning, `killpy list` shows a live progress indicator on **stderr** so you can see which detector is running. Stdout receives only the final output (table, JSON, or NDJSON), so pipes and redirections are never polluted. Use `--quiet` / `-q` to silence the progress indicator entirely.
|
|
238
|
+
|
|
211
239
|
`--json` example output:
|
|
212
240
|
|
|
213
241
|
```json
|
|
@@ -409,9 +437,12 @@ Deletes every `.venv` / `pyvenv.cfg` env not accessed in the last 90 days, witho
|
|
|
409
437
|
**How do I use killpy in a CI pipeline or script?**
|
|
410
438
|
|
|
411
439
|
```bash
|
|
412
|
-
# List as machine-readable JSON
|
|
440
|
+
# List as machine-readable JSON (progress goes to stderr, JSON to stdout)
|
|
413
441
|
killpy list --json
|
|
414
442
|
|
|
443
|
+
# Suppress progress output entirely with --quiet / -q
|
|
444
|
+
killpy list --json --quiet | jq '.[] | .size_human'
|
|
445
|
+
|
|
415
446
|
# Stream results as NDJSON in real time
|
|
416
447
|
killpy list --json-stream | jq '.size_bytes'
|
|
417
448
|
|
|
@@ -419,6 +450,8 @@ killpy list --json-stream | jq '.size_bytes'
|
|
|
419
450
|
killpy --path ./build_artifacts --delete-all --yes
|
|
420
451
|
```
|
|
421
452
|
|
|
453
|
+
Progress messages always go to **stderr**, so stdout is clean for piping even without `--quiet`. Use `-q` when you want no output at all on stderr.
|
|
454
|
+
|
|
422
455
|
**How do I skip certain directories?**
|
|
423
456
|
|
|
424
457
|
```bash
|
|
@@ -530,7 +563,7 @@ Typical minimal setup (safe for daily use):
|
|
|
530
563
|
|
|
531
564
|
```yaml
|
|
532
565
|
- repo: https://github.com/Tlaloc-Es/KillPy
|
|
533
|
-
rev: 0.
|
|
566
|
+
rev: 0.24.0
|
|
534
567
|
hooks:
|
|
535
568
|
- id: killpy
|
|
536
569
|
```
|
|
@@ -11,6 +11,7 @@ from killpy.cli import TableApp
|
|
|
11
11
|
from killpy.commands.clean import clean
|
|
12
12
|
from killpy.commands.delete import delete_cmd
|
|
13
13
|
from killpy.commands.doctor import doctor_cmd
|
|
14
|
+
from killpy.commands.find import find_cmd
|
|
14
15
|
from killpy.commands.list import list_cmd
|
|
15
16
|
from killpy.commands.stats import stats_cmd
|
|
16
17
|
from killpy.files import format_size
|
|
@@ -120,6 +121,7 @@ cli.add_command(list_cmd, name="list")
|
|
|
120
121
|
cli.add_command(delete_cmd, name="delete")
|
|
121
122
|
cli.add_command(stats_cmd, name="stats")
|
|
122
123
|
cli.add_command(doctor_cmd, name="doctor")
|
|
124
|
+
cli.add_command(find_cmd, name="find")
|
|
123
125
|
|
|
124
126
|
|
|
125
127
|
if __name__ == "__main__":
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""``killpy find`` – locate environments that have a specific package installed."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
from packaging.requirements import Requirement
|
|
10
|
+
from packaging.version import InvalidVersion
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
from rich.table import Table
|
|
13
|
+
|
|
14
|
+
from killpy.commands._utils import filter_envs
|
|
15
|
+
from killpy.scanner import Scanner
|
|
16
|
+
|
|
17
|
+
# ---------------------------------------------------------------------------
|
|
18
|
+
# Helpers: read installed-package metadata from a venv directory
|
|
19
|
+
# ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _site_packages(env_path: Path) -> list[Path]:
|
|
23
|
+
"""Return all site-packages directories found inside *env_path*.
|
|
24
|
+
|
|
25
|
+
Supports the Unix layout (``lib/python3.x/site-packages``) and the
|
|
26
|
+
Windows layout (``Lib/site-packages``).
|
|
27
|
+
"""
|
|
28
|
+
result: list[Path] = []
|
|
29
|
+
|
|
30
|
+
# Unix: lib/python<ver>/site-packages
|
|
31
|
+
lib = env_path / "lib"
|
|
32
|
+
if lib.is_dir():
|
|
33
|
+
for child in lib.iterdir():
|
|
34
|
+
sp = child / "site-packages"
|
|
35
|
+
if sp.is_dir():
|
|
36
|
+
result.append(sp)
|
|
37
|
+
|
|
38
|
+
# Windows: Lib/site-packages
|
|
39
|
+
win_sp = env_path / "Lib" / "site-packages"
|
|
40
|
+
if win_sp.is_dir():
|
|
41
|
+
result.append(win_sp)
|
|
42
|
+
|
|
43
|
+
return result
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _read_metadata_field(metadata_path: Path, field: str) -> str | None:
|
|
47
|
+
"""Return the value of a single RFC 822-style *field* from *metadata_path*."""
|
|
48
|
+
prefix = f"{field}:"
|
|
49
|
+
try:
|
|
50
|
+
with metadata_path.open(encoding="utf-8", errors="replace") as fh:
|
|
51
|
+
for line in fh:
|
|
52
|
+
if line.startswith(prefix):
|
|
53
|
+
return line[len(prefix) :].strip()
|
|
54
|
+
# RFC 822 headers end at the first blank line.
|
|
55
|
+
if not line.strip():
|
|
56
|
+
break
|
|
57
|
+
except OSError:
|
|
58
|
+
pass
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _normalise_name(name: str) -> str:
|
|
63
|
+
"""Normalise a distribution name to a lowercase, underscore form."""
|
|
64
|
+
return name.lower().replace("-", "_").replace(".", "_")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def installed_packages(env_path: Path) -> dict[str, str]:
|
|
68
|
+
"""Return ``{normalised_name: version}`` for every package installed in *env_path*.
|
|
69
|
+
|
|
70
|
+
Reads ``*.dist-info/METADATA`` files from all site-packages directories
|
|
71
|
+
found under the environment root. Non-venv paths (e.g. conda envs created
|
|
72
|
+
with ``--prefix``) are supported as long as they follow the same layout.
|
|
73
|
+
"""
|
|
74
|
+
packages: dict[str, str] = {}
|
|
75
|
+
for sp in _site_packages(env_path):
|
|
76
|
+
for dist_info in sp.glob("*.dist-info"):
|
|
77
|
+
metadata = dist_info / "METADATA"
|
|
78
|
+
if not metadata.exists():
|
|
79
|
+
continue
|
|
80
|
+
name = _read_metadata_field(metadata, "Name")
|
|
81
|
+
version = _read_metadata_field(metadata, "Version")
|
|
82
|
+
if name and version:
|
|
83
|
+
packages[_normalise_name(name)] = version
|
|
84
|
+
return packages
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def package_version_match(
|
|
88
|
+
packages: dict[str, str], requirement: Requirement
|
|
89
|
+
) -> str | None:
|
|
90
|
+
"""Return the installed version string if it satisfies *requirement*, else ``None``.""" # noqa: E501
|
|
91
|
+
norm_name = _normalise_name(requirement.name)
|
|
92
|
+
installed = packages.get(norm_name)
|
|
93
|
+
if installed is None:
|
|
94
|
+
return None
|
|
95
|
+
try:
|
|
96
|
+
if requirement.specifier.contains(installed, prereleases=True):
|
|
97
|
+
return installed
|
|
98
|
+
except InvalidVersion:
|
|
99
|
+
pass
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# ---------------------------------------------------------------------------
|
|
104
|
+
# Click command
|
|
105
|
+
# ---------------------------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@click.command("find")
|
|
109
|
+
@click.argument("package")
|
|
110
|
+
@click.option(
|
|
111
|
+
"--path",
|
|
112
|
+
default=Path.cwd,
|
|
113
|
+
type=click.Path(path_type=Path, exists=True, file_okay=False, dir_okay=True),
|
|
114
|
+
help="Root directory to scan.",
|
|
115
|
+
)
|
|
116
|
+
@click.option(
|
|
117
|
+
"--type",
|
|
118
|
+
"types",
|
|
119
|
+
multiple=True,
|
|
120
|
+
metavar="TYPE",
|
|
121
|
+
help=(
|
|
122
|
+
"Limit the scan to these environment types (repeatable). "
|
|
123
|
+
"Example: --type venv --type conda"
|
|
124
|
+
),
|
|
125
|
+
)
|
|
126
|
+
@click.option(
|
|
127
|
+
"--json",
|
|
128
|
+
"as_json",
|
|
129
|
+
is_flag=True,
|
|
130
|
+
default=False,
|
|
131
|
+
help="Output as a JSON array.",
|
|
132
|
+
)
|
|
133
|
+
def find_cmd(
|
|
134
|
+
package: str,
|
|
135
|
+
path: Path,
|
|
136
|
+
types: tuple[str, ...],
|
|
137
|
+
as_json: bool,
|
|
138
|
+
) -> None:
|
|
139
|
+
"""Find environments that have PACKAGE installed.
|
|
140
|
+
|
|
141
|
+
PACKAGE accepts standard PEP 508 / uv-style version specifiers:
|
|
142
|
+
|
|
143
|
+
\b
|
|
144
|
+
killpy find requests
|
|
145
|
+
killpy find "requests>=2.28"
|
|
146
|
+
killpy find "numpy>=1.24,<2.0"
|
|
147
|
+
killpy find "django==4.2.*"
|
|
148
|
+
killpy find "scipy~=1.11"
|
|
149
|
+
"""
|
|
150
|
+
try:
|
|
151
|
+
req = Requirement(package)
|
|
152
|
+
except Exception as exc: # packaging.requirements.InvalidRequirement
|
|
153
|
+
raise click.BadParameter(str(exc), param_hint="PACKAGE") from exc
|
|
154
|
+
|
|
155
|
+
scanner = Scanner(types=set(types) if types else None)
|
|
156
|
+
envs = scanner.scan(path)
|
|
157
|
+
envs = filter_envs(envs, types or None, None)
|
|
158
|
+
|
|
159
|
+
matches: list[tuple] = [] # (Environment, version_string)
|
|
160
|
+
for env in envs:
|
|
161
|
+
pkgs = installed_packages(env.path)
|
|
162
|
+
version = package_version_match(pkgs, req)
|
|
163
|
+
if version is not None:
|
|
164
|
+
matches.append((env, version))
|
|
165
|
+
|
|
166
|
+
if as_json:
|
|
167
|
+
click.echo(
|
|
168
|
+
json.dumps(
|
|
169
|
+
[{**env.to_dict(), "matched_version": ver} for env, ver in matches],
|
|
170
|
+
indent=2,
|
|
171
|
+
)
|
|
172
|
+
)
|
|
173
|
+
return
|
|
174
|
+
|
|
175
|
+
console = Console()
|
|
176
|
+
|
|
177
|
+
if not matches:
|
|
178
|
+
console.print(
|
|
179
|
+
f"[yellow]No environments found with[/yellow] "
|
|
180
|
+
f"[bold]{package}[/bold] installed."
|
|
181
|
+
)
|
|
182
|
+
return
|
|
183
|
+
|
|
184
|
+
table = Table(title=f"Environments with {package!r}", show_lines=False)
|
|
185
|
+
table.add_column("Path", style="cyan")
|
|
186
|
+
table.add_column("Type", style="magenta")
|
|
187
|
+
table.add_column("Installed version", style="green")
|
|
188
|
+
table.add_column("Env size", style="yellow", justify="right")
|
|
189
|
+
|
|
190
|
+
for env, ver in matches:
|
|
191
|
+
table.add_row(str(env.path), env.type, ver, env.size_human)
|
|
192
|
+
|
|
193
|
+
console.print(table)
|
|
194
|
+
console.print(
|
|
195
|
+
f"\n[bold]{len(matches)}[/bold] environment(s) match "
|
|
196
|
+
f"[bold cyan]{package}[/bold cyan]."
|
|
197
|
+
)
|
|
@@ -14,6 +14,70 @@ from killpy.files import format_size
|
|
|
14
14
|
from killpy.scanner import Scanner
|
|
15
15
|
|
|
16
16
|
|
|
17
|
+
def _run_json_stream(
|
|
18
|
+
scanner: Scanner,
|
|
19
|
+
path: Path,
|
|
20
|
+
types: tuple[str, ...],
|
|
21
|
+
older_than: int | None,
|
|
22
|
+
quiet: bool,
|
|
23
|
+
stderr_console: Console,
|
|
24
|
+
) -> None:
|
|
25
|
+
if not quiet:
|
|
26
|
+
stderr_console.print("[dim]Scanning…[/dim]")
|
|
27
|
+
|
|
28
|
+
def _progress(detector, envs):
|
|
29
|
+
if not quiet:
|
|
30
|
+
stderr_console.print(
|
|
31
|
+
f"[dim] {detector.name}[/dim] — [dim]{len(envs)} found[/dim]",
|
|
32
|
+
)
|
|
33
|
+
for env in filter_envs(envs, types or None, older_than):
|
|
34
|
+
click.echo(json.dumps(env.to_dict()))
|
|
35
|
+
|
|
36
|
+
scanner.scan(path, on_progress=_progress)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _scan_with_progress(
|
|
40
|
+
scanner: Scanner, path: Path, quiet: bool, stderr_console: Console
|
|
41
|
+
):
|
|
42
|
+
if quiet:
|
|
43
|
+
return scanner.scan(path)
|
|
44
|
+
|
|
45
|
+
status = stderr_console.status("Scanning…", spinner="dots")
|
|
46
|
+
status.start()
|
|
47
|
+
|
|
48
|
+
def _progress(detector, _envs):
|
|
49
|
+
status.update(f"Scanning… [dim]{detector.name}[/dim]")
|
|
50
|
+
|
|
51
|
+
envs = scanner.scan(path, on_progress=_progress)
|
|
52
|
+
status.stop()
|
|
53
|
+
return envs
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _print_table(envs: list, console: Console) -> None:
|
|
57
|
+
table = Table(show_header=True, header_style="bold cyan")
|
|
58
|
+
table.add_column("Type", style="dim", min_width=10)
|
|
59
|
+
table.add_column("Name", min_width=20)
|
|
60
|
+
table.add_column("Last accessed", min_width=12)
|
|
61
|
+
table.add_column("Size", justify="right", min_width=9)
|
|
62
|
+
table.add_column("Path")
|
|
63
|
+
|
|
64
|
+
for env in envs:
|
|
65
|
+
table.add_row(
|
|
66
|
+
env.type,
|
|
67
|
+
env.name,
|
|
68
|
+
env.last_accessed_str,
|
|
69
|
+
env.size_human,
|
|
70
|
+
str(env.path),
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
console.print(table)
|
|
74
|
+
total = sum(e.size_bytes for e in envs)
|
|
75
|
+
console.print(
|
|
76
|
+
f"\n[bold]{len(envs)}[/bold] environment(s) — "
|
|
77
|
+
f"[bold]{format_size(total)}[/bold] total"
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
17
81
|
@click.command("list")
|
|
18
82
|
@click.option(
|
|
19
83
|
"--path",
|
|
@@ -52,26 +116,30 @@ from killpy.scanner import Scanner
|
|
|
52
116
|
default=False,
|
|
53
117
|
help="Output as JSON array.",
|
|
54
118
|
)
|
|
119
|
+
@click.option(
|
|
120
|
+
"--quiet",
|
|
121
|
+
"-q",
|
|
122
|
+
is_flag=True,
|
|
123
|
+
default=False,
|
|
124
|
+
help="Suppress progress messages (useful in scripts/pipelines).",
|
|
125
|
+
)
|
|
55
126
|
def list_cmd(
|
|
56
127
|
path: Path,
|
|
57
128
|
types: tuple[str, ...],
|
|
58
129
|
older_than: int | None,
|
|
59
130
|
as_json: bool,
|
|
60
131
|
as_json_stream: bool,
|
|
132
|
+
quiet: bool,
|
|
61
133
|
) -> None:
|
|
62
134
|
"""List all detected Python environments under PATH."""
|
|
63
135
|
scanner = Scanner(types=set(types) if types else None)
|
|
136
|
+
stderr_console = Console(stderr=True)
|
|
64
137
|
|
|
65
138
|
if as_json_stream:
|
|
66
|
-
|
|
67
|
-
def _stream_progress(_detector, envs):
|
|
68
|
-
for env in filter_envs(envs, types or None, older_than):
|
|
69
|
-
click.echo(json.dumps(env.to_dict()))
|
|
70
|
-
|
|
71
|
-
scanner.scan(path, on_progress=_stream_progress)
|
|
139
|
+
_run_json_stream(scanner, path, types, older_than, quiet, stderr_console)
|
|
72
140
|
return
|
|
73
141
|
|
|
74
|
-
envs = scanner
|
|
142
|
+
envs = _scan_with_progress(scanner, path, quiet, stderr_console)
|
|
75
143
|
envs = filter_envs(envs, types or None, older_than)
|
|
76
144
|
|
|
77
145
|
if as_json:
|
|
@@ -82,26 +150,4 @@ def list_cmd(
|
|
|
82
150
|
click.echo("No environments found.")
|
|
83
151
|
return
|
|
84
152
|
|
|
85
|
-
|
|
86
|
-
table = Table(show_header=True, header_style="bold cyan")
|
|
87
|
-
table.add_column("Type", style="dim", min_width=10)
|
|
88
|
-
table.add_column("Name", min_width=20)
|
|
89
|
-
table.add_column("Last accessed", min_width=12)
|
|
90
|
-
table.add_column("Size", justify="right", min_width=9)
|
|
91
|
-
table.add_column("Path")
|
|
92
|
-
|
|
93
|
-
for env in envs:
|
|
94
|
-
table.add_row(
|
|
95
|
-
env.type,
|
|
96
|
-
env.name,
|
|
97
|
-
env.last_accessed_str,
|
|
98
|
-
env.size_human,
|
|
99
|
-
str(env.path),
|
|
100
|
-
)
|
|
101
|
-
|
|
102
|
-
console.print(table)
|
|
103
|
-
total = sum(e.size_bytes for e in envs)
|
|
104
|
-
console.print(
|
|
105
|
-
f"\n[bold]{len(envs)}[/bold] environment(s) — "
|
|
106
|
-
f"[bold]{format_size(total)}[/bold] total"
|
|
107
|
-
)
|
|
153
|
+
_print_table(envs, Console())
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: killpy
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.24.0
|
|
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/
|
|
@@ -15,6 +15,7 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
15
15
|
Requires-Python: >=3.10
|
|
16
16
|
Description-Content-Type: text/markdown
|
|
17
17
|
Requires-Dist: click>=8.1.8
|
|
18
|
+
Requires-Dist: packaging>=24.0
|
|
18
19
|
Requires-Dist: rich>=13.9.4
|
|
19
20
|
Requires-Dist: textual>=1.0.0
|
|
20
21
|
|
|
@@ -46,6 +47,31 @@ uvx killpy --path ~
|
|
|
46
47
|
|
|
47
48
|
______________________________________________________________________
|
|
48
49
|
|
|
50
|
+
## Table of Contents
|
|
51
|
+
|
|
52
|
+
- [The Problem](#the-problem)
|
|
53
|
+
- [What killpy detects](#what-killpy-detects)
|
|
54
|
+
- [Quickstart](#quickstart)
|
|
55
|
+
- [Interactive TUI](#interactive-tui)
|
|
56
|
+
- [Keyboard shortcuts](#keyboard-shortcuts)
|
|
57
|
+
- [Search / filter](#search--filter)
|
|
58
|
+
- [Multi-select mode](#multi-select-mode)
|
|
59
|
+
- [CLI reference](#cli-reference)
|
|
60
|
+
- [`killpy` — launch TUI or headless delete](#killpy--launch-tui-or-headless-delete)
|
|
61
|
+
- [`killpy list` — inspect environments](#killpy-list--inspect-environments)
|
|
62
|
+
- [`killpy delete` — remove environments](#killpy-delete--remove-environments)
|
|
63
|
+
- [`killpy stats` — disk usage summary](#killpy-stats--disk-usage-summary)
|
|
64
|
+
- [`killpy clean` — remove cache directories](#killpy-clean--remove-cache-directories)
|
|
65
|
+
- [`killpy doctor` — smart health report](#killpy-doctor--smart-health-report)
|
|
66
|
+
- [killpy vs alternatives](#killpy-vs-alternatives)
|
|
67
|
+
- [FAQ](#faq)
|
|
68
|
+
- [Pre-commit hooks](#pre-commit-hooks)
|
|
69
|
+
- [Safety](#safety)
|
|
70
|
+
- [Roadmap](#roadmap)
|
|
71
|
+
- [Contributing](#contributing)
|
|
72
|
+
|
|
73
|
+
______________________________________________________________________
|
|
74
|
+
|
|
49
75
|
## The Problem
|
|
50
76
|
|
|
51
77
|
If you have been writing Python for more than a year, your disk is probably full of environments you forgot about.
|
|
@@ -226,8 +252,11 @@ killpy list --type venv --type conda # filter by type (repeatable)
|
|
|
226
252
|
killpy list --older-than 90 # not accessed in the last 90 days
|
|
227
253
|
killpy list --json # output as a JSON array
|
|
228
254
|
killpy list --json-stream # stream as NDJSON — one line per env
|
|
255
|
+
killpy list --quiet # suppress progress output (scripts/CI)
|
|
229
256
|
```
|
|
230
257
|
|
|
258
|
+
While scanning, `killpy list` shows a live progress indicator on **stderr** so you can see which detector is running. Stdout receives only the final output (table, JSON, or NDJSON), so pipes and redirections are never polluted. Use `--quiet` / `-q` to silence the progress indicator entirely.
|
|
259
|
+
|
|
231
260
|
`--json` example output:
|
|
232
261
|
|
|
233
262
|
```json
|
|
@@ -429,9 +458,12 @@ Deletes every `.venv` / `pyvenv.cfg` env not accessed in the last 90 days, witho
|
|
|
429
458
|
**How do I use killpy in a CI pipeline or script?**
|
|
430
459
|
|
|
431
460
|
```bash
|
|
432
|
-
# List as machine-readable JSON
|
|
461
|
+
# List as machine-readable JSON (progress goes to stderr, JSON to stdout)
|
|
433
462
|
killpy list --json
|
|
434
463
|
|
|
464
|
+
# Suppress progress output entirely with --quiet / -q
|
|
465
|
+
killpy list --json --quiet | jq '.[] | .size_human'
|
|
466
|
+
|
|
435
467
|
# Stream results as NDJSON in real time
|
|
436
468
|
killpy list --json-stream | jq '.size_bytes'
|
|
437
469
|
|
|
@@ -439,6 +471,8 @@ killpy list --json-stream | jq '.size_bytes'
|
|
|
439
471
|
killpy --path ./build_artifacts --delete-all --yes
|
|
440
472
|
```
|
|
441
473
|
|
|
474
|
+
Progress messages always go to **stderr**, so stdout is clean for piping even without `--quiet`. Use `-q` when you want no output at all on stderr.
|
|
475
|
+
|
|
442
476
|
**How do I skip certain directories?**
|
|
443
477
|
|
|
444
478
|
```bash
|
|
@@ -550,7 +584,7 @@ Typical minimal setup (safe for daily use):
|
|
|
550
584
|
|
|
551
585
|
```yaml
|
|
552
586
|
- repo: https://github.com/Tlaloc-Es/KillPy
|
|
553
|
-
rev: 0.
|
|
587
|
+
rev: 0.24.0
|
|
554
588
|
hooks:
|
|
555
589
|
- id: killpy
|
|
556
590
|
```
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "killpy"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.24.0"
|
|
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 = [
|
|
@@ -11,7 +11,12 @@ classifiers = [
|
|
|
11
11
|
"Programming Language :: Python :: 3.12",
|
|
12
12
|
"Programming Language :: Python :: 3.13",
|
|
13
13
|
]
|
|
14
|
-
dependencies = [
|
|
14
|
+
dependencies = [
|
|
15
|
+
"click>=8.1.8",
|
|
16
|
+
"packaging>=24.0",
|
|
17
|
+
"rich>=13.9.4",
|
|
18
|
+
"textual>=1.0.0",
|
|
19
|
+
]
|
|
15
20
|
requires-python = ">=3.10"
|
|
16
21
|
|
|
17
22
|
[project.urls]
|
|
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
|
|
File without changes
|
|
File without changes
|