killpy 0.24.2__tar.gz → 0.25.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. {killpy-0.24.2/killpy.egg-info → killpy-0.25.1}/PKG-INFO +56 -14
  2. killpy-0.24.2/PKG-INFO → killpy-0.25.1/README.md +46 -33
  3. {killpy-0.24.2 → killpy-0.25.1}/killpy/__main__.py +24 -6
  4. {killpy-0.24.2 → killpy-0.25.1}/killpy/cleaner.py +55 -7
  5. killpy-0.25.1/killpy/cleaners/__init__.py +34 -0
  6. {killpy-0.24.2 → killpy-0.25.1}/killpy/cli.py +31 -24
  7. {killpy-0.24.2 → killpy-0.25.1}/killpy/commands/_utils.py +24 -0
  8. killpy-0.25.1/killpy/commands/clean.py +24 -0
  9. {killpy-0.24.2 → killpy-0.25.1}/killpy/commands/delete.py +12 -2
  10. {killpy-0.24.2 → killpy-0.25.1}/killpy/commands/doctor.py +40 -43
  11. {killpy-0.24.2 → killpy-0.25.1}/killpy/commands/list.py +2 -1
  12. {killpy-0.24.2 → killpy-0.25.1}/killpy/detectors/artifacts.py +10 -10
  13. killpy-0.25.1/killpy/detectors/base.py +101 -0
  14. {killpy-0.24.2 → killpy-0.25.1}/killpy/detectors/cache.py +49 -23
  15. {killpy-0.24.2 → killpy-0.25.1}/killpy/detectors/conda.py +37 -13
  16. {killpy-0.24.2 → killpy-0.25.1}/killpy/detectors/hatch.py +21 -12
  17. {killpy-0.24.2 → killpy-0.25.1}/killpy/detectors/pipenv.py +9 -4
  18. {killpy-0.24.2 → killpy-0.25.1}/killpy/detectors/pipx.py +24 -9
  19. {killpy-0.24.2 → killpy-0.25.1}/killpy/detectors/poetry.py +20 -5
  20. {killpy-0.24.2 → killpy-0.25.1}/killpy/detectors/pyenv.py +15 -5
  21. {killpy-0.24.2 → killpy-0.25.1}/killpy/detectors/tox.py +3 -7
  22. {killpy-0.24.2 → killpy-0.25.1}/killpy/detectors/uv.py +4 -7
  23. {killpy-0.24.2 → killpy-0.25.1}/killpy/detectors/venv.py +11 -11
  24. killpy-0.25.1/killpy/files/__init__.py +35 -0
  25. {killpy-0.24.2 → killpy-0.25.1}/killpy/intelligence/git_analyzer.py +12 -9
  26. {killpy-0.24.2 → killpy-0.25.1}/killpy/models.py +15 -3
  27. {killpy-0.24.2 → killpy-0.25.1}/killpy/scanner.py +5 -4
  28. killpy-0.24.2/README.md → killpy-0.25.1/killpy.egg-info/PKG-INFO +75 -12
  29. {killpy-0.24.2 → killpy-0.25.1}/pyproject.toml +26 -2
  30. killpy-0.24.2/killpy/cleaners/__init__.py +0 -15
  31. killpy-0.24.2/killpy/commands/clean.py +0 -15
  32. killpy-0.24.2/killpy/detectors/base.py +0 -53
  33. killpy-0.24.2/killpy/files/__init__.py +0 -23
  34. {killpy-0.24.2 → killpy-0.25.1}/killpy/__init__.py +0 -0
  35. {killpy-0.24.2 → killpy-0.25.1}/killpy/commands/__init__.py +0 -0
  36. {killpy-0.24.2 → killpy-0.25.1}/killpy/commands/find.py +0 -0
  37. {killpy-0.24.2 → killpy-0.25.1}/killpy/commands/stats.py +0 -0
  38. {killpy-0.24.2 → killpy-0.25.1}/killpy/detectors/__init__.py +0 -0
  39. {killpy-0.24.2 → killpy-0.25.1}/killpy/intelligence/__init__.py +0 -0
  40. {killpy-0.24.2 → killpy-0.25.1}/killpy/intelligence/scoring.py +0 -0
  41. {killpy-0.24.2 → killpy-0.25.1}/killpy/intelligence/suggestions.py +0 -0
  42. {killpy-0.24.2 → killpy-0.25.1}/killpy/intelligence/tracker.py +0 -0
  43. {killpy-0.24.2 → killpy-0.25.1}/killpy.egg-info/SOURCES.txt +0 -0
  44. {killpy-0.24.2 → killpy-0.25.1}/killpy.egg-info/dependency_links.txt +0 -0
  45. {killpy-0.24.2 → killpy-0.25.1}/killpy.egg-info/entry_points.txt +0 -0
  46. {killpy-0.24.2 → killpy-0.25.1}/killpy.egg-info/requires.txt +0 -0
  47. {killpy-0.24.2 → killpy-0.25.1}/killpy.egg-info/top_level.txt +0 -0
  48. {killpy-0.24.2 → killpy-0.25.1}/setup.cfg +0 -0
