killpy 0.24.2__tar.gz → 0.25.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.24.2 → killpy-0.25.0}/PKG-INFO +47 -13
- {killpy-0.24.2 → killpy-0.25.0}/README.md +46 -12
- {killpy-0.24.2 → killpy-0.25.0}/killpy/__main__.py +18 -4
- {killpy-0.24.2 → killpy-0.25.0}/killpy/cleaner.py +55 -7
- killpy-0.25.0/killpy/cleaners/__init__.py +30 -0
- {killpy-0.24.2 → killpy-0.25.0}/killpy/cli.py +5 -0
- {killpy-0.24.2 → killpy-0.25.0}/killpy/commands/_utils.py +24 -0
- {killpy-0.24.2 → killpy-0.25.0}/killpy/commands/clean.py +7 -3
- {killpy-0.24.2 → killpy-0.25.0}/killpy/commands/delete.py +12 -2
- {killpy-0.24.2 → killpy-0.25.0}/killpy/detectors/cache.py +30 -2
- {killpy-0.24.2 → killpy-0.25.0}/killpy/detectors/conda.py +36 -9
- {killpy-0.24.2 → killpy-0.25.0}/killpy/detectors/hatch.py +17 -9
- {killpy-0.24.2 → killpy-0.25.0}/killpy/detectors/pipenv.py +5 -1
- {killpy-0.24.2 → killpy-0.25.0}/killpy/detectors/pipx.py +23 -5
- {killpy-0.24.2 → killpy-0.25.0}/killpy/detectors/poetry.py +17 -3
- {killpy-0.24.2 → killpy-0.25.0}/killpy/detectors/pyenv.py +12 -3
- killpy-0.25.0/killpy/files/__init__.py +30 -0
- {killpy-0.24.2 → killpy-0.25.0}/killpy/scanner.py +5 -4
- {killpy-0.24.2 → killpy-0.25.0}/killpy.egg-info/PKG-INFO +47 -13
- {killpy-0.24.2 → killpy-0.25.0}/pyproject.toml +1 -1
- killpy-0.24.2/killpy/cleaners/__init__.py +0 -15
- killpy-0.24.2/killpy/files/__init__.py +0 -23
- {killpy-0.24.2 → killpy-0.25.0}/killpy/__init__.py +0 -0
- {killpy-0.24.2 → killpy-0.25.0}/killpy/commands/__init__.py +0 -0
- {killpy-0.24.2 → killpy-0.25.0}/killpy/commands/doctor.py +0 -0
- {killpy-0.24.2 → killpy-0.25.0}/killpy/commands/find.py +0 -0
- {killpy-0.24.2 → killpy-0.25.0}/killpy/commands/list.py +0 -0
- {killpy-0.24.2 → killpy-0.25.0}/killpy/commands/stats.py +0 -0
- {killpy-0.24.2 → killpy-0.25.0}/killpy/detectors/__init__.py +0 -0
- {killpy-0.24.2 → killpy-0.25.0}/killpy/detectors/artifacts.py +0 -0
- {killpy-0.24.2 → killpy-0.25.0}/killpy/detectors/base.py +0 -0
- {killpy-0.24.2 → killpy-0.25.0}/killpy/detectors/tox.py +0 -0
- {killpy-0.24.2 → killpy-0.25.0}/killpy/detectors/uv.py +0 -0
- {killpy-0.24.2 → killpy-0.25.0}/killpy/detectors/venv.py +0 -0
- {killpy-0.24.2 → killpy-0.25.0}/killpy/intelligence/__init__.py +0 -0
- {killpy-0.24.2 → killpy-0.25.0}/killpy/intelligence/git_analyzer.py +0 -0
- {killpy-0.24.2 → killpy-0.25.0}/killpy/intelligence/scoring.py +0 -0
- {killpy-0.24.2 → killpy-0.25.0}/killpy/intelligence/suggestions.py +0 -0
- {killpy-0.24.2 → killpy-0.25.0}/killpy/intelligence/tracker.py +0 -0
- {killpy-0.24.2 → killpy-0.25.0}/killpy/models.py +0 -0
- {killpy-0.24.2 → killpy-0.25.0}/killpy.egg-info/SOURCES.txt +0 -0
- {killpy-0.24.2 → killpy-0.25.0}/killpy.egg-info/dependency_links.txt +0 -0
- {killpy-0.24.2 → killpy-0.25.0}/killpy.egg-info/entry_points.txt +0 -0
- {killpy-0.24.2 → killpy-0.25.0}/killpy.egg-info/requires.txt +0 -0
- {killpy-0.24.2 → killpy-0.25.0}/killpy.egg-info/top_level.txt +0 -0
- {killpy-0.24.2 → killpy-0.25.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.25.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/
|
|
@@ -44,7 +44,7 @@ uvx killpy --path ~
|
|
|
44
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
45
|
[](https://github.com/vinta/awesome-python)
|
|
46
46
|
|
|
47
|
-

|
|
48
48
|
|
|
49
49
|
</div>
|
|
50
50
|
|
|
@@ -62,6 +62,7 @@ ______________________________________________________________________
|
|
|
62
62
|
- [CLI reference](#cli-reference)
|
|
63
63
|
- [`killpy` — launch TUI or headless delete](#killpy--launch-tui-or-headless-delete)
|
|
64
64
|
- [`killpy list` — inspect environments](#killpy-list--inspect-environments)
|
|
65
|
+
- [`killpy find` — locate environments with a package](#killpy-find--locate-environments-with-a-package)
|
|
65
66
|
- [`killpy delete` — remove environments](#killpy-delete--remove-environments)
|
|
66
67
|
- [`killpy stats` — disk usage summary](#killpy-stats--disk-usage-summary)
|
|
67
68
|
- [`killpy clean` — remove cache directories](#killpy-clean--remove-cache-directories)
|
|
@@ -115,9 +116,9 @@ uvx killpy --path ~
|
|
|
115
116
|
| `pyenv` | pyenv-managed Python versions | `~/.pyenv/versions` |
|
|
116
117
|
| `pipenv` | Pipenv virtualenvs | `~/.local/share/virtualenvs` |
|
|
117
118
|
| `hatch` | Hatch environments | `~/.local/share/hatch/env` |
|
|
118
|
-
| `uv` | uv
|
|
119
|
+
| `uv` | uv tool environments (`uv tool install`) and uv-managed Pythons | `~/.local/share/uv` |
|
|
119
120
|
| `tox` | tox environments | `.tox/` inside repo |
|
|
120
|
-
| `cache` | `__pycache__`, `.mypy_cache`, `.pytest_cache`, `.ruff_cache`, global pip/uv caches |
|
|
121
|
+
| `cache` | `__pycache__`, `.mypy_cache`, `.pytest_cache`, `.ruff_cache`, plus global pip/uv caches when the scanned path contains them | project tree, `~/.cache` |
|
|
121
122
|
| `artifacts` | `dist/`, `build/`, `.egg-info`, `.dist-info` | project root |
|
|
122
123
|
|
|
123
124
|
______________________________________________________________________
|
|
@@ -180,7 +181,7 @@ killpy --path ~ --exclude "company-projects"
|
|
|
180
181
|
|
|
181
182
|
The TUI starts immediately and streams results as each detector finishes — no waiting for a full scan before you can start browsing. Select items, mark them, and confirm; **nothing is deleted without explicit action**.
|
|
182
183
|
|
|
183
|
-
Environments flagged with `⚠️` are actively in use
|
|
184
|
+
Environments flagged with `⚠️` are actively in use (the environment killpy runs from, or the pyenv global version) — the TUI shows them but refuses to delete them.
|
|
184
185
|
|
|
185
186
|
### Keyboard shortcuts
|
|
186
187
|
|
|
@@ -231,6 +232,8 @@ Options:
|
|
|
231
232
|
-D, --delete-all Scan and delete ALL found environments without
|
|
232
233
|
launching the TUI
|
|
233
234
|
-y, --yes Skip confirmation prompt (use with --delete-all)
|
|
235
|
+
--force With --delete-all: also delete environments
|
|
236
|
+
currently in use (⚠ system-critical)
|
|
234
237
|
--help Show this message and exit.
|
|
235
238
|
```
|
|
236
239
|
|
|
@@ -248,6 +251,8 @@ ______________________________________________________________________
|
|
|
248
251
|
|
|
249
252
|
### `killpy list` — inspect environments
|
|
250
253
|
|
|
254
|
+

|
|
255
|
+
|
|
251
256
|
```bash
|
|
252
257
|
killpy list # list all detected environments
|
|
253
258
|
killpy list --path ~/projects # scan a specific path
|
|
@@ -260,6 +265,8 @@ killpy list --quiet # suppress progress output (scripts/CI
|
|
|
260
265
|
|
|
261
266
|
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
267
|
|
|
268
|
+
The **Path** column mirrors the form of `--path`: a relative `--path` produces relative paths, an absolute one produces absolute paths (long paths are truncated to keep rows compact). The full absolute path is always available via `--json` / `--json-stream` in the `absolute_path` field.
|
|
269
|
+
|
|
263
270
|
`--json` example output:
|
|
264
271
|
|
|
265
272
|
```json
|
|
@@ -284,10 +291,31 @@ While scanning, `killpy list` shows a live progress indicator on **stderr** so y
|
|
|
284
291
|
killpy list --json-stream --path ~ | jq 'select(.type == "conda") | .size_human'
|
|
285
292
|
```
|
|
286
293
|
|
|
294
|
+

|
|
295
|
+
|
|
296
|
+
______________________________________________________________________
|
|
297
|
+
|
|
298
|
+
### `killpy find` — locate environments with a package
|
|
299
|
+
|
|
300
|
+

|
|
301
|
+
|
|
302
|
+
Find every environment that has a given package installed. `PACKAGE` accepts
|
|
303
|
+
standard PEP 508 / uv-style version specifiers.
|
|
304
|
+
|
|
305
|
+
```bash
|
|
306
|
+
killpy find requests # any version of requests
|
|
307
|
+
killpy find "flask>=1.0" # version specifier
|
|
308
|
+
killpy find "numpy>=1.24,<2.0" # combined constraints
|
|
309
|
+
killpy find requests --type venv # restrict to a type
|
|
310
|
+
killpy find requests --json # machine-readable output
|
|
311
|
+
```
|
|
312
|
+
|
|
287
313
|
______________________________________________________________________
|
|
288
314
|
|
|
289
315
|
### `killpy delete` — remove environments
|
|
290
316
|
|
|
317
|
+

|
|
318
|
+
|
|
291
319
|
```bash
|
|
292
320
|
killpy delete # interactive confirmation before delete
|
|
293
321
|
killpy delete --yes # skip confirmation
|
|
@@ -295,6 +323,7 @@ killpy delete --dry-run # preview — nothing is deleted
|
|
|
295
323
|
killpy delete --type venv # only a specific type
|
|
296
324
|
killpy delete --type venv --type cache # multiple types
|
|
297
325
|
killpy delete --older-than 180 --yes # delete stale envs, no prompt
|
|
326
|
+
killpy delete --force # include in-use (⚠) environments
|
|
298
327
|
killpy delete --path ~/projects
|
|
299
328
|
```
|
|
300
329
|
|
|
@@ -302,6 +331,8 @@ ______________________________________________________________________
|
|
|
302
331
|
|
|
303
332
|
### `killpy stats` — disk usage summary
|
|
304
333
|
|
|
334
|
+

|
|
335
|
+
|
|
305
336
|
```bash
|
|
306
337
|
killpy stats
|
|
307
338
|
killpy stats --path ~/projects
|
|
@@ -329,6 +360,8 @@ ______________________________________________________________________
|
|
|
329
360
|
|
|
330
361
|
### `killpy clean` — remove cache directories
|
|
331
362
|
|
|
363
|
+

|
|
364
|
+
|
|
332
365
|
```bash
|
|
333
366
|
killpy clean
|
|
334
367
|
killpy clean --path ~/projects
|
|
@@ -351,6 +384,8 @@ Options:
|
|
|
351
384
|
--help Show this message and exit.
|
|
352
385
|
```
|
|
353
386
|
|
|
387
|
+

|
|
388
|
+
|
|
354
389
|
`doctor` analyses every detected virtual environment in two phases:
|
|
355
390
|
|
|
356
391
|
**Phase 1 — Scoring (for sorting only)**
|
|
@@ -373,10 +408,9 @@ Category is assigned deterministically by applying the following rules in order:
|
|
|
373
408
|
| Priority | Rule | Category |
|
|
374
409
|
|----------|------|----------|
|
|
375
410
|
| 1 | Orphan (`is_orphan=True`) **and** `age ≥ 180 days` | `HIGH` |
|
|
376
|
-
| 2 |
|
|
377
|
-
| 3 |
|
|
378
|
-
| 4 |
|
|
379
|
-
| 5 | Fallback | `LOW` |
|
|
411
|
+
| 2 | Active git repository **or** `age < 120 days` | `LOW` |
|
|
412
|
+
| 3 | `age ≥ 120 days` | `MEDIUM` |
|
|
413
|
+
| 4 | Fallback | `LOW` |
|
|
380
414
|
|
|
381
415
|
Age and orphan status are the dominant signals. Size does not affect the category.
|
|
382
416
|
|
|
@@ -532,7 +566,7 @@ Or press `P` in the TUI to clean them for the scanned path.
|
|
|
532
566
|
|
|
533
567
|
**What does ⚠️ mean next to an environment?**
|
|
534
568
|
|
|
535
|
-
The environment is currently in use
|
|
569
|
+
The environment is currently in use — it is the environment killpy itself runs from, or your pyenv global version. killpy shows it so you are aware of it, but refuses to delete it: the TUI always skips it, and `killpy delete` / `--delete-all` skip it unless you pass `--force`.
|
|
536
570
|
|
|
537
571
|
**Does it fail if Conda, pipx or pyenv are not installed?**
|
|
538
572
|
|
|
@@ -562,7 +596,7 @@ ______________________________________________________________________
|
|
|
562
596
|
|
|
563
597
|
```yaml
|
|
564
598
|
- repo: https://github.com/Tlaloc-Es/KillPy
|
|
565
|
-
rev: 0.
|
|
599
|
+
rev: 0.24.1
|
|
566
600
|
hooks:
|
|
567
601
|
- id: killpy # remove __pycache__ on every commit
|
|
568
602
|
- id: killpy-clean-caches # also removes .mypy_cache, .pytest_cache, .ruff_cache
|
|
@@ -587,7 +621,7 @@ Typical minimal setup (safe for daily use):
|
|
|
587
621
|
|
|
588
622
|
```yaml
|
|
589
623
|
- repo: https://github.com/Tlaloc-Es/KillPy
|
|
590
|
-
rev: 0.
|
|
624
|
+
rev: 0.25.0
|
|
591
625
|
hooks:
|
|
592
626
|
- id: killpy
|
|
593
627
|
```
|
|
@@ -598,7 +632,7 @@ ______________________________________________________________________
|
|
|
598
632
|
|
|
599
633
|
## Safety
|
|
600
634
|
|
|
601
|
-
`killpy` performs **destructive, irreversible** actions. Always review the selection before confirming removal. The `--dry-run` flag lets you preview everything safely. Environments marked `⚠️` are actively in use and
|
|
635
|
+
`killpy` performs **destructive, irreversible** actions. Always review the selection before confirming removal. The `--dry-run` flag lets you preview everything safely. Environments marked `⚠️` are actively in use and are skipped by every delete path unless you explicitly pass `--force`.
|
|
602
636
|
|
|
603
637
|
**You are responsible for files deleted on your system.**
|
|
604
638
|
|
|
@@ -23,7 +23,7 @@ uvx killpy --path ~
|
|
|
23
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
24
|
[](https://github.com/vinta/awesome-python)
|
|
25
25
|
|
|
26
|
-

|
|
27
27
|
|
|
28
28
|
</div>
|
|
29
29
|
|
|
@@ -41,6 +41,7 @@ ______________________________________________________________________
|
|
|
41
41
|
- [CLI reference](#cli-reference)
|
|
42
42
|
- [`killpy` — launch TUI or headless delete](#killpy--launch-tui-or-headless-delete)
|
|
43
43
|
- [`killpy list` — inspect environments](#killpy-list--inspect-environments)
|
|
44
|
+
- [`killpy find` — locate environments with a package](#killpy-find--locate-environments-with-a-package)
|
|
44
45
|
- [`killpy delete` — remove environments](#killpy-delete--remove-environments)
|
|
45
46
|
- [`killpy stats` — disk usage summary](#killpy-stats--disk-usage-summary)
|
|
46
47
|
- [`killpy clean` — remove cache directories](#killpy-clean--remove-cache-directories)
|
|
@@ -94,9 +95,9 @@ uvx killpy --path ~
|
|
|
94
95
|
| `pyenv` | pyenv-managed Python versions | `~/.pyenv/versions` |
|
|
95
96
|
| `pipenv` | Pipenv virtualenvs | `~/.local/share/virtualenvs` |
|
|
96
97
|
| `hatch` | Hatch environments | `~/.local/share/hatch/env` |
|
|
97
|
-
| `uv` | uv
|
|
98
|
+
| `uv` | uv tool environments (`uv tool install`) and uv-managed Pythons | `~/.local/share/uv` |
|
|
98
99
|
| `tox` | tox environments | `.tox/` inside repo |
|
|
99
|
-
| `cache` | `__pycache__`, `.mypy_cache`, `.pytest_cache`, `.ruff_cache`, global pip/uv caches |
|
|
100
|
+
| `cache` | `__pycache__`, `.mypy_cache`, `.pytest_cache`, `.ruff_cache`, plus global pip/uv caches when the scanned path contains them | project tree, `~/.cache` |
|
|
100
101
|
| `artifacts` | `dist/`, `build/`, `.egg-info`, `.dist-info` | project root |
|
|
101
102
|
|
|
102
103
|
______________________________________________________________________
|
|
@@ -159,7 +160,7 @@ killpy --path ~ --exclude "company-projects"
|
|
|
159
160
|
|
|
160
161
|
The TUI starts immediately and streams results as each detector finishes — no waiting for a full scan before you can start browsing. Select items, mark them, and confirm; **nothing is deleted without explicit action**.
|
|
161
162
|
|
|
162
|
-
Environments flagged with `⚠️` are actively in use
|
|
163
|
+
Environments flagged with `⚠️` are actively in use (the environment killpy runs from, or the pyenv global version) — the TUI shows them but refuses to delete them.
|
|
163
164
|
|
|
164
165
|
### Keyboard shortcuts
|
|
165
166
|
|
|
@@ -210,6 +211,8 @@ Options:
|
|
|
210
211
|
-D, --delete-all Scan and delete ALL found environments without
|
|
211
212
|
launching the TUI
|
|
212
213
|
-y, --yes Skip confirmation prompt (use with --delete-all)
|
|
214
|
+
--force With --delete-all: also delete environments
|
|
215
|
+
currently in use (⚠ system-critical)
|
|
213
216
|
--help Show this message and exit.
|
|
214
217
|
```
|
|
215
218
|
|
|
@@ -227,6 +230,8 @@ ______________________________________________________________________
|
|
|
227
230
|
|
|
228
231
|
### `killpy list` — inspect environments
|
|
229
232
|
|
|
233
|
+

|
|
234
|
+
|
|
230
235
|
```bash
|
|
231
236
|
killpy list # list all detected environments
|
|
232
237
|
killpy list --path ~/projects # scan a specific path
|
|
@@ -239,6 +244,8 @@ killpy list --quiet # suppress progress output (scripts/CI
|
|
|
239
244
|
|
|
240
245
|
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
246
|
|
|
247
|
+
The **Path** column mirrors the form of `--path`: a relative `--path` produces relative paths, an absolute one produces absolute paths (long paths are truncated to keep rows compact). The full absolute path is always available via `--json` / `--json-stream` in the `absolute_path` field.
|
|
248
|
+
|
|
242
249
|
`--json` example output:
|
|
243
250
|
|
|
244
251
|
```json
|
|
@@ -263,10 +270,31 @@ While scanning, `killpy list` shows a live progress indicator on **stderr** so y
|
|
|
263
270
|
killpy list --json-stream --path ~ | jq 'select(.type == "conda") | .size_human'
|
|
264
271
|
```
|
|
265
272
|
|
|
273
|
+

|
|
274
|
+
|
|
275
|
+
______________________________________________________________________
|
|
276
|
+
|
|
277
|
+
### `killpy find` — locate environments with a package
|
|
278
|
+
|
|
279
|
+

|
|
280
|
+
|
|
281
|
+
Find every environment that has a given package installed. `PACKAGE` accepts
|
|
282
|
+
standard PEP 508 / uv-style version specifiers.
|
|
283
|
+
|
|
284
|
+
```bash
|
|
285
|
+
killpy find requests # any version of requests
|
|
286
|
+
killpy find "flask>=1.0" # version specifier
|
|
287
|
+
killpy find "numpy>=1.24,<2.0" # combined constraints
|
|
288
|
+
killpy find requests --type venv # restrict to a type
|
|
289
|
+
killpy find requests --json # machine-readable output
|
|
290
|
+
```
|
|
291
|
+
|
|
266
292
|
______________________________________________________________________
|
|
267
293
|
|
|
268
294
|
### `killpy delete` — remove environments
|
|
269
295
|
|
|
296
|
+

|
|
297
|
+
|
|
270
298
|
```bash
|
|
271
299
|
killpy delete # interactive confirmation before delete
|
|
272
300
|
killpy delete --yes # skip confirmation
|
|
@@ -274,6 +302,7 @@ killpy delete --dry-run # preview — nothing is deleted
|
|
|
274
302
|
killpy delete --type venv # only a specific type
|
|
275
303
|
killpy delete --type venv --type cache # multiple types
|
|
276
304
|
killpy delete --older-than 180 --yes # delete stale envs, no prompt
|
|
305
|
+
killpy delete --force # include in-use (⚠) environments
|
|
277
306
|
killpy delete --path ~/projects
|
|
278
307
|
```
|
|
279
308
|
|
|
@@ -281,6 +310,8 @@ ______________________________________________________________________
|
|
|
281
310
|
|
|
282
311
|
### `killpy stats` — disk usage summary
|
|
283
312
|
|
|
313
|
+

|
|
314
|
+
|
|
284
315
|
```bash
|
|
285
316
|
killpy stats
|
|
286
317
|
killpy stats --path ~/projects
|
|
@@ -308,6 +339,8 @@ ______________________________________________________________________
|
|
|
308
339
|
|
|
309
340
|
### `killpy clean` — remove cache directories
|
|
310
341
|
|
|
342
|
+

|
|
343
|
+
|
|
311
344
|
```bash
|
|
312
345
|
killpy clean
|
|
313
346
|
killpy clean --path ~/projects
|
|
@@ -330,6 +363,8 @@ Options:
|
|
|
330
363
|
--help Show this message and exit.
|
|
331
364
|
```
|
|
332
365
|
|
|
366
|
+

|
|
367
|
+
|
|
333
368
|
`doctor` analyses every detected virtual environment in two phases:
|
|
334
369
|
|
|
335
370
|
**Phase 1 — Scoring (for sorting only)**
|
|
@@ -352,10 +387,9 @@ Category is assigned deterministically by applying the following rules in order:
|
|
|
352
387
|
| Priority | Rule | Category |
|
|
353
388
|
|----------|------|----------|
|
|
354
389
|
| 1 | Orphan (`is_orphan=True`) **and** `age ≥ 180 days` | `HIGH` |
|
|
355
|
-
| 2 |
|
|
356
|
-
| 3 |
|
|
357
|
-
| 4 |
|
|
358
|
-
| 5 | Fallback | `LOW` |
|
|
390
|
+
| 2 | Active git repository **or** `age < 120 days` | `LOW` |
|
|
391
|
+
| 3 | `age ≥ 120 days` | `MEDIUM` |
|
|
392
|
+
| 4 | Fallback | `LOW` |
|
|
359
393
|
|
|
360
394
|
Age and orphan status are the dominant signals. Size does not affect the category.
|
|
361
395
|
|
|
@@ -511,7 +545,7 @@ Or press `P` in the TUI to clean them for the scanned path.
|
|
|
511
545
|
|
|
512
546
|
**What does ⚠️ mean next to an environment?**
|
|
513
547
|
|
|
514
|
-
The environment is currently in use
|
|
548
|
+
The environment is currently in use — it is the environment killpy itself runs from, or your pyenv global version. killpy shows it so you are aware of it, but refuses to delete it: the TUI always skips it, and `killpy delete` / `--delete-all` skip it unless you pass `--force`.
|
|
515
549
|
|
|
516
550
|
**Does it fail if Conda, pipx or pyenv are not installed?**
|
|
517
551
|
|
|
@@ -541,7 +575,7 @@ ______________________________________________________________________
|
|
|
541
575
|
|
|
542
576
|
```yaml
|
|
543
577
|
- repo: https://github.com/Tlaloc-Es/KillPy
|
|
544
|
-
rev: 0.
|
|
578
|
+
rev: 0.24.1
|
|
545
579
|
hooks:
|
|
546
580
|
- id: killpy # remove __pycache__ on every commit
|
|
547
581
|
- id: killpy-clean-caches # also removes .mypy_cache, .pytest_cache, .ruff_cache
|
|
@@ -566,7 +600,7 @@ Typical minimal setup (safe for daily use):
|
|
|
566
600
|
|
|
567
601
|
```yaml
|
|
568
602
|
- repo: https://github.com/Tlaloc-Es/KillPy
|
|
569
|
-
rev: 0.
|
|
603
|
+
rev: 0.25.0
|
|
570
604
|
hooks:
|
|
571
605
|
- id: killpy
|
|
572
606
|
```
|
|
@@ -577,7 +611,7 @@ ______________________________________________________________________
|
|
|
577
611
|
|
|
578
612
|
## Safety
|
|
579
613
|
|
|
580
|
-
`killpy` performs **destructive, irreversible** actions. Always review the selection before confirming removal. The `--dry-run` flag lets you preview everything safely. Environments marked `⚠️` are actively in use and
|
|
614
|
+
`killpy` performs **destructive, irreversible** actions. Always review the selection before confirming removal. The `--dry-run` flag lets you preview everything safely. Environments marked `⚠️` are actively in use and are skipped by every delete path unless you explicitly pass `--force`.
|
|
581
615
|
|
|
582
616
|
**You are responsible for files deleted on your system.**
|
|
583
617
|
|
|
@@ -9,6 +9,7 @@ from rich.prompt import Confirm
|
|
|
9
9
|
|
|
10
10
|
from killpy.cleaner import Cleaner, CleanerError
|
|
11
11
|
from killpy.cli import TableApp
|
|
12
|
+
from killpy.commands._utils import partition_in_use
|
|
12
13
|
from killpy.commands.clean import clean
|
|
13
14
|
from killpy.commands.delete import delete_cmd
|
|
14
15
|
from killpy.commands.doctor import doctor_cmd
|
|
@@ -20,11 +21,13 @@ from killpy.intelligence.tracker import UsageTracker
|
|
|
20
21
|
from killpy.scanner import Scanner
|
|
21
22
|
|
|
22
23
|
|
|
23
|
-
def _run_delete_all(
|
|
24
|
+
def _run_delete_all(
|
|
25
|
+
path: Path, excluded: set[str], yes: bool, force: bool = False
|
|
26
|
+
) -> None:
|
|
24
27
|
"""Scan and delete all discovered environments without launching the TUI."""
|
|
25
28
|
console = Console()
|
|
26
29
|
scanner = Scanner(excluded=excluded)
|
|
27
|
-
cleaner = Cleaner()
|
|
30
|
+
cleaner = Cleaner(force=force)
|
|
28
31
|
|
|
29
32
|
with Progress(
|
|
30
33
|
SpinnerColumn(),
|
|
@@ -34,6 +37,8 @@ def _run_delete_all(path: Path, excluded: set[str], yes: bool) -> None:
|
|
|
34
37
|
):
|
|
35
38
|
envs = scanner.scan(path)
|
|
36
39
|
|
|
40
|
+
envs = partition_in_use(envs, force, console)
|
|
41
|
+
|
|
37
42
|
if not envs:
|
|
38
43
|
console.print("[yellow]No environments found.[/yellow]")
|
|
39
44
|
return
|
|
@@ -116,15 +121,24 @@ def _run_delete_all(path: Path, excluded: set[str], yes: bool) -> None:
|
|
|
116
121
|
default=False,
|
|
117
122
|
help="Skip the confirmation prompt (use together with --delete-all).",
|
|
118
123
|
)
|
|
124
|
+
@click.option(
|
|
125
|
+
"--force",
|
|
126
|
+
is_flag=True,
|
|
127
|
+
default=False,
|
|
128
|
+
help=(
|
|
129
|
+
"With --delete-all: also delete environments currently in use "
|
|
130
|
+
"(flagged system-critical)."
|
|
131
|
+
),
|
|
132
|
+
)
|
|
119
133
|
@click.pass_context
|
|
120
|
-
def cli(ctx, path: Path, exclude: str, delete_all: bool, yes: bool):
|
|
134
|
+
def cli(ctx, path: Path, exclude: str, delete_all: bool, yes: bool, force: bool):
|
|
121
135
|
logging.basicConfig(level=logging.WARNING)
|
|
122
136
|
excluded = (
|
|
123
137
|
{p.strip() for p in exclude.split(",") if p.strip()} if exclude else set()
|
|
124
138
|
)
|
|
125
139
|
if not ctx.invoked_subcommand:
|
|
126
140
|
if delete_all:
|
|
127
|
-
_run_delete_all(path, excluded, yes)
|
|
141
|
+
_run_delete_all(path, excluded, yes, force)
|
|
128
142
|
else:
|
|
129
143
|
app = TableApp(root_dir=path, excluded=excluded)
|
|
130
144
|
app.run()
|
|
@@ -27,6 +27,10 @@ from killpy.models import Environment
|
|
|
27
27
|
|
|
28
28
|
logger = logging.getLogger(__name__)
|
|
29
29
|
|
|
30
|
+
# Minimum number of path components below the filesystem root for a
|
|
31
|
+
# deletion target: refuses e.g. /usr or /home, which are never environments.
|
|
32
|
+
_MIN_DEPTH_BELOW_ROOT = 2
|
|
33
|
+
|
|
30
34
|
|
|
31
35
|
class CleanerError(Exception):
|
|
32
36
|
"""Raised when a deletion operation fails."""
|
|
@@ -40,10 +44,15 @@ class Cleaner:
|
|
|
40
44
|
dry_run:
|
|
41
45
|
When ``True`` no destructive operations are performed; the method
|
|
42
46
|
still returns the would-be size so callers can show totals.
|
|
47
|
+
force:
|
|
48
|
+
When ``True``, allow deleting environments flagged
|
|
49
|
+
:attr:`~killpy.models.Environment.is_system_critical` (currently in
|
|
50
|
+
use). By default those are refused with a :class:`CleanerError`.
|
|
43
51
|
"""
|
|
44
52
|
|
|
45
|
-
def __init__(self, dry_run: bool = False) -> None:
|
|
53
|
+
def __init__(self, dry_run: bool = False, force: bool = False) -> None:
|
|
46
54
|
self.dry_run = dry_run
|
|
55
|
+
self.force = force
|
|
47
56
|
|
|
48
57
|
# ------------------------------------------------------------------ #
|
|
49
58
|
# Public API #
|
|
@@ -61,13 +70,22 @@ class Cleaner:
|
|
|
61
70
|
-------
|
|
62
71
|
int
|
|
63
72
|
Number of bytes freed (or that *would have been* freed in dry-run
|
|
64
|
-
mode).
|
|
73
|
+
mode). ``0`` when the path was already gone — e.g. it was
|
|
74
|
+
removed together with a previously deleted parent environment —
|
|
75
|
+
so callers don't over-report freed space.
|
|
65
76
|
|
|
66
77
|
Raises
|
|
67
78
|
------
|
|
68
79
|
CleanerError
|
|
69
|
-
If the underlying removal command fails
|
|
80
|
+
If the underlying removal command fails, or when *env* is
|
|
81
|
+
system-critical and :attr:`force` is ``False``.
|
|
70
82
|
"""
|
|
83
|
+
if env.is_system_critical and not self.force:
|
|
84
|
+
raise CleanerError(
|
|
85
|
+
f"Refusing to delete {env.path}: environment is currently in "
|
|
86
|
+
"use (system-critical). Pass --force to delete it anyway."
|
|
87
|
+
)
|
|
88
|
+
|
|
71
89
|
if self.dry_run:
|
|
72
90
|
logger.info("[dry-run] Would delete %s (%s)", env.path, env.size_human)
|
|
73
91
|
return env.size_bytes
|
|
@@ -79,8 +97,8 @@ class Cleaner:
|
|
|
79
97
|
self._remove_pipx(env.name)
|
|
80
98
|
elif env.managed_by == "uv":
|
|
81
99
|
self._remove_uv_tool(env.name)
|
|
82
|
-
|
|
83
|
-
|
|
100
|
+
elif not self._remove_filesystem(env.path):
|
|
101
|
+
return 0
|
|
84
102
|
except CleanerError:
|
|
85
103
|
raise
|
|
86
104
|
except Exception as exc: # noqa: BLE001
|
|
@@ -128,11 +146,41 @@ class Cleaner:
|
|
|
128
146
|
# ------------------------------------------------------------------ #
|
|
129
147
|
|
|
130
148
|
@staticmethod
|
|
131
|
-
def
|
|
149
|
+
def _ensure_sane_deletion_target(path: Path) -> None:
|
|
150
|
+
"""Refuse obviously wrong deletion targets (defense in depth).
|
|
151
|
+
|
|
152
|
+
The scanner should never produce these, but a bug upstream — or a
|
|
153
|
+
directory swapped underneath us between scan and delete — must not
|
|
154
|
+
translate into wiping the filesystem root, the user's home, or a
|
|
155
|
+
symlink's target tree. These checks are unconditional; ``force``
|
|
156
|
+
does not bypass them.
|
|
157
|
+
"""
|
|
158
|
+
if path.is_symlink():
|
|
159
|
+
raise CleanerError(
|
|
160
|
+
f"Refusing to delete {path}: it is a symlink (the "
|
|
161
|
+
"environment may have been replaced since it was scanned)"
|
|
162
|
+
)
|
|
163
|
+
try:
|
|
164
|
+
resolved = path.resolve()
|
|
165
|
+
except OSError as exc:
|
|
166
|
+
raise CleanerError(f"Cannot resolve {path}: {exc}") from exc
|
|
167
|
+
anchor = Path(resolved.anchor)
|
|
168
|
+
if resolved == anchor:
|
|
169
|
+
raise CleanerError(f"Refusing to delete the filesystem root: {resolved}")
|
|
170
|
+
if resolved == Path.home():
|
|
171
|
+
raise CleanerError(f"Refusing to delete the home directory: {resolved}")
|
|
172
|
+
if len(resolved.parts) - len(anchor.parts) < _MIN_DEPTH_BELOW_ROOT:
|
|
173
|
+
raise CleanerError(f"Refusing to delete top-level directory: {resolved}")
|
|
174
|
+
|
|
175
|
+
@staticmethod
|
|
176
|
+
def _remove_filesystem(path: Path) -> bool:
|
|
177
|
+
"""Remove *path* recursively; return False when it no longer exists."""
|
|
132
178
|
if not path.exists():
|
|
133
179
|
logger.warning("Path no longer exists: %s", path)
|
|
134
|
-
return
|
|
180
|
+
return False
|
|
181
|
+
Cleaner._ensure_sane_deletion_target(path)
|
|
135
182
|
shutil.rmtree(path)
|
|
183
|
+
return True
|
|
136
184
|
|
|
137
185
|
@staticmethod
|
|
138
186
|
def _remove_conda(env_name: str) -> None:
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import shutil
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from killpy.files import get_total_size
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def remove_pycache(path: Path) -> int:
|
|
9
|
+
"""Remove every ``__pycache__`` directory under *path*.
|
|
10
|
+
|
|
11
|
+
The walk never follows symlinks, so a link placed inside the tree
|
|
12
|
+
cannot steer the deletion outside the scanned root. Failed removals
|
|
13
|
+
are skipped and not counted as freed space.
|
|
14
|
+
"""
|
|
15
|
+
total_freed_space = 0
|
|
16
|
+
for current_root, directories, _files in os.walk(path, topdown=True):
|
|
17
|
+
if "__pycache__" not in directories:
|
|
18
|
+
continue
|
|
19
|
+
# Prune it from the walk: it is deleted below, not descended into.
|
|
20
|
+
directories.remove("__pycache__")
|
|
21
|
+
pycache_dir = Path(current_root) / "__pycache__"
|
|
22
|
+
if pycache_dir.is_symlink():
|
|
23
|
+
continue
|
|
24
|
+
try:
|
|
25
|
+
size = get_total_size(pycache_dir)
|
|
26
|
+
shutil.rmtree(pycache_dir)
|
|
27
|
+
except OSError:
|
|
28
|
+
continue
|
|
29
|
+
total_freed_space += size
|
|
30
|
+
return total_freed_space
|
|
@@ -717,6 +717,11 @@ class TableApp(App):
|
|
|
717
717
|
def delete_environment(self, environment: Environment) -> bool:
|
|
718
718
|
# Shared helper — no tab gating here: it is called from both the
|
|
719
719
|
# venv tab (delete actions) and the pipx tab (uninstall action).
|
|
720
|
+
if environment.is_system_critical:
|
|
721
|
+
self.query_one("#status-label", Label).update(
|
|
722
|
+
f"⚠️ Not deleted — currently in use: {environment.path}"
|
|
723
|
+
)
|
|
724
|
+
return False
|
|
720
725
|
try:
|
|
721
726
|
self.cleaner.delete(environment)
|
|
722
727
|
return True
|
|
@@ -4,6 +4,8 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
from datetime import datetime, timedelta, timezone
|
|
6
6
|
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
|
|
7
9
|
from killpy.models import Environment
|
|
8
10
|
|
|
9
11
|
# Maps user-facing type names (detector names) to the concrete ``Environment.type``
|
|
@@ -27,6 +29,28 @@ _TYPE_ALIASES: dict[str, frozenset[str]] = {
|
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
|
|
32
|
+
def partition_in_use(
|
|
33
|
+
envs: list[Environment], force: bool, console: Console
|
|
34
|
+
) -> list[Environment]:
|
|
35
|
+
"""Drop in-use (system-critical) environments unless *force*, reporting skips.
|
|
36
|
+
|
|
37
|
+
Returns the environments that are safe to delete. When *force* is
|
|
38
|
+
``True`` the list is returned unchanged.
|
|
39
|
+
"""
|
|
40
|
+
if force:
|
|
41
|
+
return envs
|
|
42
|
+
protected = [e for e in envs if e.is_system_critical]
|
|
43
|
+
if not protected:
|
|
44
|
+
return envs
|
|
45
|
+
console.print(
|
|
46
|
+
f"\n[yellow]Skipping {len(protected)} environment(s) currently in use"
|
|
47
|
+
" — pass --force to include them:[/yellow]"
|
|
48
|
+
)
|
|
49
|
+
for env in protected:
|
|
50
|
+
console.print(f" [dim]⚠ {env.path}[/dim]")
|
|
51
|
+
return [e for e in envs if not e.is_system_critical]
|
|
52
|
+
|
|
53
|
+
|
|
30
54
|
def filter_envs(
|
|
31
55
|
envs: list[Environment],
|
|
32
56
|
types: tuple[str, ...] | None,
|
|
@@ -7,9 +7,13 @@ from killpy.files import format_size
|
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
@click.command()
|
|
10
|
-
@click.option(
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
@click.option(
|
|
11
|
+
"--path",
|
|
12
|
+
default=Path.cwd,
|
|
13
|
+
type=click.Path(path_type=Path, exists=True, file_okay=False, dir_okay=True),
|
|
14
|
+
help="Path to the directory to clean",
|
|
15
|
+
)
|
|
16
|
+
def clean(path: Path):
|
|
13
17
|
click.echo(f"Cleaning {path}…")
|
|
14
18
|
total_freed_space = remove_pycache(path)
|
|
15
19
|
click.echo(f"{format_size(total_freed_space)} freed")
|