killpy 0.23.0__tar.gz → 0.24.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-0.23.0 → killpy-0.24.1}/PKG-INFO +39 -3
- {killpy-0.23.0 → killpy-0.24.1}/README.md +38 -2
- {killpy-0.23.0 → killpy-0.24.1}/killpy/__main__.py +10 -2
- {killpy-0.23.0 → killpy-0.24.1}/killpy/cli.py +52 -22
- {killpy-0.23.0 → killpy-0.24.1}/killpy/commands/delete.py +10 -2
- {killpy-0.23.0 → killpy-0.24.1}/killpy/commands/list.py +76 -30
- {killpy-0.23.0 → killpy-0.24.1}/killpy/intelligence/tracker.py +29 -1
- {killpy-0.23.0 → killpy-0.24.1}/killpy.egg-info/PKG-INFO +39 -3
- {killpy-0.23.0 → killpy-0.24.1}/pyproject.toml +1 -1
- {killpy-0.23.0 → killpy-0.24.1}/killpy/__init__.py +0 -0
- {killpy-0.23.0 → killpy-0.24.1}/killpy/cleaner.py +0 -0
- {killpy-0.23.0 → killpy-0.24.1}/killpy/cleaners/__init__.py +0 -0
- {killpy-0.23.0 → killpy-0.24.1}/killpy/commands/__init__.py +0 -0
- {killpy-0.23.0 → killpy-0.24.1}/killpy/commands/_utils.py +0 -0
- {killpy-0.23.0 → killpy-0.24.1}/killpy/commands/clean.py +0 -0
- {killpy-0.23.0 → killpy-0.24.1}/killpy/commands/doctor.py +0 -0
- {killpy-0.23.0 → killpy-0.24.1}/killpy/commands/find.py +0 -0
- {killpy-0.23.0 → killpy-0.24.1}/killpy/commands/stats.py +0 -0
- {killpy-0.23.0 → killpy-0.24.1}/killpy/detectors/__init__.py +0 -0
- {killpy-0.23.0 → killpy-0.24.1}/killpy/detectors/artifacts.py +0 -0
- {killpy-0.23.0 → killpy-0.24.1}/killpy/detectors/base.py +0 -0
- {killpy-0.23.0 → killpy-0.24.1}/killpy/detectors/cache.py +0 -0
- {killpy-0.23.0 → killpy-0.24.1}/killpy/detectors/conda.py +0 -0
- {killpy-0.23.0 → killpy-0.24.1}/killpy/detectors/hatch.py +0 -0
- {killpy-0.23.0 → killpy-0.24.1}/killpy/detectors/pipenv.py +0 -0
- {killpy-0.23.0 → killpy-0.24.1}/killpy/detectors/pipx.py +0 -0
- {killpy-0.23.0 → killpy-0.24.1}/killpy/detectors/poetry.py +0 -0
- {killpy-0.23.0 → killpy-0.24.1}/killpy/detectors/pyenv.py +0 -0
- {killpy-0.23.0 → killpy-0.24.1}/killpy/detectors/tox.py +0 -0
- {killpy-0.23.0 → killpy-0.24.1}/killpy/detectors/uv.py +0 -0
- {killpy-0.23.0 → killpy-0.24.1}/killpy/detectors/venv.py +0 -0
- {killpy-0.23.0 → killpy-0.24.1}/killpy/files/__init__.py +0 -0
- {killpy-0.23.0 → killpy-0.24.1}/killpy/intelligence/__init__.py +0 -0
- {killpy-0.23.0 → killpy-0.24.1}/killpy/intelligence/git_analyzer.py +0 -0
- {killpy-0.23.0 → killpy-0.24.1}/killpy/intelligence/scoring.py +0 -0
- {killpy-0.23.0 → killpy-0.24.1}/killpy/intelligence/suggestions.py +0 -0
- {killpy-0.23.0 → killpy-0.24.1}/killpy/models.py +0 -0
- {killpy-0.23.0 → killpy-0.24.1}/killpy/scanner.py +0 -0
- {killpy-0.23.0 → killpy-0.24.1}/killpy.egg-info/SOURCES.txt +0 -0
- {killpy-0.23.0 → killpy-0.24.1}/killpy.egg-info/dependency_links.txt +0 -0
- {killpy-0.23.0 → killpy-0.24.1}/killpy.egg-info/entry_points.txt +0 -0
- {killpy-0.23.0 → killpy-0.24.1}/killpy.egg-info/requires.txt +0 -0
- {killpy-0.23.0 → killpy-0.24.1}/killpy.egg-info/top_level.txt +0 -0
- {killpy-0.23.0 → killpy-0.24.1}/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.1
|
|
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/
|
|
@@ -31,6 +31,8 @@ Find and delete old `.venv`, conda, poetry, pipenv, uv and more — safely, in s
|
|
|
31
31
|
uvx killpy --path ~
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
+
> ⭐ Featured in [awesome-python](https://github.com/vinta/awesome-python) — a curated list of the best Python tools
|
|
35
|
+
|
|
34
36
|
[Documentation](https://tlaloc-es.github.io/killpy/)
|
|
35
37
|
|
|
36
38
|
[](https://pypi.org/project/killpy/)
|
|
@@ -40,6 +42,7 @@ uvx killpy --path ~
|
|
|
40
42
|
[](https://codecov.io/gh/Tlaloc-Es/killpy)
|
|
41
43
|
[](LICENSE)
|
|
42
44
|
[](<https://twitter.com/intent/tweet?text=%F0%9F%90%8D%20KillPy%20helps%20you%20reclaim%20disk%20space%20by%20detecting%20unused%20Python%20environments%20(.venv,%20poetry%20env,%20conda%20env)%20and%20pipx%20packages.%20Clean,%20organize%20and%20free%20up%20space%20effortlessly!%20%F0%9F%9A%80&url=https://github.com/Tlaloc-Es/KillPy>)
|
|
45
|
+
[](https://github.com/vinta/awesome-python)
|
|
43
46
|
|
|
44
47
|

|
|
45
48
|
|
|
@@ -47,6 +50,31 @@ uvx killpy --path ~
|
|
|
47
50
|
|
|
48
51
|
______________________________________________________________________
|
|
49
52
|
|
|
53
|
+
## Table of Contents
|
|
54
|
+
|
|
55
|
+
- [The Problem](#the-problem)
|
|
56
|
+
- [What killpy detects](#what-killpy-detects)
|
|
57
|
+
- [Quickstart](#quickstart)
|
|
58
|
+
- [Interactive TUI](#interactive-tui)
|
|
59
|
+
- [Keyboard shortcuts](#keyboard-shortcuts)
|
|
60
|
+
- [Search / filter](#search--filter)
|
|
61
|
+
- [Multi-select mode](#multi-select-mode)
|
|
62
|
+
- [CLI reference](#cli-reference)
|
|
63
|
+
- [`killpy` — launch TUI or headless delete](#killpy--launch-tui-or-headless-delete)
|
|
64
|
+
- [`killpy list` — inspect environments](#killpy-list--inspect-environments)
|
|
65
|
+
- [`killpy delete` — remove environments](#killpy-delete--remove-environments)
|
|
66
|
+
- [`killpy stats` — disk usage summary](#killpy-stats--disk-usage-summary)
|
|
67
|
+
- [`killpy clean` — remove cache directories](#killpy-clean--remove-cache-directories)
|
|
68
|
+
- [`killpy doctor` — smart health report](#killpy-doctor--smart-health-report)
|
|
69
|
+
- [killpy vs alternatives](#killpy-vs-alternatives)
|
|
70
|
+
- [FAQ](#faq)
|
|
71
|
+
- [Pre-commit hooks](#pre-commit-hooks)
|
|
72
|
+
- [Safety](#safety)
|
|
73
|
+
- [Roadmap](#roadmap)
|
|
74
|
+
- [Contributing](#contributing)
|
|
75
|
+
|
|
76
|
+
______________________________________________________________________
|
|
77
|
+
|
|
50
78
|
## The Problem
|
|
51
79
|
|
|
52
80
|
If you have been writing Python for more than a year, your disk is probably full of environments you forgot about.
|
|
@@ -227,8 +255,11 @@ killpy list --type venv --type conda # filter by type (repeatable)
|
|
|
227
255
|
killpy list --older-than 90 # not accessed in the last 90 days
|
|
228
256
|
killpy list --json # output as a JSON array
|
|
229
257
|
killpy list --json-stream # stream as NDJSON — one line per env
|
|
258
|
+
killpy list --quiet # suppress progress output (scripts/CI)
|
|
230
259
|
```
|
|
231
260
|
|
|
261
|
+
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.
|
|
262
|
+
|
|
232
263
|
`--json` example output:
|
|
233
264
|
|
|
234
265
|
```json
|
|
@@ -430,9 +461,12 @@ Deletes every `.venv` / `pyvenv.cfg` env not accessed in the last 90 days, witho
|
|
|
430
461
|
**How do I use killpy in a CI pipeline or script?**
|
|
431
462
|
|
|
432
463
|
```bash
|
|
433
|
-
# List as machine-readable JSON
|
|
464
|
+
# List as machine-readable JSON (progress goes to stderr, JSON to stdout)
|
|
434
465
|
killpy list --json
|
|
435
466
|
|
|
467
|
+
# Suppress progress output entirely with --quiet / -q
|
|
468
|
+
killpy list --json --quiet | jq '.[] | .size_human'
|
|
469
|
+
|
|
436
470
|
# Stream results as NDJSON in real time
|
|
437
471
|
killpy list --json-stream | jq '.size_bytes'
|
|
438
472
|
|
|
@@ -440,6 +474,8 @@ killpy list --json-stream | jq '.size_bytes'
|
|
|
440
474
|
killpy --path ./build_artifacts --delete-all --yes
|
|
441
475
|
```
|
|
442
476
|
|
|
477
|
+
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.
|
|
478
|
+
|
|
443
479
|
**How do I skip certain directories?**
|
|
444
480
|
|
|
445
481
|
```bash
|
|
@@ -551,7 +587,7 @@ Typical minimal setup (safe for daily use):
|
|
|
551
587
|
|
|
552
588
|
```yaml
|
|
553
589
|
- repo: https://github.com/Tlaloc-Es/KillPy
|
|
554
|
-
rev: 0.
|
|
590
|
+
rev: 0.24.1
|
|
555
591
|
hooks:
|
|
556
592
|
- id: killpy
|
|
557
593
|
```
|
|
@@ -10,6 +10,8 @@ Find and delete old `.venv`, conda, poetry, pipenv, uv and more — safely, in s
|
|
|
10
10
|
uvx killpy --path ~
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
+
> ⭐ Featured in [awesome-python](https://github.com/vinta/awesome-python) — a curated list of the best Python tools
|
|
14
|
+
|
|
13
15
|
[Documentation](https://tlaloc-es.github.io/killpy/)
|
|
14
16
|
|
|
15
17
|
[](https://pypi.org/project/killpy/)
|
|
@@ -19,6 +21,7 @@ uvx killpy --path ~
|
|
|
19
21
|
[](https://codecov.io/gh/Tlaloc-Es/killpy)
|
|
20
22
|
[](LICENSE)
|
|
21
23
|
[](<https://twitter.com/intent/tweet?text=%F0%9F%90%8D%20KillPy%20helps%20you%20reclaim%20disk%20space%20by%20detecting%20unused%20Python%20environments%20(.venv,%20poetry%20env,%20conda%20env)%20and%20pipx%20packages.%20Clean,%20organize%20and%20free%20up%20space%20effortlessly!%20%F0%9F%9A%80&url=https://github.com/Tlaloc-Es/KillPy>)
|
|
24
|
+
[](https://github.com/vinta/awesome-python)
|
|
22
25
|
|
|
23
26
|

|
|
24
27
|
|
|
@@ -26,6 +29,31 @@ uvx killpy --path ~
|
|
|
26
29
|
|
|
27
30
|
______________________________________________________________________
|
|
28
31
|
|
|
32
|
+
## Table of Contents
|
|
33
|
+
|
|
34
|
+
- [The Problem](#the-problem)
|
|
35
|
+
- [What killpy detects](#what-killpy-detects)
|
|
36
|
+
- [Quickstart](#quickstart)
|
|
37
|
+
- [Interactive TUI](#interactive-tui)
|
|
38
|
+
- [Keyboard shortcuts](#keyboard-shortcuts)
|
|
39
|
+
- [Search / filter](#search--filter)
|
|
40
|
+
- [Multi-select mode](#multi-select-mode)
|
|
41
|
+
- [CLI reference](#cli-reference)
|
|
42
|
+
- [`killpy` — launch TUI or headless delete](#killpy--launch-tui-or-headless-delete)
|
|
43
|
+
- [`killpy list` — inspect environments](#killpy-list--inspect-environments)
|
|
44
|
+
- [`killpy delete` — remove environments](#killpy-delete--remove-environments)
|
|
45
|
+
- [`killpy stats` — disk usage summary](#killpy-stats--disk-usage-summary)
|
|
46
|
+
- [`killpy clean` — remove cache directories](#killpy-clean--remove-cache-directories)
|
|
47
|
+
- [`killpy doctor` — smart health report](#killpy-doctor--smart-health-report)
|
|
48
|
+
- [killpy vs alternatives](#killpy-vs-alternatives)
|
|
49
|
+
- [FAQ](#faq)
|
|
50
|
+
- [Pre-commit hooks](#pre-commit-hooks)
|
|
51
|
+
- [Safety](#safety)
|
|
52
|
+
- [Roadmap](#roadmap)
|
|
53
|
+
- [Contributing](#contributing)
|
|
54
|
+
|
|
55
|
+
______________________________________________________________________
|
|
56
|
+
|
|
29
57
|
## The Problem
|
|
30
58
|
|
|
31
59
|
If you have been writing Python for more than a year, your disk is probably full of environments you forgot about.
|
|
@@ -206,8 +234,11 @@ killpy list --type venv --type conda # filter by type (repeatable)
|
|
|
206
234
|
killpy list --older-than 90 # not accessed in the last 90 days
|
|
207
235
|
killpy list --json # output as a JSON array
|
|
208
236
|
killpy list --json-stream # stream as NDJSON — one line per env
|
|
237
|
+
killpy list --quiet # suppress progress output (scripts/CI)
|
|
209
238
|
```
|
|
210
239
|
|
|
240
|
+
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.
|
|
241
|
+
|
|
211
242
|
`--json` example output:
|
|
212
243
|
|
|
213
244
|
```json
|
|
@@ -409,9 +440,12 @@ Deletes every `.venv` / `pyvenv.cfg` env not accessed in the last 90 days, witho
|
|
|
409
440
|
**How do I use killpy in a CI pipeline or script?**
|
|
410
441
|
|
|
411
442
|
```bash
|
|
412
|
-
# List as machine-readable JSON
|
|
443
|
+
# List as machine-readable JSON (progress goes to stderr, JSON to stdout)
|
|
413
444
|
killpy list --json
|
|
414
445
|
|
|
446
|
+
# Suppress progress output entirely with --quiet / -q
|
|
447
|
+
killpy list --json --quiet | jq '.[] | .size_human'
|
|
448
|
+
|
|
415
449
|
# Stream results as NDJSON in real time
|
|
416
450
|
killpy list --json-stream | jq '.size_bytes'
|
|
417
451
|
|
|
@@ -419,6 +453,8 @@ killpy list --json-stream | jq '.size_bytes'
|
|
|
419
453
|
killpy --path ./build_artifacts --delete-all --yes
|
|
420
454
|
```
|
|
421
455
|
|
|
456
|
+
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.
|
|
457
|
+
|
|
422
458
|
**How do I skip certain directories?**
|
|
423
459
|
|
|
424
460
|
```bash
|
|
@@ -530,7 +566,7 @@ Typical minimal setup (safe for daily use):
|
|
|
530
566
|
|
|
531
567
|
```yaml
|
|
532
568
|
- repo: https://github.com/Tlaloc-Es/KillPy
|
|
533
|
-
rev: 0.
|
|
569
|
+
rev: 0.24.1
|
|
534
570
|
hooks:
|
|
535
571
|
- id: killpy
|
|
536
572
|
```
|
|
@@ -52,6 +52,14 @@ def _run_delete_all(path: Path, excluded: set[str], yes: bool) -> None:
|
|
|
52
52
|
console.print("Aborted.")
|
|
53
53
|
return
|
|
54
54
|
|
|
55
|
+
# Record this cleanup session up-front so `killpy stats --history` reflects
|
|
56
|
+
# it; record_deletion() below updates the same record with the freed bytes.
|
|
57
|
+
tracker = UsageTracker()
|
|
58
|
+
try:
|
|
59
|
+
tracker.record_scan_result(envs, path)
|
|
60
|
+
except Exception: # noqa: BLE001
|
|
61
|
+
pass
|
|
62
|
+
|
|
55
63
|
deleted = 0
|
|
56
64
|
freed = 0
|
|
57
65
|
for env in envs:
|
|
@@ -67,9 +75,9 @@ def _run_delete_all(path: Path, excluded: set[str], yes: bool) -> None:
|
|
|
67
75
|
f"freed [bold]{format_size(freed)}[/bold]."
|
|
68
76
|
)
|
|
69
77
|
|
|
70
|
-
# Best-effort: record
|
|
78
|
+
# Best-effort: update the history record created above with freed bytes.
|
|
71
79
|
try:
|
|
72
|
-
|
|
80
|
+
tracker.record_deletion(freed)
|
|
73
81
|
except Exception: # noqa: BLE001
|
|
74
82
|
pass
|
|
75
83
|
|
|
@@ -11,6 +11,7 @@ from typing import Any, TypedDict
|
|
|
11
11
|
from rich.text import Text
|
|
12
12
|
from textual.app import App, ComposeResult
|
|
13
13
|
from textual.binding import Binding
|
|
14
|
+
from textual.coordinate import Coordinate
|
|
14
15
|
from textual.widgets import (
|
|
15
16
|
DataTable,
|
|
16
17
|
Footer,
|
|
@@ -25,7 +26,7 @@ from textual.widgets import (
|
|
|
25
26
|
from killpy.cleaner import Cleaner, CleanerError
|
|
26
27
|
from killpy.cleaners import remove_pycache
|
|
27
28
|
from killpy.files import format_size
|
|
28
|
-
from killpy.intelligence import SuggestionEngine, score_all
|
|
29
|
+
from killpy.intelligence import SuggestionEngine, UsageTracker, score_all
|
|
29
30
|
from killpy.models import Environment
|
|
30
31
|
from killpy.scanner import Scanner
|
|
31
32
|
|
|
@@ -142,9 +143,10 @@ class TableApp(App):
|
|
|
142
143
|
self._filter_query: str = ""
|
|
143
144
|
self._venv_display_indices: list[int] = []
|
|
144
145
|
self._multi_select_mode: bool = False
|
|
145
|
-
self.
|
|
146
|
+
self._selected_venv_paths: set[str] = set()
|
|
146
147
|
self._health_by_path: dict[str, str] = {}
|
|
147
148
|
self.cleaner = Cleaner()
|
|
149
|
+
self.tracker = UsageTracker()
|
|
148
150
|
self.scanner = Scanner(
|
|
149
151
|
types={
|
|
150
152
|
"venv",
|
|
@@ -395,7 +397,7 @@ class TableApp(App):
|
|
|
395
397
|
row["size"],
|
|
396
398
|
row["size_human"],
|
|
397
399
|
_health_text(row["health"]),
|
|
398
|
-
self._compute_row_status(
|
|
400
|
+
self._compute_row_status(row),
|
|
399
401
|
)
|
|
400
402
|
|
|
401
403
|
def render_pipx_table(self) -> None:
|
|
@@ -416,18 +418,18 @@ class TableApp(App):
|
|
|
416
418
|
return data_index, self.venv_rows[data_index]
|
|
417
419
|
return None
|
|
418
420
|
|
|
419
|
-
def _compute_row_status(self,
|
|
421
|
+
def _compute_row_status(self, row: VenvRow) -> str:
|
|
420
422
|
"""Return the status string to display, taking multi-select into account."""
|
|
421
423
|
if row["status"] == EnvStatus.DELETED.value:
|
|
422
424
|
return EnvStatus.DELETED.value
|
|
423
|
-
if self._multi_select_mode and
|
|
425
|
+
if self._multi_select_mode and row["path"] in self._selected_venv_paths:
|
|
424
426
|
return "\u25cf SELECTED"
|
|
425
427
|
return row["status"]
|
|
426
428
|
|
|
427
429
|
def _update_multi_select_label(self) -> None:
|
|
428
430
|
label = self.query_one("#multi-select-label", Label)
|
|
429
431
|
if self._multi_select_mode:
|
|
430
|
-
n = len(self.
|
|
432
|
+
n = len(self._selected_venv_paths)
|
|
431
433
|
label.add_class("visible")
|
|
432
434
|
label.update(
|
|
433
435
|
f"[bold yellow]Multi-select[/bold yellow] — "
|
|
@@ -572,8 +574,27 @@ class TableApp(App):
|
|
|
572
574
|
status_label.update(
|
|
573
575
|
f"Found {venv_count} virtual environments and {pipx_count} pipx packages"
|
|
574
576
|
)
|
|
577
|
+
self._record_scan()
|
|
575
578
|
await self._compute_health_scores()
|
|
576
579
|
|
|
580
|
+
def _record_scan(self) -> None:
|
|
581
|
+
"""Persist this scan session so ``killpy stats --history`` reflects it."""
|
|
582
|
+
try:
|
|
583
|
+
envs = [row["environment"] for row in self.venv_rows]
|
|
584
|
+
envs += [row["environment"] for row in self.pipx_rows]
|
|
585
|
+
self.tracker.record_scan_result(envs, self.root_dir)
|
|
586
|
+
except Exception: # noqa: BLE001
|
|
587
|
+
pass
|
|
588
|
+
|
|
589
|
+
def _record_deletion(self, freed: int) -> None:
|
|
590
|
+
"""Best-effort: add *freed* bytes to the current history record."""
|
|
591
|
+
if freed <= 0:
|
|
592
|
+
return
|
|
593
|
+
try:
|
|
594
|
+
self.tracker.record_deletion(freed)
|
|
595
|
+
except Exception: # noqa: BLE001
|
|
596
|
+
pass
|
|
597
|
+
|
|
577
598
|
async def _compute_health_scores(self) -> None:
|
|
578
599
|
"""Score venv environments and populate the Health column."""
|
|
579
600
|
if not self.venv_rows:
|
|
@@ -614,25 +635,32 @@ class TableApp(App):
|
|
|
614
635
|
|
|
615
636
|
@is_venv_tab
|
|
616
637
|
def action_confirm_delete(self):
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
638
|
+
freed_now = 0
|
|
639
|
+
if self._multi_select_mode and self._selected_venv_paths:
|
|
640
|
+
# Multi-select mode: delete all selected rows. Selection is
|
|
641
|
+
# tracked by path (not by list position) so it stays valid even
|
|
642
|
+
# after the rows have been re-sorted.
|
|
643
|
+
for row in self.venv_rows:
|
|
644
|
+
if row["path"] not in self._selected_venv_paths:
|
|
645
|
+
continue
|
|
621
646
|
if row["status"] == EnvStatus.DELETED.value:
|
|
622
647
|
continue
|
|
623
648
|
if self.delete_environment(row["environment"]):
|
|
649
|
+
freed_now += int(row["size"])
|
|
624
650
|
self.bytes_release += int(row["size"])
|
|
625
651
|
row["status"] = EnvStatus.DELETED.value
|
|
626
|
-
self.
|
|
652
|
+
self._selected_venv_paths.clear()
|
|
627
653
|
else:
|
|
628
654
|
# Normal mode: delete all rows marked for deletion
|
|
629
655
|
for row in self.venv_rows:
|
|
630
656
|
if row["status"] != EnvStatus.MARKED_TO_DELETE.value:
|
|
631
657
|
continue
|
|
632
658
|
if self.delete_environment(row["environment"]):
|
|
659
|
+
freed_now += int(row["size"])
|
|
633
660
|
self.bytes_release += int(row["size"])
|
|
634
661
|
row["status"] = EnvStatus.DELETED.value
|
|
635
662
|
|
|
663
|
+
self._record_deletion(freed_now)
|
|
636
664
|
self.render_venv_table()
|
|
637
665
|
self.query_one("#status-label", Label).update(
|
|
638
666
|
f"{format_size(self.bytes_release)} deleted"
|
|
@@ -675,6 +703,7 @@ class TableApp(App):
|
|
|
675
703
|
return
|
|
676
704
|
if self.delete_environment(row["environment"]):
|
|
677
705
|
self.bytes_release += int(row["size"])
|
|
706
|
+
self._record_deletion(int(row["size"]))
|
|
678
707
|
row["status"] = EnvStatus.DELETED.value
|
|
679
708
|
table.update_cell_at(
|
|
680
709
|
(cursor_cell.row, self.VENV_COL_STATUS),
|
|
@@ -706,6 +735,7 @@ class TableApp(App):
|
|
|
706
735
|
row["status"] = EnvStatus.DELETED.value
|
|
707
736
|
table.update_cell_at((cursor_cell.row, 3), EnvStatus.DELETED.value)
|
|
708
737
|
self.bytes_release += int(row["size"])
|
|
738
|
+
self._record_deletion(int(row["size"]))
|
|
709
739
|
self.query_one("#status-label", Label).update(
|
|
710
740
|
f"{format_size(self.bytes_release)} deleted"
|
|
711
741
|
)
|
|
@@ -794,7 +824,7 @@ class TableApp(App):
|
|
|
794
824
|
"""Toggle multi-select mode on/off (T key)."""
|
|
795
825
|
self._multi_select_mode = not self._multi_select_mode
|
|
796
826
|
if not self._multi_select_mode:
|
|
797
|
-
self.
|
|
827
|
+
self._selected_venv_paths.clear()
|
|
798
828
|
self._update_multi_select_label()
|
|
799
829
|
self.render_venv_table()
|
|
800
830
|
|
|
@@ -810,17 +840,17 @@ class TableApp(App):
|
|
|
810
840
|
resolved = self._resolve_venv_row(cursor_cell.row)
|
|
811
841
|
if not resolved:
|
|
812
842
|
return
|
|
813
|
-
|
|
843
|
+
_, row = resolved
|
|
814
844
|
if row["status"] == EnvStatus.DELETED.value:
|
|
815
845
|
return
|
|
816
|
-
if
|
|
817
|
-
self.
|
|
846
|
+
if row["path"] in self._selected_venv_paths:
|
|
847
|
+
self._selected_venv_paths.discard(row["path"])
|
|
818
848
|
else:
|
|
819
|
-
self.
|
|
849
|
+
self._selected_venv_paths.add(row["path"])
|
|
820
850
|
self._update_multi_select_label()
|
|
821
851
|
table.update_cell_at(
|
|
822
|
-
(cursor_cell.row, self.VENV_COL_STATUS),
|
|
823
|
-
self._compute_row_status(
|
|
852
|
+
Coordinate(cursor_cell.row, self.VENV_COL_STATUS),
|
|
853
|
+
self._compute_row_status(row),
|
|
824
854
|
)
|
|
825
855
|
|
|
826
856
|
@is_venv_tab
|
|
@@ -829,13 +859,13 @@ class TableApp(App):
|
|
|
829
859
|
if not self._multi_select_mode:
|
|
830
860
|
return
|
|
831
861
|
non_deleted = {
|
|
832
|
-
i
|
|
862
|
+
self.venv_rows[i]["path"]
|
|
833
863
|
for i in self._venv_display_indices
|
|
834
864
|
if self.venv_rows[i]["status"] != EnvStatus.DELETED.value
|
|
835
865
|
}
|
|
836
|
-
if non_deleted == self.
|
|
837
|
-
self.
|
|
866
|
+
if non_deleted == self._selected_venv_paths:
|
|
867
|
+
self._selected_venv_paths.clear()
|
|
838
868
|
else:
|
|
839
|
-
self.
|
|
869
|
+
self._selected_venv_paths = non_deleted
|
|
840
870
|
self._update_multi_select_label()
|
|
841
871
|
self.render_venv_table()
|
|
@@ -96,6 +96,14 @@ def delete_cmd(
|
|
|
96
96
|
abort=True,
|
|
97
97
|
)
|
|
98
98
|
|
|
99
|
+
# Record this cleanup session up-front so `killpy stats --history` reflects
|
|
100
|
+
# it; record_deletion() below updates the same record with the freed bytes.
|
|
101
|
+
tracker = UsageTracker()
|
|
102
|
+
try:
|
|
103
|
+
tracker.record_scan_result(envs, path)
|
|
104
|
+
except Exception: # noqa: BLE001
|
|
105
|
+
pass
|
|
106
|
+
|
|
99
107
|
cleaner = Cleaner(dry_run=False)
|
|
100
108
|
freed = 0
|
|
101
109
|
errors = 0
|
|
@@ -114,9 +122,9 @@ def delete_cmd(
|
|
|
114
122
|
+ (f" — [red]{errors} error(s)[/red]" if errors else "")
|
|
115
123
|
)
|
|
116
124
|
|
|
117
|
-
# Best-effort: record
|
|
125
|
+
# Best-effort: update the history record created above with freed bytes.
|
|
118
126
|
try:
|
|
119
|
-
|
|
127
|
+
tracker.record_deletion(freed)
|
|
120
128
|
except Exception: # noqa: BLE001
|
|
121
129
|
pass
|
|
122
130
|
|
|
@@ -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())
|
|
@@ -6,9 +6,11 @@ import json
|
|
|
6
6
|
import logging
|
|
7
7
|
import os
|
|
8
8
|
import tempfile
|
|
9
|
+
from collections.abc import Iterable
|
|
10
|
+
from datetime import datetime, timezone
|
|
9
11
|
from pathlib import Path
|
|
10
12
|
|
|
11
|
-
from killpy.models import ScanRecord, ScoredEnvironment
|
|
13
|
+
from killpy.models import Environment, ScanRecord, ScoredEnvironment
|
|
12
14
|
|
|
13
15
|
logger = logging.getLogger(__name__)
|
|
14
16
|
|
|
@@ -35,6 +37,32 @@ class UsageTracker:
|
|
|
35
37
|
history.append(record.to_dict())
|
|
36
38
|
self._save(history)
|
|
37
39
|
|
|
40
|
+
def record_scan_result(
|
|
41
|
+
self,
|
|
42
|
+
environments: Iterable[Environment],
|
|
43
|
+
scan_path: Path | str,
|
|
44
|
+
*,
|
|
45
|
+
deleted_bytes: int = 0,
|
|
46
|
+
) -> None:
|
|
47
|
+
"""Persist a :class:`~killpy.models.ScanRecord` built from a scan result.
|
|
48
|
+
|
|
49
|
+
Convenience wrapper around :meth:`record_scan` so callers don't have to
|
|
50
|
+
assemble a :class:`ScanRecord` by hand. Records the number of
|
|
51
|
+
environments found and their total size; ``deleted_bytes`` can be set
|
|
52
|
+
when the freed amount is already known, otherwise use
|
|
53
|
+
:meth:`record_deletion` afterwards to update the same record.
|
|
54
|
+
"""
|
|
55
|
+
envs = list(environments)
|
|
56
|
+
self.record_scan(
|
|
57
|
+
ScanRecord(
|
|
58
|
+
timestamp=datetime.now(tz=timezone.utc),
|
|
59
|
+
total_space_found=sum(e.size_bytes for e in envs),
|
|
60
|
+
total_space_deleted=deleted_bytes,
|
|
61
|
+
environments_count=len(envs),
|
|
62
|
+
scan_path=str(scan_path),
|
|
63
|
+
)
|
|
64
|
+
)
|
|
65
|
+
|
|
38
66
|
def record_deletion(self, size_bytes: int) -> None:
|
|
39
67
|
"""Increment the ``total_space_deleted`` counter of the last scan record."""
|
|
40
68
|
history = self._load()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: killpy
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.24.1
|
|
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/
|
|
@@ -31,6 +31,8 @@ Find and delete old `.venv`, conda, poetry, pipenv, uv and more — safely, in s
|
|
|
31
31
|
uvx killpy --path ~
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
+
> ⭐ Featured in [awesome-python](https://github.com/vinta/awesome-python) — a curated list of the best Python tools
|
|
35
|
+
|
|
34
36
|
[Documentation](https://tlaloc-es.github.io/killpy/)
|
|
35
37
|
|
|
36
38
|
[](https://pypi.org/project/killpy/)
|
|
@@ -40,6 +42,7 @@ uvx killpy --path ~
|
|
|
40
42
|
[](https://codecov.io/gh/Tlaloc-Es/killpy)
|
|
41
43
|
[](LICENSE)
|
|
42
44
|
[](<https://twitter.com/intent/tweet?text=%F0%9F%90%8D%20KillPy%20helps%20you%20reclaim%20disk%20space%20by%20detecting%20unused%20Python%20environments%20(.venv,%20poetry%20env,%20conda%20env)%20and%20pipx%20packages.%20Clean,%20organize%20and%20free%20up%20space%20effortlessly!%20%F0%9F%9A%80&url=https://github.com/Tlaloc-Es/KillPy>)
|
|
45
|
+
[](https://github.com/vinta/awesome-python)
|
|
43
46
|
|
|
44
47
|

|
|
45
48
|
|
|
@@ -47,6 +50,31 @@ uvx killpy --path ~
|
|
|
47
50
|
|
|
48
51
|
______________________________________________________________________
|
|
49
52
|
|
|
53
|
+
## Table of Contents
|
|
54
|
+
|
|
55
|
+
- [The Problem](#the-problem)
|
|
56
|
+
- [What killpy detects](#what-killpy-detects)
|
|
57
|
+
- [Quickstart](#quickstart)
|
|
58
|
+
- [Interactive TUI](#interactive-tui)
|
|
59
|
+
- [Keyboard shortcuts](#keyboard-shortcuts)
|
|
60
|
+
- [Search / filter](#search--filter)
|
|
61
|
+
- [Multi-select mode](#multi-select-mode)
|
|
62
|
+
- [CLI reference](#cli-reference)
|
|
63
|
+
- [`killpy` — launch TUI or headless delete](#killpy--launch-tui-or-headless-delete)
|
|
64
|
+
- [`killpy list` — inspect environments](#killpy-list--inspect-environments)
|
|
65
|
+
- [`killpy delete` — remove environments](#killpy-delete--remove-environments)
|
|
66
|
+
- [`killpy stats` — disk usage summary](#killpy-stats--disk-usage-summary)
|
|
67
|
+
- [`killpy clean` — remove cache directories](#killpy-clean--remove-cache-directories)
|
|
68
|
+
- [`killpy doctor` — smart health report](#killpy-doctor--smart-health-report)
|
|
69
|
+
- [killpy vs alternatives](#killpy-vs-alternatives)
|
|
70
|
+
- [FAQ](#faq)
|
|
71
|
+
- [Pre-commit hooks](#pre-commit-hooks)
|
|
72
|
+
- [Safety](#safety)
|
|
73
|
+
- [Roadmap](#roadmap)
|
|
74
|
+
- [Contributing](#contributing)
|
|
75
|
+
|
|
76
|
+
______________________________________________________________________
|
|
77
|
+
|
|
50
78
|
## The Problem
|
|
51
79
|
|
|
52
80
|
If you have been writing Python for more than a year, your disk is probably full of environments you forgot about.
|
|
@@ -227,8 +255,11 @@ killpy list --type venv --type conda # filter by type (repeatable)
|
|
|
227
255
|
killpy list --older-than 90 # not accessed in the last 90 days
|
|
228
256
|
killpy list --json # output as a JSON array
|
|
229
257
|
killpy list --json-stream # stream as NDJSON — one line per env
|
|
258
|
+
killpy list --quiet # suppress progress output (scripts/CI)
|
|
230
259
|
```
|
|
231
260
|
|
|
261
|
+
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.
|
|
262
|
+
|
|
232
263
|
`--json` example output:
|
|
233
264
|
|
|
234
265
|
```json
|
|
@@ -430,9 +461,12 @@ Deletes every `.venv` / `pyvenv.cfg` env not accessed in the last 90 days, witho
|
|
|
430
461
|
**How do I use killpy in a CI pipeline or script?**
|
|
431
462
|
|
|
432
463
|
```bash
|
|
433
|
-
# List as machine-readable JSON
|
|
464
|
+
# List as machine-readable JSON (progress goes to stderr, JSON to stdout)
|
|
434
465
|
killpy list --json
|
|
435
466
|
|
|
467
|
+
# Suppress progress output entirely with --quiet / -q
|
|
468
|
+
killpy list --json --quiet | jq '.[] | .size_human'
|
|
469
|
+
|
|
436
470
|
# Stream results as NDJSON in real time
|
|
437
471
|
killpy list --json-stream | jq '.size_bytes'
|
|
438
472
|
|
|
@@ -440,6 +474,8 @@ killpy list --json-stream | jq '.size_bytes'
|
|
|
440
474
|
killpy --path ./build_artifacts --delete-all --yes
|
|
441
475
|
```
|
|
442
476
|
|
|
477
|
+
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.
|
|
478
|
+
|
|
443
479
|
**How do I skip certain directories?**
|
|
444
480
|
|
|
445
481
|
```bash
|
|
@@ -551,7 +587,7 @@ Typical minimal setup (safe for daily use):
|
|
|
551
587
|
|
|
552
588
|
```yaml
|
|
553
589
|
- repo: https://github.com/Tlaloc-Es/KillPy
|
|
554
|
-
rev: 0.
|
|
590
|
+
rev: 0.24.1
|
|
555
591
|
hooks:
|
|
556
592
|
- id: killpy
|
|
557
593
|
```
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "killpy"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.24.1"
|
|
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 = [
|
|
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
|