@@ -1,17 +1,25 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: killpy
3
- Version: 0.24.2
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 🧹.
3
+ Version: 0.25.1
4
+ Summary: Find and delete unused Python environments (.venv, conda, poetry, pipenv, pipx, pyenv, uv) and caches to free disk space from a fast terminal UI or a single CLI command.
5
5
  Project-URL: Homepage, https://github.com/Tlaloc-Es/killpy
6
6
  Project-URL: Documentation, https://tlaloc-es.github.io/killpy/
7
7
  Project-URL: Repository, https://github.com/Tlaloc-Es/killpy
8
8
  Project-URL: Bug Tracker, https://github.com/Tlaloc-Es/killpy/issues
9
+ Keywords: venv,virtualenv,conda,poetry,pipenv,pipx,pyenv,uv,cleanup,disk-space,cache,cli,tui,textual,developer-tools
10
+ Classifier: Development Status :: 4 - Beta
9
11
  Classifier: Environment :: Console
12
+ Classifier: Environment :: Console :: Curses
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: System Administrators
10
15
  Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
11
17
  Classifier: Programming Language :: Python :: 3.10
12
18
  Classifier: Programming Language :: Python :: 3.11
13
19
  Classifier: Programming Language :: Python :: 3.12
14
20
  Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: System :: Systems Administration
22
+ Classifier: Topic :: Utilities
15
23
  Requires-Python: >=3.10
16
24
  Description-Content-Type: text/markdown
17
25
  Requires-Dist: click>=8.1.8
