killpy 0.20.0__tar.gz → 0.22.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.20.0 → killpy-0.22.0}/PKG-INFO +131 -7
- {killpy-0.20.0 → killpy-0.22.0}/README.md +130 -6
- {killpy-0.20.0 → killpy-0.22.0}/killpy/__main__.py +11 -0
- {killpy-0.20.0 → killpy-0.22.0}/killpy/cli.py +52 -5
- killpy-0.22.0/killpy/commands/_utils.py +65 -0
- {killpy-0.20.0 → killpy-0.22.0}/killpy/commands/clean.py +2 -4
- {killpy-0.20.0 → killpy-0.22.0}/killpy/commands/delete.py +9 -24
- killpy-0.22.0/killpy/commands/doctor.py +285 -0
- {killpy-0.20.0 → killpy-0.22.0}/killpy/commands/list.py +3 -25
- {killpy-0.20.0 → killpy-0.22.0}/killpy/commands/stats.py +38 -1
- {killpy-0.20.0 → killpy-0.22.0}/killpy/detectors/artifacts.py +2 -2
- {killpy-0.20.0 → killpy-0.22.0}/killpy/detectors/conda.py +4 -4
- {killpy-0.20.0 → killpy-0.22.0}/killpy/detectors/hatch.py +2 -2
- {killpy-0.20.0 → killpy-0.22.0}/killpy/detectors/pipenv.py +2 -2
- {killpy-0.20.0 → killpy-0.22.0}/killpy/detectors/pipx.py +32 -26
- {killpy-0.20.0 → killpy-0.22.0}/killpy/detectors/poetry.py +2 -2
- {killpy-0.20.0 → killpy-0.22.0}/killpy/detectors/pyenv.py +2 -2
- {killpy-0.20.0 → killpy-0.22.0}/killpy/detectors/tox.py +2 -2
- {killpy-0.20.0 → killpy-0.22.0}/killpy/detectors/uv.py +2 -2
- {killpy-0.20.0 → killpy-0.22.0}/killpy/detectors/venv.py +0 -1
- killpy-0.22.0/killpy/intelligence/__init__.py +56 -0
- killpy-0.22.0/killpy/intelligence/git_analyzer.py +119 -0
- killpy-0.22.0/killpy/intelligence/scoring.py +227 -0
- killpy-0.22.0/killpy/intelligence/suggestions.py +120 -0
- killpy-0.22.0/killpy/intelligence/tracker.py +123 -0
- {killpy-0.20.0 → killpy-0.22.0}/killpy/models.py +75 -1
- {killpy-0.20.0 → killpy-0.22.0}/killpy.egg-info/PKG-INFO +131 -7
- {killpy-0.20.0 → killpy-0.22.0}/killpy.egg-info/SOURCES.txt +8 -1
- {killpy-0.20.0 → killpy-0.22.0}/pyproject.toml +11 -3
- {killpy-0.20.0 → killpy-0.22.0}/killpy/__init__.py +0 -0
- {killpy-0.20.0 → killpy-0.22.0}/killpy/cleaner.py +0 -0
- {killpy-0.20.0 → killpy-0.22.0}/killpy/cleaners/__init__.py +0 -0
- {killpy-0.20.0 → killpy-0.22.0}/killpy/commands/__init__.py +0 -0
- {killpy-0.20.0 → killpy-0.22.0}/killpy/detectors/__init__.py +0 -0
- {killpy-0.20.0 → killpy-0.22.0}/killpy/detectors/base.py +0 -0
- {killpy-0.20.0 → killpy-0.22.0}/killpy/detectors/cache.py +0 -0
- {killpy-0.20.0 → killpy-0.22.0}/killpy/files/__init__.py +0 -0
- {killpy-0.20.0 → killpy-0.22.0}/killpy/scanner.py +0 -0
- {killpy-0.20.0 → killpy-0.22.0}/killpy.egg-info/dependency_links.txt +0 -0
- {killpy-0.20.0 → killpy-0.22.0}/killpy.egg-info/entry_points.txt +0 -0
- {killpy-0.20.0 → killpy-0.22.0}/killpy.egg-info/requires.txt +0 -0
- {killpy-0.20.0 → killpy-0.22.0}/killpy.egg-info/top_level.txt +0 -0
- {killpy-0.20.0 → killpy-0.22.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.22.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/
|
|
@@ -22,9 +22,13 @@ Requires-Dist: textual>=1.0.0
|
|
|
22
22
|
|
|
23
23
|

|
|
24
24
|
|
|
25
|
-
###
|
|
25
|
+
### Free GBs of disk space by removing unused Python environments
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
Find and delete old `.venv`, conda, poetry, pipenv, uv and more — safely, in seconds.
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
uvx killpy --path ~
|
|
31
|
+
```
|
|
28
32
|
|
|
29
33
|
[Documentation](https://tlaloc-es.github.io/killpy/)
|
|
30
34
|
|
|
@@ -270,6 +274,7 @@ ______________________________________________________________________
|
|
|
270
274
|
killpy stats
|
|
271
275
|
killpy stats --path ~/projects
|
|
272
276
|
killpy stats --json
|
|
277
|
+
killpy stats --history # cumulative scan history
|
|
273
278
|
```
|
|
274
279
|
|
|
275
280
|
Example output:
|
|
@@ -301,6 +306,98 @@ Removes `__pycache__` directories recursively under the target path.
|
|
|
301
306
|
|
|
302
307
|
______________________________________________________________________
|
|
303
308
|
|
|
309
|
+
### `killpy doctor` — smart health report
|
|
310
|
+
|
|
311
|
+
```
|
|
312
|
+
Usage: killpy doctor [OPTIONS]
|
|
313
|
+
|
|
314
|
+
Options:
|
|
315
|
+
--path DIRECTORY Root directory to scan [default: cwd]
|
|
316
|
+
--all Show all environments grouped by category
|
|
317
|
+
(HIGH / MEDIUM / LOW). Default shows only the top 5.
|
|
318
|
+
--json Output as JSON.
|
|
319
|
+
--help Show this message and exit.
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
`doctor` analyses every detected virtual environment in two phases:
|
|
323
|
+
|
|
324
|
+
**Phase 1 — Scoring (for sorting only)**
|
|
325
|
+
|
|
326
|
+
A numeric score between 0 and 1 is computed from four weighted signals:
|
|
327
|
+
|
|
328
|
+
| Signal | Description |
|
|
329
|
+
|--------|-------------|
|
|
330
|
+
| **Size** | Larger environments score higher (sigmoid-normalised around 500 MB). |
|
|
331
|
+
| **Age** | Days since last access, linear up to 365 days. |
|
|
332
|
+
| **Orphan status** | No `pyproject.toml`, `requirements.txt`, or other project marker found nearby. |
|
|
333
|
+
| **Git inactivity** | The associated git repository has no recent commits. |
|
|
334
|
+
|
|
335
|
+
The score determines *ordering* within each category (highest score listed first). It does **not** determine the category itself.
|
|
336
|
+
|
|
337
|
+
**Phase 2 — Rule-based classification**
|
|
338
|
+
|
|
339
|
+
Category is assigned deterministically by applying the following rules in order:
|
|
340
|
+
|
|
341
|
+
| Priority | Rule | Category |
|
|
342
|
+
|----------|------|----------|
|
|
343
|
+
| 1 | Orphan (`is_orphan=True`) **and** `age ≥ 180 days` | `HIGH` |
|
|
344
|
+
| 2 | No project files **and** `age ≥ 365 days` | `HIGH` |
|
|
345
|
+
| 3 | Active git repository **or** `age < 120 days` | `LOW` |
|
|
346
|
+
| 4 | `age ≥ 120 days` | `MEDIUM` |
|
|
347
|
+
| 5 | Fallback | `LOW` |
|
|
348
|
+
|
|
349
|
+
Age and orphan status are the dominant signals. Size does not affect the category.
|
|
350
|
+
|
|
351
|
+
| Category | Recommended action |
|
|
352
|
+
|----------|--------------------|
|
|
353
|
+
| `HIGH` | Delete — unused and orphaned |
|
|
354
|
+
| `MEDIUM` | Review — possibly unused |
|
|
355
|
+
| `LOW` | Keep — actively used / Keep |
|
|
356
|
+
|
|
357
|
+
Examples:
|
|
358
|
+
|
|
359
|
+
```bash
|
|
360
|
+
killpy doctor # top 5 offenders in current directory
|
|
361
|
+
killpy doctor --path ~ # scan home folder
|
|
362
|
+
killpy doctor --all # show all environments by category
|
|
363
|
+
killpy doctor --json # machine-readable output
|
|
364
|
+
killpy doctor --path ~/projects --all # full report for a specific tree
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
Example output (default):
|
|
368
|
+
|
|
369
|
+
```
|
|
370
|
+
──────────── Environment Health Report ────────────
|
|
371
|
+
Scanned: /home/user/projects
|
|
372
|
+
Environments found: 18 | Total size: 6.2 GB | Estimated wasted: 3.8 GB
|
|
373
|
+
HIGH (safe to delete): 5 MEDIUM (review): 7 LOW (keep): 6
|
|
374
|
+
|
|
375
|
+
Top 5 Offenders
|
|
376
|
+
┌──────────────────────────┬────────┬───────────┬───────┬──────────┐
|
|
377
|
+
│ Path │ Size │ Age (days)│ Score │ Category │
|
|
378
|
+
├──────────────────────────┼────────┼───────────┼───────┼──────────┤
|
|
379
|
+
│ ~/old-project/.venv │ 850 MB │ 312 │ 0.94 │ HIGH │
|
|
380
|
+
│ ~/tutorial2023/.venv │ 420 MB │ 198 │ 0.87 │ HIGH │
|
|
381
|
+
└──────────────────────────┴────────┴───────────┴───────┴──────────┘
|
|
382
|
+
|
|
383
|
+
Recommendation: Run `killpy delete --older-than 180` to free up to 3.8 GB.
|
|
384
|
+
(12 MEDIUM/LOW environment(s) hidden — run with --all to see them)
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
Use `--all` to see all three category tables at once:
|
|
388
|
+
|
|
389
|
+
```bash
|
|
390
|
+
killpy doctor --all
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
The JSON output is useful for scripting or auditing:
|
|
394
|
+
|
|
395
|
+
```bash
|
|
396
|
+
killpy doctor --json | jq '.suggestions[] | select(.category=="HIGH") | .env_path'
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
______________________________________________________________________
|
|
400
|
+
|
|
304
401
|
## killpy vs alternatives
|
|
305
402
|
|
|
306
403
|
| Tool | venv | conda | poetry | pipx | pyenv | caches | artifacts | TUI | search | multi-select |
|
|
@@ -422,18 +519,44 @@ Each virtual environment is a full copy (or symlinked tree) of a Python interpre
|
|
|
422
519
|
|
|
423
520
|
______________________________________________________________________
|
|
424
521
|
|
|
425
|
-
## Pre-commit
|
|
522
|
+
## Pre-commit hooks
|
|
426
523
|
|
|
427
|
-
|
|
524
|
+
`killpy` ships four hooks for [pre-commit](https://pre-commit.com/). Add the ones you need to your `.pre-commit-config.yaml`:
|
|
428
525
|
|
|
429
526
|
```yaml
|
|
430
527
|
- repo: https://github.com/Tlaloc-Es/KillPy
|
|
431
|
-
rev: 0.
|
|
528
|
+
rev: 0.20.0
|
|
529
|
+
hooks:
|
|
530
|
+
- id: killpy # remove __pycache__ on every commit
|
|
531
|
+
- id: killpy-clean-caches # also removes .mypy_cache, .pytest_cache, .ruff_cache
|
|
532
|
+
- id: killpy-clean-artifacts # remove dist/, build/, *.egg-info before committing
|
|
533
|
+
- id: killpy-remove-venv # remove .venv (manual stage — see below)
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
| Hook id | What it removes | Default stage |
|
|
537
|
+
|---------|-----------------|---------------|
|
|
538
|
+
| `killpy` | `__pycache__` directories | `pre-commit` |
|
|
539
|
+
| `killpy-clean-caches` | All local cache dirs (`__pycache__`, `.mypy_cache`, `.pytest_cache`, `.ruff_cache`) | `pre-commit` |
|
|
540
|
+
| `killpy-clean-artifacts` | Build artifacts (`dist/`, `build/`, `*.egg-info`) | `pre-commit` |
|
|
541
|
+
| `killpy-remove-venv` | Local `.venv` environments | `manual` |
|
|
542
|
+
|
|
543
|
+
**`killpy-remove-venv`** is staged as `manual` because deleting the environment on every commit would require you to recreate it each time. Run it explicitly when you want a clean slate:
|
|
544
|
+
|
|
545
|
+
```bash
|
|
546
|
+
pre-commit run killpy-remove-venv --hook-stage manual
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
Typical minimal setup (safe for daily use):
|
|
550
|
+
|
|
551
|
+
```yaml
|
|
552
|
+
- repo: https://github.com/Tlaloc-Es/KillPy
|
|
553
|
+
rev: 0.22.0
|
|
432
554
|
hooks:
|
|
433
555
|
- id: killpy
|
|
434
|
-
pass_filenames: false
|
|
435
556
|
```
|
|
436
557
|
|
|
558
|
+
Add `killpy-clean-artifacts` if your project generates `dist/` or `build/` locally and you want to guarantee they are never staged by accident.
|
|
559
|
+
|
|
437
560
|
______________________________________________________________________
|
|
438
561
|
|
|
439
562
|
## Safety
|
|
@@ -455,6 +578,7 @@ ______________________________________________________________________
|
|
|
455
578
|
- [ ] Windows support improvements (pyenv-win, conda on Windows PATH)
|
|
456
579
|
- [ ] TUI: filter panel, live progress bar, confirmation dialog with total bytes before delete
|
|
457
580
|
- [ ] Detect unused dependencies inside `pyproject.toml` / `requirements.txt`
|
|
581
|
+
- [ ] Add environment reporting (Python version, installed packages, and versions) to better differentiate between virtual environments with different dependencies
|
|
458
582
|
|
|
459
583
|
______________________________________________________________________
|
|
460
584
|
|
|
@@ -2,9 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|

|
|
4
4
|
|
|
5
|
-
###
|
|
5
|
+
### Free GBs of disk space by removing unused Python environments
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Find and delete old `.venv`, conda, poetry, pipenv, uv and more — safely, in seconds.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
uvx killpy --path ~
|
|
11
|
+
```
|
|
8
12
|
|
|
9
13
|
[Documentation](https://tlaloc-es.github.io/killpy/)
|
|
10
14
|
|
|
@@ -250,6 +254,7 @@ ______________________________________________________________________
|
|
|
250
254
|
killpy stats
|
|
251
255
|
killpy stats --path ~/projects
|
|
252
256
|
killpy stats --json
|
|
257
|
+
killpy stats --history # cumulative scan history
|
|
253
258
|
```
|
|
254
259
|
|
|
255
260
|
Example output:
|
|
@@ -281,6 +286,98 @@ Removes `__pycache__` directories recursively under the target path.
|
|
|
281
286
|
|
|
282
287
|
______________________________________________________________________
|
|
283
288
|
|
|
289
|
+
### `killpy doctor` — smart health report
|
|
290
|
+
|
|
291
|
+
```
|
|
292
|
+
Usage: killpy doctor [OPTIONS]
|
|
293
|
+
|
|
294
|
+
Options:
|
|
295
|
+
--path DIRECTORY Root directory to scan [default: cwd]
|
|
296
|
+
--all Show all environments grouped by category
|
|
297
|
+
(HIGH / MEDIUM / LOW). Default shows only the top 5.
|
|
298
|
+
--json Output as JSON.
|
|
299
|
+
--help Show this message and exit.
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
`doctor` analyses every detected virtual environment in two phases:
|
|
303
|
+
|
|
304
|
+
**Phase 1 — Scoring (for sorting only)**
|
|
305
|
+
|
|
306
|
+
A numeric score between 0 and 1 is computed from four weighted signals:
|
|
307
|
+
|
|
308
|
+
| Signal | Description |
|
|
309
|
+
|--------|-------------|
|
|
310
|
+
| **Size** | Larger environments score higher (sigmoid-normalised around 500 MB). |
|
|
311
|
+
| **Age** | Days since last access, linear up to 365 days. |
|
|
312
|
+
| **Orphan status** | No `pyproject.toml`, `requirements.txt`, or other project marker found nearby. |
|
|
313
|
+
| **Git inactivity** | The associated git repository has no recent commits. |
|
|
314
|
+
|
|
315
|
+
The score determines *ordering* within each category (highest score listed first). It does **not** determine the category itself.
|
|
316
|
+
|
|
317
|
+
**Phase 2 — Rule-based classification**
|
|
318
|
+
|
|
319
|
+
Category is assigned deterministically by applying the following rules in order:
|
|
320
|
+
|
|
321
|
+
| Priority | Rule | Category |
|
|
322
|
+
|----------|------|----------|
|
|
323
|
+
| 1 | Orphan (`is_orphan=True`) **and** `age ≥ 180 days` | `HIGH` |
|
|
324
|
+
| 2 | No project files **and** `age ≥ 365 days` | `HIGH` |
|
|
325
|
+
| 3 | Active git repository **or** `age < 120 days` | `LOW` |
|
|
326
|
+
| 4 | `age ≥ 120 days` | `MEDIUM` |
|
|
327
|
+
| 5 | Fallback | `LOW` |
|
|
328
|
+
|
|
329
|
+
Age and orphan status are the dominant signals. Size does not affect the category.
|
|
330
|
+
|
|
331
|
+
| Category | Recommended action |
|
|
332
|
+
|----------|--------------------|
|
|
333
|
+
| `HIGH` | Delete — unused and orphaned |
|
|
334
|
+
| `MEDIUM` | Review — possibly unused |
|
|
335
|
+
| `LOW` | Keep — actively used / Keep |
|
|
336
|
+
|
|
337
|
+
Examples:
|
|
338
|
+
|
|
339
|
+
```bash
|
|
340
|
+
killpy doctor # top 5 offenders in current directory
|
|
341
|
+
killpy doctor --path ~ # scan home folder
|
|
342
|
+
killpy doctor --all # show all environments by category
|
|
343
|
+
killpy doctor --json # machine-readable output
|
|
344
|
+
killpy doctor --path ~/projects --all # full report for a specific tree
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
Example output (default):
|
|
348
|
+
|
|
349
|
+
```
|
|
350
|
+
──────────── Environment Health Report ────────────
|
|
351
|
+
Scanned: /home/user/projects
|
|
352
|
+
Environments found: 18 | Total size: 6.2 GB | Estimated wasted: 3.8 GB
|
|
353
|
+
HIGH (safe to delete): 5 MEDIUM (review): 7 LOW (keep): 6
|
|
354
|
+
|
|
355
|
+
Top 5 Offenders
|
|
356
|
+
┌──────────────────────────┬────────┬───────────┬───────┬──────────┐
|
|
357
|
+
│ Path │ Size │ Age (days)│ Score │ Category │
|
|
358
|
+
├──────────────────────────┼────────┼───────────┼───────┼──────────┤
|
|
359
|
+
│ ~/old-project/.venv │ 850 MB │ 312 │ 0.94 │ HIGH │
|
|
360
|
+
│ ~/tutorial2023/.venv │ 420 MB │ 198 │ 0.87 │ HIGH │
|
|
361
|
+
└──────────────────────────┴────────┴───────────┴───────┴──────────┘
|
|
362
|
+
|
|
363
|
+
Recommendation: Run `killpy delete --older-than 180` to free up to 3.8 GB.
|
|
364
|
+
(12 MEDIUM/LOW environment(s) hidden — run with --all to see them)
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
Use `--all` to see all three category tables at once:
|
|
368
|
+
|
|
369
|
+
```bash
|
|
370
|
+
killpy doctor --all
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
The JSON output is useful for scripting or auditing:
|
|
374
|
+
|
|
375
|
+
```bash
|
|
376
|
+
killpy doctor --json | jq '.suggestions[] | select(.category=="HIGH") | .env_path'
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
______________________________________________________________________
|
|
380
|
+
|
|
284
381
|
## killpy vs alternatives
|
|
285
382
|
|
|
286
383
|
| Tool | venv | conda | poetry | pipx | pyenv | caches | artifacts | TUI | search | multi-select |
|
|
@@ -402,18 +499,44 @@ Each virtual environment is a full copy (or symlinked tree) of a Python interpre
|
|
|
402
499
|
|
|
403
500
|
______________________________________________________________________
|
|
404
501
|
|
|
405
|
-
## Pre-commit
|
|
502
|
+
## Pre-commit hooks
|
|
406
503
|
|
|
407
|
-
|
|
504
|
+
`killpy` ships four hooks for [pre-commit](https://pre-commit.com/). Add the ones you need to your `.pre-commit-config.yaml`:
|
|
408
505
|
|
|
409
506
|
```yaml
|
|
410
507
|
- repo: https://github.com/Tlaloc-Es/KillPy
|
|
411
|
-
rev: 0.
|
|
508
|
+
rev: 0.20.0
|
|
509
|
+
hooks:
|
|
510
|
+
- id: killpy # remove __pycache__ on every commit
|
|
511
|
+
- id: killpy-clean-caches # also removes .mypy_cache, .pytest_cache, .ruff_cache
|
|
512
|
+
- id: killpy-clean-artifacts # remove dist/, build/, *.egg-info before committing
|
|
513
|
+
- id: killpy-remove-venv # remove .venv (manual stage — see below)
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
| Hook id | What it removes | Default stage |
|
|
517
|
+
|---------|-----------------|---------------|
|
|
518
|
+
| `killpy` | `__pycache__` directories | `pre-commit` |
|
|
519
|
+
| `killpy-clean-caches` | All local cache dirs (`__pycache__`, `.mypy_cache`, `.pytest_cache`, `.ruff_cache`) | `pre-commit` |
|
|
520
|
+
| `killpy-clean-artifacts` | Build artifacts (`dist/`, `build/`, `*.egg-info`) | `pre-commit` |
|
|
521
|
+
| `killpy-remove-venv` | Local `.venv` environments | `manual` |
|
|
522
|
+
|
|
523
|
+
**`killpy-remove-venv`** is staged as `manual` because deleting the environment on every commit would require you to recreate it each time. Run it explicitly when you want a clean slate:
|
|
524
|
+
|
|
525
|
+
```bash
|
|
526
|
+
pre-commit run killpy-remove-venv --hook-stage manual
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
Typical minimal setup (safe for daily use):
|
|
530
|
+
|
|
531
|
+
```yaml
|
|
532
|
+
- repo: https://github.com/Tlaloc-Es/KillPy
|
|
533
|
+
rev: 0.22.0
|
|
412
534
|
hooks:
|
|
413
535
|
- id: killpy
|
|
414
|
-
pass_filenames: false
|
|
415
536
|
```
|
|
416
537
|
|
|
538
|
+
Add `killpy-clean-artifacts` if your project generates `dist/` or `build/` locally and you want to guarantee they are never staged by accident.
|
|
539
|
+
|
|
417
540
|
______________________________________________________________________
|
|
418
541
|
|
|
419
542
|
## Safety
|
|
@@ -435,6 +558,7 @@ ______________________________________________________________________
|
|
|
435
558
|
- [ ] Windows support improvements (pyenv-win, conda on Windows PATH)
|
|
436
559
|
- [ ] TUI: filter panel, live progress bar, confirmation dialog with total bytes before delete
|
|
437
560
|
- [ ] Detect unused dependencies inside `pyproject.toml` / `requirements.txt`
|
|
561
|
+
- [ ] Add environment reporting (Python version, installed packages, and versions) to better differentiate between virtual environments with different dependencies
|
|
438
562
|
|
|
439
563
|
______________________________________________________________________
|
|
440
564
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import logging
|
|
1
2
|
from pathlib import Path
|
|
2
3
|
|
|
3
4
|
import click
|
|
@@ -9,9 +10,11 @@ from killpy.cleaner import Cleaner
|
|
|
9
10
|
from killpy.cli import TableApp
|
|
10
11
|
from killpy.commands.clean import clean
|
|
11
12
|
from killpy.commands.delete import delete_cmd
|
|
13
|
+
from killpy.commands.doctor import doctor_cmd
|
|
12
14
|
from killpy.commands.list import list_cmd
|
|
13
15
|
from killpy.commands.stats import stats_cmd
|
|
14
16
|
from killpy.files import format_size
|
|
17
|
+
from killpy.intelligence.tracker import UsageTracker
|
|
15
18
|
from killpy.scanner import Scanner
|
|
16
19
|
|
|
17
20
|
|
|
@@ -63,6 +66,12 @@ def _run_delete_all(path: Path, excluded: set[str], yes: bool) -> None:
|
|
|
63
66
|
f"freed [bold]{format_size(freed)}[/bold]."
|
|
64
67
|
)
|
|
65
68
|
|
|
69
|
+
# Best-effort: record deletion in history.
|
|
70
|
+
try:
|
|
71
|
+
UsageTracker().record_deletion(freed)
|
|
72
|
+
except Exception: # noqa: BLE001
|
|
73
|
+
pass
|
|
74
|
+
|
|
66
75
|
|
|
67
76
|
@click.group(invoke_without_command=True)
|
|
68
77
|
@click.option(
|
|
@@ -94,6 +103,7 @@ def _run_delete_all(path: Path, excluded: set[str], yes: bool) -> None:
|
|
|
94
103
|
)
|
|
95
104
|
@click.pass_context
|
|
96
105
|
def cli(ctx, path: Path, exclude: str, delete_all: bool, yes: bool):
|
|
106
|
+
logging.basicConfig(level=logging.WARNING)
|
|
97
107
|
excluded = (
|
|
98
108
|
{p.strip() for p in exclude.split(",") if p.strip()} if exclude else set()
|
|
99
109
|
)
|
|
@@ -109,6 +119,7 @@ cli.add_command(clean)
|
|
|
109
119
|
cli.add_command(list_cmd, name="list")
|
|
110
120
|
cli.add_command(delete_cmd, name="delete")
|
|
111
121
|
cli.add_command(stats_cmd, name="stats")
|
|
122
|
+
cli.add_command(doctor_cmd, name="doctor")
|
|
112
123
|
|
|
113
124
|
|
|
114
125
|
if __name__ == "__main__":
|
|
@@ -8,6 +8,7 @@ from importlib.metadata import PackageNotFoundError, version
|
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
from typing import Any, TypedDict
|
|
10
10
|
|
|
11
|
+
from rich.text import Text
|
|
11
12
|
from textual.app import App, ComposeResult
|
|
12
13
|
from textual.binding import Binding
|
|
13
14
|
from textual.widgets import (
|
|
@@ -24,9 +25,23 @@ from textual.widgets import (
|
|
|
24
25
|
from killpy.cleaner import Cleaner, CleanerError
|
|
25
26
|
from killpy.cleaners import remove_pycache
|
|
26
27
|
from killpy.files import format_size
|
|
28
|
+
from killpy.intelligence import SuggestionEngine, score_all
|
|
27
29
|
from killpy.models import Environment
|
|
28
30
|
from killpy.scanner import Scanner
|
|
29
31
|
|
|
32
|
+
_HEALTH_STYLES: dict[str, tuple[str, str]] = {
|
|
33
|
+
"HIGH": ("HIGH", "bold red"),
|
|
34
|
+
"MEDIUM": ("MED", "bold yellow"),
|
|
35
|
+
"LOW": ("LOW", "bold green"),
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _health_text(category: str) -> Text | str:
|
|
40
|
+
if category in _HEALTH_STYLES:
|
|
41
|
+
label, style = _HEALTH_STYLES[category]
|
|
42
|
+
return Text(label, style=style)
|
|
43
|
+
return ""
|
|
44
|
+
|
|
30
45
|
|
|
31
46
|
def is_venv_tab(func):
|
|
32
47
|
def wrapper(self, *args, **kwargs):
|
|
@@ -68,6 +83,7 @@ class VenvRow(TypedDict):
|
|
|
68
83
|
last_modified: str
|
|
69
84
|
size: int
|
|
70
85
|
size_human: str
|
|
86
|
+
health: str
|
|
71
87
|
status: str
|
|
72
88
|
environment: Environment
|
|
73
89
|
|
|
@@ -87,6 +103,7 @@ class TableApp(App):
|
|
|
87
103
|
"Last Modified",
|
|
88
104
|
"Size",
|
|
89
105
|
"Size (Human Readable)",
|
|
106
|
+
"Health",
|
|
90
107
|
"Status",
|
|
91
108
|
]
|
|
92
109
|
PIPX_HEADERS = ["Package", "Size", "Size (Human Readable)", "Status"]
|
|
@@ -95,7 +112,8 @@ class TableApp(App):
|
|
|
95
112
|
VENV_COL_LAST_MODIFIED = 2
|
|
96
113
|
VENV_COL_SIZE = 3
|
|
97
114
|
VENV_COL_SIZE_HUMAN = 4
|
|
98
|
-
|
|
115
|
+
VENV_COL_HEALTH = 5
|
|
116
|
+
VENV_COL_STATUS = 6
|
|
99
117
|
|
|
100
118
|
PIPX_COL_PACKAGE = 0
|
|
101
119
|
PIPX_COL_SIZE = 1
|
|
@@ -125,6 +143,7 @@ class TableApp(App):
|
|
|
125
143
|
self._venv_display_indices: list[int] = []
|
|
126
144
|
self._multi_select_mode: bool = False
|
|
127
145
|
self._selected_venv_indices: set[int] = set()
|
|
146
|
+
self._health_by_path: dict[str, str] = {}
|
|
128
147
|
self.cleaner = Cleaner()
|
|
129
148
|
self.scanner = Scanner(
|
|
130
149
|
types={
|
|
@@ -315,6 +334,7 @@ class TableApp(App):
|
|
|
315
334
|
"last_modified": environment.last_accessed_str,
|
|
316
335
|
"size": environment.size_bytes,
|
|
317
336
|
"size_human": environment.size_human,
|
|
337
|
+
"health": self._health_by_path.get(str(environment.path), ""),
|
|
318
338
|
"status": "",
|
|
319
339
|
"environment": environment,
|
|
320
340
|
}
|
|
@@ -332,6 +352,7 @@ class TableApp(App):
|
|
|
332
352
|
row["last_modified"],
|
|
333
353
|
row["size"],
|
|
334
354
|
row["size_human"],
|
|
355
|
+
_health_text(row["health"]),
|
|
335
356
|
row["status"],
|
|
336
357
|
)
|
|
337
358
|
|
|
@@ -373,6 +394,7 @@ class TableApp(App):
|
|
|
373
394
|
row["last_modified"],
|
|
374
395
|
row["size"],
|
|
375
396
|
row["size_human"],
|
|
397
|
+
_health_text(row["health"]),
|
|
376
398
|
self._compute_row_status(i, row),
|
|
377
399
|
)
|
|
378
400
|
|
|
@@ -453,6 +475,11 @@ class TableApp(App):
|
|
|
453
475
|
self.venv_rows.sort(
|
|
454
476
|
key=lambda row: row["size_human"].lower(), reverse=reverse
|
|
455
477
|
)
|
|
478
|
+
elif column_index == self.VENV_COL_HEALTH:
|
|
479
|
+
_order = {"HIGH": 0, "MEDIUM": 1, "LOW": 2, "": 3}
|
|
480
|
+
self.venv_rows.sort(
|
|
481
|
+
key=lambda row: _order.get(row["health"], 3), reverse=reverse
|
|
482
|
+
)
|
|
456
483
|
elif column_index == self.VENV_COL_STATUS:
|
|
457
484
|
self.venv_rows.sort(key=lambda row: row["status"].lower(), reverse=reverse)
|
|
458
485
|
else:
|
|
@@ -545,6 +572,21 @@ class TableApp(App):
|
|
|
545
572
|
status_label.update(
|
|
546
573
|
f"Found {venv_count} virtual environments and {pipx_count} pipx packages"
|
|
547
574
|
)
|
|
575
|
+
await self._compute_health_scores()
|
|
576
|
+
|
|
577
|
+
async def _compute_health_scores(self) -> None:
|
|
578
|
+
"""Score venv environments and populate the Health column."""
|
|
579
|
+
if not self.venv_rows:
|
|
580
|
+
return
|
|
581
|
+
envs = [row["environment"] for row in self.venv_rows]
|
|
582
|
+
scored = await asyncio.to_thread(lambda: score_all(envs, run_git=False))
|
|
583
|
+
engine = SuggestionEngine()
|
|
584
|
+
suggestions = engine.classify_all(scored)
|
|
585
|
+
for suggestion in suggestions:
|
|
586
|
+
self._health_by_path[str(suggestion.env_path)] = suggestion.category
|
|
587
|
+
for row in self.venv_rows:
|
|
588
|
+
row["health"] = self._health_by_path.get(row["path"], "")
|
|
589
|
+
self.render_venv_table()
|
|
548
590
|
|
|
549
591
|
async def action_clean_pycache(self):
|
|
550
592
|
total_freed_space = await asyncio.to_thread(remove_pycache, self.root_dir)
|
|
@@ -612,11 +654,12 @@ class TableApp(App):
|
|
|
612
654
|
return
|
|
613
655
|
elif current_status == EnvStatus.MARKED_TO_DELETE.value:
|
|
614
656
|
row["status"] = ""
|
|
615
|
-
table.update_cell_at((cursor_cell.row,
|
|
657
|
+
table.update_cell_at((cursor_cell.row, self.VENV_COL_STATUS), "")
|
|
616
658
|
else:
|
|
617
659
|
row["status"] = EnvStatus.MARKED_TO_DELETE.value
|
|
618
660
|
table.update_cell_at(
|
|
619
|
-
(cursor_cell.row,
|
|
661
|
+
(cursor_cell.row, self.VENV_COL_STATUS),
|
|
662
|
+
EnvStatus.MARKED_TO_DELETE.value,
|
|
620
663
|
)
|
|
621
664
|
|
|
622
665
|
@is_venv_tab
|
|
@@ -633,7 +676,10 @@ class TableApp(App):
|
|
|
633
676
|
if self.delete_environment(row["environment"]):
|
|
634
677
|
self.bytes_release += int(row["size"])
|
|
635
678
|
row["status"] = EnvStatus.DELETED.value
|
|
636
|
-
table.update_cell_at(
|
|
679
|
+
table.update_cell_at(
|
|
680
|
+
(cursor_cell.row, self.VENV_COL_STATUS),
|
|
681
|
+
EnvStatus.DELETED.value,
|
|
682
|
+
)
|
|
637
683
|
self.query_one("#status-label", Label).update(
|
|
638
684
|
f"{format_size(self.bytes_release)} deleted"
|
|
639
685
|
)
|
|
@@ -773,7 +819,8 @@ class TableApp(App):
|
|
|
773
819
|
self._selected_venv_indices.add(data_index)
|
|
774
820
|
self._update_multi_select_label()
|
|
775
821
|
table.update_cell_at(
|
|
776
|
-
(cursor_cell.row,
|
|
822
|
+
(cursor_cell.row, self.VENV_COL_STATUS),
|
|
823
|
+
self._compute_row_status(data_index, row),
|
|
777
824
|
)
|
|
778
825
|
|
|
779
826
|
@is_venv_tab
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Shared helpers for ``killpy`` commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import datetime, timedelta, timezone
|
|
6
|
+
|
|
7
|
+
from killpy.models import Environment
|
|
8
|
+
|
|
9
|
+
# Maps user-facing type names (detector names) to the concrete ``Environment.type``
|
|
10
|
+
# values those detectors produce. Two detectors use sub-type tags instead of
|
|
11
|
+
# their own name: VenvDetector (tags: ".venv", "pyvenv.cfg") and CacheDetector
|
|
12
|
+
# (tags: "__pycache__", ".mypy_cache", ".pytest_cache", ".ruff_cache",
|
|
13
|
+
# "pip-cache", "uv-cache"). Without this mapping, ``--type venv`` and
|
|
14
|
+
# ``--type cache`` would never match anything.
|
|
15
|
+
_TYPE_ALIASES: dict[str, frozenset[str]] = {
|
|
16
|
+
"venv": frozenset({".venv", "pyvenv.cfg"}),
|
|
17
|
+
"cache": frozenset(
|
|
18
|
+
{
|
|
19
|
+
"__pycache__",
|
|
20
|
+
".mypy_cache",
|
|
21
|
+
".pytest_cache",
|
|
22
|
+
".ruff_cache",
|
|
23
|
+
"pip-cache",
|
|
24
|
+
"uv-cache",
|
|
25
|
+
}
|
|
26
|
+
),
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def filter_envs(
|
|
31
|
+
envs: list[Environment],
|
|
32
|
+
types: tuple[str, ...] | None,
|
|
33
|
+
older_than: int | None,
|
|
34
|
+
) -> list[Environment]:
|
|
35
|
+
"""Return a filtered subset of *envs*.
|
|
36
|
+
|
|
37
|
+
Parameters
|
|
38
|
+
----------
|
|
39
|
+
envs:
|
|
40
|
+
Full list of detected environments.
|
|
41
|
+
types:
|
|
42
|
+
If provided, only environments whose :attr:`~killpy.models.Environment.type`
|
|
43
|
+
matches one of these strings (case-insensitive) are kept. Detector
|
|
44
|
+
names such as ``"venv"`` and ``"cache"`` are automatically expanded to
|
|
45
|
+
their concrete sub-type values via :data:`_TYPE_ALIASES`.
|
|
46
|
+
older_than:
|
|
47
|
+
If provided, only environments not accessed in the last *older_than* days
|
|
48
|
+
are kept.
|
|
49
|
+
"""
|
|
50
|
+
now = datetime.now(tz=timezone.utc)
|
|
51
|
+
result = envs
|
|
52
|
+
|
|
53
|
+
if types:
|
|
54
|
+
expanded: set[str] = set()
|
|
55
|
+
for t in types:
|
|
56
|
+
t_lower = t.strip().lower()
|
|
57
|
+
expanded.add(t_lower)
|
|
58
|
+
expanded.update(_TYPE_ALIASES.get(t_lower, frozenset()))
|
|
59
|
+
result = [e for e in result if e.type.lower() in expanded]
|
|
60
|
+
|
|
61
|
+
if older_than is not None:
|
|
62
|
+
cutoff = now - timedelta(days=older_than)
|
|
63
|
+
result = [e for e in result if e.last_accessed < cutoff]
|
|
64
|
+
|
|
65
|
+
return result
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import logging
|
|
2
1
|
from pathlib import Path
|
|
3
2
|
|
|
4
3
|
import click
|
|
@@ -10,8 +9,7 @@ from killpy.files import format_size
|
|
|
10
9
|
@click.command()
|
|
11
10
|
@click.option("--path", default=Path.cwd(), help="Path to the directory to clean")
|
|
12
11
|
def clean(path):
|
|
13
|
-
logging.basicConfig(level=logging.INFO)
|
|
14
12
|
path = Path(path)
|
|
15
|
-
|
|
13
|
+
click.echo(f"Cleaning {path}…")
|
|
16
14
|
total_freed_space = remove_pycache(path)
|
|
17
|
-
|
|
15
|
+
click.echo(f"{format_size(total_freed_space)} freed")
|