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.
Files changed (46) hide show
  1. {killpy-0.24.2 → killpy-0.25.0}/PKG-INFO +47 -13
  2. {killpy-0.24.2 → killpy-0.25.0}/README.md +46 -12
  3. {killpy-0.24.2 → killpy-0.25.0}/killpy/__main__.py +18 -4
  4. {killpy-0.24.2 → killpy-0.25.0}/killpy/cleaner.py +55 -7
  5. killpy-0.25.0/killpy/cleaners/__init__.py +30 -0
  6. {killpy-0.24.2 → killpy-0.25.0}/killpy/cli.py +5 -0
  7. {killpy-0.24.2 → killpy-0.25.0}/killpy/commands/_utils.py +24 -0
  8. {killpy-0.24.2 → killpy-0.25.0}/killpy/commands/clean.py +7 -3
  9. {killpy-0.24.2 → killpy-0.25.0}/killpy/commands/delete.py +12 -2
  10. {killpy-0.24.2 → killpy-0.25.0}/killpy/detectors/cache.py +30 -2
  11. {killpy-0.24.2 → killpy-0.25.0}/killpy/detectors/conda.py +36 -9
  12. {killpy-0.24.2 → killpy-0.25.0}/killpy/detectors/hatch.py +17 -9
  13. {killpy-0.24.2 → killpy-0.25.0}/killpy/detectors/pipenv.py +5 -1
  14. {killpy-0.24.2 → killpy-0.25.0}/killpy/detectors/pipx.py +23 -5
  15. {killpy-0.24.2 → killpy-0.25.0}/killpy/detectors/poetry.py +17 -3
  16. {killpy-0.24.2 → killpy-0.25.0}/killpy/detectors/pyenv.py +12 -3
  17. killpy-0.25.0/killpy/files/__init__.py +30 -0
  18. {killpy-0.24.2 → killpy-0.25.0}/killpy/scanner.py +5 -4
  19. {killpy-0.24.2 → killpy-0.25.0}/killpy.egg-info/PKG-INFO +47 -13
  20. {killpy-0.24.2 → killpy-0.25.0}/pyproject.toml +1 -1
  21. killpy-0.24.2/killpy/cleaners/__init__.py +0 -15
  22. killpy-0.24.2/killpy/files/__init__.py +0 -23
  23. {killpy-0.24.2 → killpy-0.25.0}/killpy/__init__.py +0 -0
  24. {killpy-0.24.2 → killpy-0.25.0}/killpy/commands/__init__.py +0 -0
  25. {killpy-0.24.2 → killpy-0.25.0}/killpy/commands/doctor.py +0 -0
  26. {killpy-0.24.2 → killpy-0.25.0}/killpy/commands/find.py +0 -0
  27. {killpy-0.24.2 → killpy-0.25.0}/killpy/commands/list.py +0 -0
  28. {killpy-0.24.2 → killpy-0.25.0}/killpy/commands/stats.py +0 -0
  29. {killpy-0.24.2 → killpy-0.25.0}/killpy/detectors/__init__.py +0 -0
  30. {killpy-0.24.2 → killpy-0.25.0}/killpy/detectors/artifacts.py +0 -0
  31. {killpy-0.24.2 → killpy-0.25.0}/killpy/detectors/base.py +0 -0
  32. {killpy-0.24.2 → killpy-0.25.0}/killpy/detectors/tox.py +0 -0
  33. {killpy-0.24.2 → killpy-0.25.0}/killpy/detectors/uv.py +0 -0
  34. {killpy-0.24.2 → killpy-0.25.0}/killpy/detectors/venv.py +0 -0
  35. {killpy-0.24.2 → killpy-0.25.0}/killpy/intelligence/__init__.py +0 -0
  36. {killpy-0.24.2 → killpy-0.25.0}/killpy/intelligence/git_analyzer.py +0 -0
  37. {killpy-0.24.2 → killpy-0.25.0}/killpy/intelligence/scoring.py +0 -0
  38. {killpy-0.24.2 → killpy-0.25.0}/killpy/intelligence/suggestions.py +0 -0
  39. {killpy-0.24.2 → killpy-0.25.0}/killpy/intelligence/tracker.py +0 -0
  40. {killpy-0.24.2 → killpy-0.25.0}/killpy/models.py +0 -0
  41. {killpy-0.24.2 → killpy-0.25.0}/killpy.egg-info/SOURCES.txt +0 -0
  42. {killpy-0.24.2 → killpy-0.25.0}/killpy.egg-info/dependency_links.txt +0 -0
  43. {killpy-0.24.2 → killpy-0.25.0}/killpy.egg-info/entry_points.txt +0 -0
  44. {killpy-0.24.2 → killpy-0.25.0}/killpy.egg-info/requires.txt +0 -0
  45. {killpy-0.24.2 → killpy-0.25.0}/killpy.egg-info/top_level.txt +0 -0
  46. {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.24.2
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
  [![Tweet](https://img.shields.io/twitter/url/http/shields.io.svg?style=social)](<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
  [![Awesome Python](https://awesome.re/badge.svg)](https://github.com/vinta/awesome-python)
46
46
 
47
- ![killpy in action](https://raw.githubusercontent.com/Tlaloc-Es/killpy/master/show.gif)
47
+ ![killpy in action](https://raw.githubusercontent.com/Tlaloc-Es/killpy/master/docs/gifs/demo.gif)
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 virtual environments | project root `.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 | everywhere |
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 by the current Python sessionkillpy will show them but they should not be deleted.
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
+ ![killpy list](https://raw.githubusercontent.com/Tlaloc-Es/killpy/master/docs/gifs/list.gif)
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
+ ![killpy list --json](https://raw.githubusercontent.com/Tlaloc-Es/killpy/master/docs/gifs/list-json.gif)
295
+
296
+ ______________________________________________________________________
297
+
298
+ ### `killpy find` — locate environments with a package
299
+
300
+ ![killpy find](https://raw.githubusercontent.com/Tlaloc-Es/killpy/master/docs/gifs/find.gif)
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
+ ![killpy delete](https://raw.githubusercontent.com/Tlaloc-Es/killpy/master/docs/gifs/delete.gif)
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
+ ![killpy stats](https://raw.githubusercontent.com/Tlaloc-Es/killpy/master/docs/gifs/stats.gif)
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
+ ![killpy clean](https://raw.githubusercontent.com/Tlaloc-Es/killpy/master/docs/gifs/clean.gif)
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
+ ![killpy doctor](https://raw.githubusercontent.com/Tlaloc-Es/killpy/master/docs/gifs/doctor.gif)
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 | No project files **and** `age 365 days` | `HIGH` |
377
- | 3 | Active git repository **or** `age < 120 days` | `LOW` |
378
- | 4 | `age ≥ 120 days` | `MEDIUM` |
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 by the Python session running killpy. It should not be deleted. killpy will still show it so you are aware of it, but treat it with care.
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.20.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.24.2
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 should not be deleted.
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
  [![Tweet](https://img.shields.io/twitter/url/http/shields.io.svg?style=social)](<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
  [![Awesome Python](https://awesome.re/badge.svg)](https://github.com/vinta/awesome-python)
25
25
 
26
- ![killpy in action](https://raw.githubusercontent.com/Tlaloc-Es/killpy/master/show.gif)
26
+ ![killpy in action](https://raw.githubusercontent.com/Tlaloc-Es/killpy/master/docs/gifs/demo.gif)
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 virtual environments | project root `.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 | everywhere |
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 by the current Python sessionkillpy will show them but they should not be deleted.
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
+ ![killpy list](https://raw.githubusercontent.com/Tlaloc-Es/killpy/master/docs/gifs/list.gif)
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
+ ![killpy list --json](https://raw.githubusercontent.com/Tlaloc-Es/killpy/master/docs/gifs/list-json.gif)
274
+
275
+ ______________________________________________________________________
276
+
277
+ ### `killpy find` — locate environments with a package
278
+
279
+ ![killpy find](https://raw.githubusercontent.com/Tlaloc-Es/killpy/master/docs/gifs/find.gif)
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
+ ![killpy delete](https://raw.githubusercontent.com/Tlaloc-Es/killpy/master/docs/gifs/delete.gif)
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
+ ![killpy stats](https://raw.githubusercontent.com/Tlaloc-Es/killpy/master/docs/gifs/stats.gif)
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
+ ![killpy clean](https://raw.githubusercontent.com/Tlaloc-Es/killpy/master/docs/gifs/clean.gif)
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
+ ![killpy doctor](https://raw.githubusercontent.com/Tlaloc-Es/killpy/master/docs/gifs/doctor.gif)
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 | No project files **and** `age 365 days` | `HIGH` |
356
- | 3 | Active git repository **or** `age < 120 days` | `LOW` |
357
- | 4 | `age ≥ 120 days` | `MEDIUM` |
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 by the Python session running killpy. It should not be deleted. killpy will still show it so you are aware of it, but treat it with care.
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.20.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.24.2
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 should not be deleted.
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(path: Path, excluded: set[str], yes: bool) -> None:
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
- else:
83
- self._remove_filesystem(env.path)
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 _remove_filesystem(path: Path) -> None:
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("--path", default=Path.cwd(), help="Path to the directory to clean")
11
- def clean(path):
12
- path = Path(path)
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")