@@ -44,7 +52,7 @@ uvx killpy --path ~
44
52
  [![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
53
  [![Awesome Python](https://awesome.re/badge.svg)](https://github.com/vinta/awesome-python)
46
54
 
47
- ![killpy in action](https://raw.githubusercontent.com/Tlaloc-Es/killpy/master/show.gif)
55
+ ![killpy in action](https://raw.githubusercontent.com/Tlaloc-Es/killpy/master/docs/gifs/demo.gif)
48
56
 
49
57
  </div>
50
58
 
@@ -62,6 +70,7 @@ ______________________________________________________________________
62
70
  - [CLI reference](#cli-reference)
63
71
  - [`killpy` — launch TUI or headless delete](#killpy--launch-tui-or-headless-delete)
64
72
  - [`killpy list` — inspect environments](#killpy-list--inspect-environments)
73
+ - [`killpy find` — locate environments with a package](#killpy-find--locate-environments-with-a-package)
65
74
  - [`killpy delete` — remove environments](#killpy-delete--remove-environments)
66
75
  - [`killpy stats` — disk usage summary](#killpy-stats--disk-usage-summary)
67
76
  - [`killpy clean` — remove cache directories](#killpy-clean--remove-cache-directories)
@@ -115,9 +124,9 @@ uvx killpy --path ~
115
124
  | `pyenv` | pyenv-managed Python versions | `~/.pyenv/versions` |
116
125
  | `pipenv` | Pipenv virtualenvs | `~/.local/share/virtualenvs` |
117
126
  | `hatch` | Hatch environments | `~/.local/share/hatch/env` |
118
- | `uv` | uv virtual environments | project root `.uv/` |
127
+ | `uv` | uv tool environments (`uv tool install`) and uv-managed Pythons | `~/.local/share/uv` |
119
128
  | `tox` | tox environments | `.tox/` inside repo |
120
- | `cache` | `__pycache__`, `.mypy_cache`, `.pytest_cache`, `.ruff_cache`, global pip/uv caches | everywhere |
129
+ | `cache` | `__pycache__`, `.mypy_cache`, `.pytest_cache`, `.ruff_cache`, plus global pip/uv caches when the scanned path contains them | project tree, `~/.cache` |
121
130
  | `artifacts` | `dist/`, `build/`, `.egg-info`, `.dist-info` | project root |
122
131
 
123
132
  ______________________________________________________________________
@@ -180,7 +189,7 @@ killpy --path ~ --exclude "company-projects"
180
189
 
181
190
  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
191
 
183
- Environments flagged with `⚠️` are actively in use by the current Python sessionkillpy will show them but they should not be deleted.
192
+ 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
193
 
185
194
  ### Keyboard shortcuts
186
195
 
@@ -231,6 +240,8 @@ Options:
231
240
  -D, --delete-all Scan and delete ALL found environments without
232
241
  launching the TUI
233
242
  -y, --yes Skip confirmation prompt (use with --delete-all)
243
+ --force With --delete-all: also delete environments
244
+ currently in use (⚠ system-critical)
234
245
  --help Show this message and exit.
235
246
  ```
236
247
 
@@ -248,6 +259,8 @@ ______________________________________________________________________
248
259
 
249
260
  ### `killpy list` — inspect environments
250
261
 
262
+ ![killpy list](https://raw.githubusercontent.com/Tlaloc-Es/killpy/master/docs/gifs/list.gif)
263
+
251
264
  ```bash
252
265
  killpy list # list all detected environments
253
266
  killpy list --path ~/projects # scan a specific path
@@ -260,6 +273,8 @@ killpy list --quiet # suppress progress output (scripts/CI
260
273
 
261
274
  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
275
 
276
+ 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.
277
+
263
278
  `--json` example output:
264
279
 
265
280
  ```json
@@ -284,10 +299,31 @@ While scanning, `killpy list` shows a live progress indicator on **stderr** so y
284
299
  killpy list --json-stream --path ~ | jq 'select(.type == "conda") | .size_human'
285
300
  ```
286
301
 
302
+ ![killpy list --json](https://raw.githubusercontent.com/Tlaloc-Es/killpy/master/docs/gifs/list-json.gif)
303
+
304
+ ______________________________________________________________________
305
+
306
+ ### `killpy find` — locate environments with a package
307
+
308
+ ![killpy find](https://raw.githubusercontent.com/Tlaloc-Es/killpy/master/docs/gifs/find.gif)
309
+
310
+ Find every environment that has a given package installed. `PACKAGE` accepts
311
+ standard PEP 508 / uv-style version specifiers.
312
+
313
+ ```bash
314
+ killpy find requests # any version of requests
315
+ killpy find "flask>=1.0" # version specifier
316
+ killpy find "numpy>=1.24,<2.0" # combined constraints
317
+ killpy find requests --type venv # restrict to a type
318
+ killpy find requests --json # machine-readable output
319
+ ```
320
+
287
321
  ______________________________________________________________________
288
322
 
289
323
  ### `killpy delete` — remove environments
290
324
 
325
+ ![killpy delete](https://raw.githubusercontent.com/Tlaloc-Es/killpy/master/docs/gifs/delete.gif)
326
+
291
327
  ```bash
292
328
  killpy delete # interactive confirmation before delete
293
329
  killpy delete --yes # skip confirmation
@@ -295,6 +331,7 @@ killpy delete --dry-run # preview — nothing is deleted
295
331
  killpy delete --type venv # only a specific type
296
332
  killpy delete --type venv --type cache # multiple types
297
333
  killpy delete --older-than 180 --yes # delete stale envs, no prompt
334
+ killpy delete --force # include in-use (⚠) environments
298
335
  killpy delete --path ~/projects
299
336
  ```
300
337
 
@@ -302,6 +339,8 @@ ______________________________________________________________________
302
339
 
303
340
  ### `killpy stats` — disk usage summary
304
341
 
342
+ ![killpy stats](https://raw.githubusercontent.com/Tlaloc-Es/killpy/master/docs/gifs/stats.gif)
343
+
305
344
  ```bash
306
345
  killpy stats
307
346
  killpy stats --path ~/projects
@@ -329,6 +368,8 @@ ______________________________________________________________________
329
368
 
330
369
  ### `killpy clean` — remove cache directories
331
370
 
371
+ ![killpy clean](https://raw.githubusercontent.com/Tlaloc-Es/killpy/master/docs/gifs/clean.gif)
372
+
332
373
  ```bash
333
374
  killpy clean
334
375
  killpy clean --path ~/projects
@@ -351,6 +392,8 @@ Options:
351
392
  --help Show this message and exit.
352
393
  ```
353
394
 
395
+ ![killpy doctor](https://raw.githubusercontent.com/Tlaloc-Es/killpy/master/docs/gifs/doctor.gif)
396
+
354
397
  `doctor` analyses every detected virtual environment in two phases:
355
398
 
356
399
  **Phase 1 — Scoring (for sorting only)**
@@ -373,10 +416,9 @@ Category is assigned deterministically by applying the following rules in order:
373
416
  | Priority | Rule | Category |
374
417
  |----------|------|----------|
375
418
  | 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` |
419
+ | 2 | Active git repository **or** `age < 120 days` | `LOW` |
420
+ | 3 | `age 120 days` | `MEDIUM` |
421
+ | 4 | Fallback | `LOW` |
380
422
 
381
423
  Age and orphan status are the dominant signals. Size does not affect the category.
382
424
 
@@ -532,7 +574,7 @@ Or press `P` in the TUI to clean them for the scanned path.
532
574
 
533
575
  **What does ⚠️ mean next to an environment?**
534
576
 
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.
577
+ 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
578
 
537
579
  **Does it fail if Conda, pipx or pyenv are not installed?**
538
580
 
@@ -562,7 +604,7 @@ ______________________________________________________________________
562
604
 
563
605
  ```yaml
564
606
  - repo: https://github.com/Tlaloc-Es/KillPy
565
- rev: 0.20.0
607
+ rev: 0.25.1
566
608
  hooks:
567
609
  - id: killpy # remove __pycache__ on every commit
568
610
  - id: killpy-clean-caches # also removes .mypy_cache, .pytest_cache, .ruff_cache
@@ -587,7 +629,7 @@ Typical minimal setup (safe for daily use):
587
629
 
588
630
  ```yaml
589
631
  - repo: https://github.com/Tlaloc-Es/KillPy
590
- rev: 0.24.2
632
+ rev: 0.25.1
591
633
  hooks:
592
634
  - id: killpy
593
635
  ```
@@ -598,7 +640,7 @@ ______________________________________________________________________
598
640
 
599
641
  ## Safety
600
642
 
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.
643
+ `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
644
 
603
645
  **You are responsible for files deleted on your system.**
604
646
 
@@ -1,24 +1,3 @@
1
- Metadata-Version: 2.4
2
- Name: killpy
3
- Version: 0.24.2
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
- Project-URL: Homepage, https://github.com/Tlaloc-Es/killpy
6
- Project-URL: Documentation, https://tlaloc-es.github.io/killpy/
7
- Project-URL: Repository, https://github.com/Tlaloc-Es/killpy
8
- Project-URL: Bug Tracker, https://github.com/Tlaloc-Es/killpy/issues
9
- Classifier: Environment :: Console
10
- Classifier: License :: OSI Approved :: MIT License
11
- Classifier: Programming Language :: Python :: 3.10
12
- Classifier: Programming Language :: Python :: 3.11
13
- Classifier: Programming Language :: Python :: 3.12
14
- Classifier: Programming Language :: Python :: 3.13
15
- Requires-Python: >=3.10
16
- Description-Content-Type: text/markdown
17
- Requires-Dist: click>=8.1.8
18
- Requires-Dist: packaging>=24.0
19
- Requires-Dist: rich>=13.9.4
20
- Requires-Dist: textual>=1.0.0
21
-
22
1
  <div align="center">
23
2
 
24
3
  ![Logo](https://raw.githubusercontent.com/Tlaloc-Es/killpy/master/logo.png)
@@ -44,7 +23,7 @@ uvx killpy --path ~
44
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>)
45
24
  [![Awesome Python](https://awesome.re/badge.svg)](https://github.com/vinta/awesome-python)
46
25
 
47
- ![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)
48
27
 
49
28
  </div>
50
29
 
@@ -62,6 +41,7 @@ ______________________________________________________________________
62
41
  - [CLI reference](#cli-reference)
63
42
  - [`killpy` — launch TUI or headless delete](#killpy--launch-tui-or-headless-delete)
64
43
  - [`killpy list` — inspect environments](#killpy-list--inspect-environments)
44
+ - [`killpy find` — locate environments with a package](#killpy-find--locate-environments-with-a-package)
65
45
  - [`killpy delete` — remove environments](#killpy-delete--remove-environments)
66
46
  - [`killpy stats` — disk usage summary](#killpy-stats--disk-usage-summary)
67
47
  - [`killpy clean` — remove cache directories](#killpy-clean--remove-cache-directories)
@@ -115,9 +95,9 @@ uvx killpy --path ~
115
95
  | `pyenv` | pyenv-managed Python versions | `~/.pyenv/versions` |
116
96
  | `pipenv` | Pipenv virtualenvs | `~/.local/share/virtualenvs` |
117
97
  | `hatch` | Hatch environments | `~/.local/share/hatch/env` |
118
- | `uv` | uv virtual environments | project root `.uv/` |
98
+ | `uv` | uv tool environments (`uv tool install`) and uv-managed Pythons | `~/.local/share/uv` |
119
99
  | `tox` | tox environments | `.tox/` inside repo |
120
- | `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` |
121
101
  | `artifacts` | `dist/`, `build/`, `.egg-info`, `.dist-info` | project root |
122
102
 
123
103
  ______________________________________________________________________
@@ -180,7 +160,7 @@ killpy --path ~ --exclude "company-projects"
180
160
 
181
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**.
182
162
 
183
- 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.
184
164
 
185
165
  ### Keyboard shortcuts
186
166
 
@@ -231,6 +211,8 @@ Options:
231
211
  -D, --delete-all Scan and delete ALL found environments without
232
212
  launching the TUI
233
213
  -y, --yes Skip confirmation prompt (use with --delete-all)
214
+ --force With --delete-all: also delete environments
215
+ currently in use (⚠ system-critical)
234
216
  --help Show this message and exit.
235
217
  ```
236
218
 
@@ -248,6 +230,8 @@ ______________________________________________________________________
248
230
 
249
231
  ### `killpy list` — inspect environments
250
232
 
233
+ ![killpy list](https://raw.githubusercontent.com/Tlaloc-Es/killpy/master/docs/gifs/list.gif)
234
+
251
235
  ```bash
252
236
  killpy list # list all detected environments
253
237
  killpy list --path ~/projects # scan a specific path
@@ -260,6 +244,8 @@ killpy list --quiet # suppress progress output (scripts/CI
260
244
 
261
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.
262
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
+
263
249
  `--json` example output:
264
250
 
265
251
  ```json
@@ -284,10 +270,31 @@ While scanning, `killpy list` shows a live progress indicator on **stderr** so y
284
270
  killpy list --json-stream --path ~ | jq 'select(.type == "conda") | .size_human'
285
271
  ```
286
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
+
287
292
  ______________________________________________________________________
288
293
 
289
294
  ### `killpy delete` — remove environments
290
295
 
296
+ ![killpy delete](https://raw.githubusercontent.com/Tlaloc-Es/killpy/master/docs/gifs/delete.gif)
297
+
291
298
  ```bash
292
299
  killpy delete # interactive confirmation before delete
293
300
  killpy delete --yes # skip confirmation
@@ -295,6 +302,7 @@ killpy delete --dry-run # preview — nothing is deleted
295
302
  killpy delete --type venv # only a specific type
296
303
  killpy delete --type venv --type cache # multiple types
297
304
  killpy delete --older-than 180 --yes # delete stale envs, no prompt
305
+ killpy delete --force # include in-use (⚠) environments
298
306
  killpy delete --path ~/projects
299
307
  ```
300
308
 
@@ -302,6 +310,8 @@ ______________________________________________________________________
302
310
 
303
311
  ### `killpy stats` — disk usage summary
304
312
 
313
+ ![killpy stats](https://raw.githubusercontent.com/Tlaloc-Es/killpy/master/docs/gifs/stats.gif)
314
+
305
315
  ```bash
306
316
  killpy stats
307
317
  killpy stats --path ~/projects
@@ -329,6 +339,8 @@ ______________________________________________________________________
329
339
 
330
340
  ### `killpy clean` — remove cache directories
331
341
 
342
+ ![killpy clean](https://raw.githubusercontent.com/Tlaloc-Es/killpy/master/docs/gifs/clean.gif)
343
+
332
344
  ```bash
333
345
  killpy clean
334
346
  killpy clean --path ~/projects
@@ -351,6 +363,8 @@ Options:
351
363
  --help Show this message and exit.
352
364
  ```
353
365
 
366
+ ![killpy doctor](https://raw.githubusercontent.com/Tlaloc-Es/killpy/master/docs/gifs/doctor.gif)
367
+
354
368
  `doctor` analyses every detected virtual environment in two phases:
355
369
 
356
370
  **Phase 1 — Scoring (for sorting only)**
@@ -373,10 +387,9 @@ Category is assigned deterministically by applying the following rules in order:
373
387
  | Priority | Rule | Category |
374
388
  |----------|------|----------|
375
389
  | 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` |
390
+ | 2 | Active git repository **or** `age < 120 days` | `LOW` |
391
+ | 3 | `age 120 days` | `MEDIUM` |
392
+ | 4 | Fallback | `LOW` |
380
393
 
381
394
  Age and orphan status are the dominant signals. Size does not affect the category.
382
395
 
@@ -532,7 +545,7 @@ Or press `P` in the TUI to clean them for the scanned path.
532
545
 
533
546
  **What does ⚠️ mean next to an environment?**
534
547
 
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.
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`.
536
549
 
537
550
  **Does it fail if Conda, pipx or pyenv are not installed?**
538
551
 
@@ -562,7 +575,7 @@ ______________________________________________________________________
562
575
 
563
576
  ```yaml
564
577
  - repo: https://github.com/Tlaloc-Es/KillPy
565
- rev: 0.20.0
578
+ rev: 0.25.1
566
579
  hooks:
567
580
  - id: killpy # remove __pycache__ on every commit
568
581
  - id: killpy-clean-caches # also removes .mypy_cache, .pytest_cache, .ruff_cache
@@ -587,7 +600,7 @@ Typical minimal setup (safe for daily use):
587
600
 
588
601
  ```yaml
589
602
  - repo: https://github.com/Tlaloc-Es/KillPy
590
- rev: 0.24.2
603
+ rev: 0.25.1
591
604
  hooks:
592
605
  - id: killpy
593
606
  ```
@@ -598,7 +611,7 @@ ______________________________________________________________________
598
611
 
599
612
  ## Safety
600
613
 
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.
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`.
602
615
 
603
616
  **You are responsible for files deleted on your system.**
604
617
 
@@ -1,3 +1,7 @@
1
+ """killpy CLI entry point: the ``cli`` click group and its subcommands."""
2
+
3
+ from __future__ import annotations
4
+
1
5
  import logging
2
6
  import sys
3
7
  from pathlib import Path
@@ -9,7 +13,8 @@ from rich.prompt import Confirm
9
13
 
10
14
  from killpy.cleaner import Cleaner, CleanerError
11
15
  from killpy.cli import TableApp
12
- from killpy.commands.clean import clean
16
+ from killpy.commands._utils import partition_in_use
17
+ from killpy.commands.clean import clean_cmd
13
18
  from killpy.commands.delete import delete_cmd
14
19
  from killpy.commands.doctor import doctor_cmd
15
20
  from killpy.commands.find import find_cmd
@@ -20,11 +25,13 @@ from killpy.intelligence.tracker import UsageTracker
20
25
  from killpy.scanner import Scanner
21
26
 
22
27
 
23
- def _run_delete_all(path: Path, excluded: set[str], yes: bool) -> None:
28
+ def _run_delete_all(
29
+ path: Path, excluded: set[str], yes: bool, force: bool = False
30
+ ) -> None:
24
31
  """Scan and delete all discovered environments without launching the TUI."""
25
32
  console = Console()
26
33
  scanner = Scanner(excluded=excluded)
27
- cleaner = Cleaner()
34
+ cleaner = Cleaner(force=force)
28
35
 
29
36
  with Progress(
30
37
  SpinnerColumn(),
@@ -34,6 +41,8 @@ def _run_delete_all(path: Path, excluded: set[str], yes: bool) -> None:
34
41
  ):
35
42
  envs = scanner.scan(path)
36
43
 
44
+ envs = partition_in_use(envs, force, console)
45
+
37
46
  if not envs:
38
47
  console.print("[yellow]No environments found.[/yellow]")
39
48
  return
@@ -116,21 +125,30 @@ def _run_delete_all(path: Path, excluded: set[str], yes: bool) -> None:
116
125
  default=False,
117
126
  help="Skip the confirmation prompt (use together with --delete-all).",
118
127
  )
128
+ @click.option(
129
+ "--force",
130
+ is_flag=True,
131
+ default=False,
132
+ help=(
133
+ "With --delete-all: also delete environments currently in use "
134
+ "(flagged system-critical)."
135
+ ),
136
+ )
119
137
  @click.pass_context
120
- def cli(ctx, path: Path, exclude: str, delete_all: bool, yes: bool):
138
+ def cli(ctx, path: Path, exclude: str, delete_all: bool, yes: bool, force: bool):
121
139
  logging.basicConfig(level=logging.WARNING)
122
140
  excluded = (
123
141
  {p.strip() for p in exclude.split(",") if p.strip()} if exclude else set()
124
142
  )
125
143
  if not ctx.invoked_subcommand:
126
144
  if delete_all:
127
- _run_delete_all(path, excluded, yes)
145
+ _run_delete_all(path, excluded, yes, force)
128
146
  else:
129
147
  app = TableApp(root_dir=path, excluded=excluded)
130
148
  app.run()
131
149
 
132
150
 
133
- cli.add_command(clean)
151
+ cli.add_command(clean_cmd, name="clean")
134
152
  cli.add_command(list_cmd, name="list")
135
153
  cli.add_command(delete_cmd, name="delete")
136
154
  cli.add_command(stats_cmd, name="stats")
@@ -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,34 @@
1
+ """Stateless ``__pycache__`` removal helper used by ``killpy clean``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import shutil
7
+ from pathlib import Path
8
+
9
+ from killpy.files import get_total_size
10
+
11
+
12
+ def remove_pycache(path: Path) -> int:
13
+ """Remove every ``__pycache__`` directory under *path*.
14
+
15
+ The walk never follows symlinks, so a link placed inside the tree
16
+ cannot steer the deletion outside the scanned root. Failed removals
17
+ are skipped and not counted as freed space.
18
+ """
19
+ total_freed_space = 0
20
+ for current_root, directories, _files in os.walk(path, topdown=True):
21
+ if "__pycache__" not in directories:
22
+ continue
23
+ # Prune it from the walk: it is deleted below, not descended into.
24
+ directories.remove("__pycache__")
25
+ pycache_dir = Path(current_root) / "__pycache__"
26
+ if pycache_dir.is_symlink():
27
+ continue
28
+ try:
29
+ size = get_total_size(pycache_dir)
30
+ shutil.rmtree(pycache_dir)
31
+ except OSError:
32
+ continue
33
+ total_freed_space += size
34
+ return total_freed_space