killpy 0.22.0__tar.gz → 0.24.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 (44) hide show
  1. {killpy-0.22.0 → killpy-0.24.0}/PKG-INFO +37 -3
  2. {killpy-0.22.0 → killpy-0.24.0}/README.md +35 -2
  3. {killpy-0.22.0 → killpy-0.24.0}/killpy/__main__.py +2 -0
  4. killpy-0.24.0/killpy/commands/find.py +197 -0
  5. {killpy-0.22.0 → killpy-0.24.0}/killpy/commands/list.py +76 -30
  6. {killpy-0.22.0 → killpy-0.24.0}/killpy.egg-info/PKG-INFO +37 -3
  7. {killpy-0.22.0 → killpy-0.24.0}/killpy.egg-info/SOURCES.txt +1 -0
  8. {killpy-0.22.0 → killpy-0.24.0}/killpy.egg-info/requires.txt +1 -0
  9. {killpy-0.22.0 → killpy-0.24.0}/pyproject.toml +7 -2
  10. {killpy-0.22.0 → killpy-0.24.0}/killpy/__init__.py +0 -0
  11. {killpy-0.22.0 → killpy-0.24.0}/killpy/cleaner.py +0 -0
  12. {killpy-0.22.0 → killpy-0.24.0}/killpy/cleaners/__init__.py +0 -0
  13. {killpy-0.22.0 → killpy-0.24.0}/killpy/cli.py +0 -0
  14. {killpy-0.22.0 → killpy-0.24.0}/killpy/commands/__init__.py +0 -0
  15. {killpy-0.22.0 → killpy-0.24.0}/killpy/commands/_utils.py +0 -0
  16. {killpy-0.22.0 → killpy-0.24.0}/killpy/commands/clean.py +0 -0
  17. {killpy-0.22.0 → killpy-0.24.0}/killpy/commands/delete.py +0 -0
  18. {killpy-0.22.0 → killpy-0.24.0}/killpy/commands/doctor.py +0 -0
  19. {killpy-0.22.0 → killpy-0.24.0}/killpy/commands/stats.py +0 -0
  20. {killpy-0.22.0 → killpy-0.24.0}/killpy/detectors/__init__.py +0 -0
  21. {killpy-0.22.0 → killpy-0.24.0}/killpy/detectors/artifacts.py +0 -0
  22. {killpy-0.22.0 → killpy-0.24.0}/killpy/detectors/base.py +0 -0
  23. {killpy-0.22.0 → killpy-0.24.0}/killpy/detectors/cache.py +0 -0
  24. {killpy-0.22.0 → killpy-0.24.0}/killpy/detectors/conda.py +0 -0
  25. {killpy-0.22.0 → killpy-0.24.0}/killpy/detectors/hatch.py +0 -0
  26. {killpy-0.22.0 → killpy-0.24.0}/killpy/detectors/pipenv.py +0 -0
  27. {killpy-0.22.0 → killpy-0.24.0}/killpy/detectors/pipx.py +0 -0
  28. {killpy-0.22.0 → killpy-0.24.0}/killpy/detectors/poetry.py +0 -0
  29. {killpy-0.22.0 → killpy-0.24.0}/killpy/detectors/pyenv.py +0 -0
  30. {killpy-0.22.0 → killpy-0.24.0}/killpy/detectors/tox.py +0 -0
  31. {killpy-0.22.0 → killpy-0.24.0}/killpy/detectors/uv.py +0 -0
  32. {killpy-0.22.0 → killpy-0.24.0}/killpy/detectors/venv.py +0 -0
  33. {killpy-0.22.0 → killpy-0.24.0}/killpy/files/__init__.py +0 -0
  34. {killpy-0.22.0 → killpy-0.24.0}/killpy/intelligence/__init__.py +0 -0
  35. {killpy-0.22.0 → killpy-0.24.0}/killpy/intelligence/git_analyzer.py +0 -0
  36. {killpy-0.22.0 → killpy-0.24.0}/killpy/intelligence/scoring.py +0 -0
  37. {killpy-0.22.0 → killpy-0.24.0}/killpy/intelligence/suggestions.py +0 -0
  38. {killpy-0.22.0 → killpy-0.24.0}/killpy/intelligence/tracker.py +0 -0
  39. {killpy-0.22.0 → killpy-0.24.0}/killpy/models.py +0 -0
  40. {killpy-0.22.0 → killpy-0.24.0}/killpy/scanner.py +0 -0
  41. {killpy-0.22.0 → killpy-0.24.0}/killpy.egg-info/dependency_links.txt +0 -0
  42. {killpy-0.22.0 → killpy-0.24.0}/killpy.egg-info/entry_points.txt +0 -0
  43. {killpy-0.22.0 → killpy-0.24.0}/killpy.egg-info/top_level.txt +0 -0
  44. {killpy-0.22.0 → killpy-0.24.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: killpy
3
- Version: 0.22.0
3
+ Version: 0.24.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/
@@ -15,6 +15,7 @@ Classifier: Programming Language :: Python :: 3.13
15
15
  Requires-Python: >=3.10
16
16
  Description-Content-Type: text/markdown
17
17
  Requires-Dist: click>=8.1.8
18
+ Requires-Dist: packaging>=24.0
18
19
  Requires-Dist: rich>=13.9.4
19
20
  Requires-Dist: textual>=1.0.0
20
21
 
@@ -46,6 +47,31 @@ uvx killpy --path ~
46
47
 
47
48
  ______________________________________________________________________
48
49
 
50
+ ## Table of Contents
51
+
52
+ - [The Problem](#the-problem)
53
+ - [What killpy detects](#what-killpy-detects)
54
+ - [Quickstart](#quickstart)
55
+ - [Interactive TUI](#interactive-tui)
56
+ - [Keyboard shortcuts](#keyboard-shortcuts)
57
+ - [Search / filter](#search--filter)
58
+ - [Multi-select mode](#multi-select-mode)
59
+ - [CLI reference](#cli-reference)
60
+ - [`killpy` — launch TUI or headless delete](#killpy--launch-tui-or-headless-delete)
61
+ - [`killpy list` — inspect environments](#killpy-list--inspect-environments)
62
+ - [`killpy delete` — remove environments](#killpy-delete--remove-environments)
63
+ - [`killpy stats` — disk usage summary](#killpy-stats--disk-usage-summary)
64
+ - [`killpy clean` — remove cache directories](#killpy-clean--remove-cache-directories)
65
+ - [`killpy doctor` — smart health report](#killpy-doctor--smart-health-report)
66
+ - [killpy vs alternatives](#killpy-vs-alternatives)
67
+ - [FAQ](#faq)
68
+ - [Pre-commit hooks](#pre-commit-hooks)
69
+ - [Safety](#safety)
70
+ - [Roadmap](#roadmap)
71
+ - [Contributing](#contributing)
72
+
73
+ ______________________________________________________________________
74
+
49
75
  ## The Problem
50
76
 
51
77
  If you have been writing Python for more than a year, your disk is probably full of environments you forgot about.
@@ -226,8 +252,11 @@ killpy list --type venv --type conda # filter by type (repeatable)
226
252
  killpy list --older-than 90 # not accessed in the last 90 days
227
253
  killpy list --json # output as a JSON array
228
254
  killpy list --json-stream # stream as NDJSON — one line per env
255
+ killpy list --quiet # suppress progress output (scripts/CI)
229
256
  ```
230
257
 
258
+ 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.
259
+
231
260
  `--json` example output:
232
261
 
233
262
  ```json
@@ -429,9 +458,12 @@ Deletes every `.venv` / `pyvenv.cfg` env not accessed in the last 90 days, witho
429
458
  **How do I use killpy in a CI pipeline or script?**
430
459
 
431
460
  ```bash
432
- # List as machine-readable JSON
461
+ # List as machine-readable JSON (progress goes to stderr, JSON to stdout)
433
462
  killpy list --json
434
463
 
464
+ # Suppress progress output entirely with --quiet / -q
465
+ killpy list --json --quiet | jq '.[] | .size_human'
466
+
435
467
  # Stream results as NDJSON in real time
436
468
  killpy list --json-stream | jq '.size_bytes'
437
469
 
@@ -439,6 +471,8 @@ killpy list --json-stream | jq '.size_bytes'
439
471
  killpy --path ./build_artifacts --delete-all --yes
440
472
  ```
441
473
 
474
+ Progress messages always go to **stderr**, so stdout is clean for piping even without `--quiet`. Use `-q` when you want no output at all on stderr.
475
+
442
476
  **How do I skip certain directories?**
443
477
 
444
478
  ```bash
@@ -550,7 +584,7 @@ Typical minimal setup (safe for daily use):
550
584
 
551
585
  ```yaml
552
586
  - repo: https://github.com/Tlaloc-Es/KillPy
553
- rev: 0.22.0
587
+ rev: 0.24.0
554
588
  hooks:
555
589
  - id: killpy
556
590
  ```
@@ -26,6 +26,31 @@ uvx killpy --path ~
26
26
 
27
27
  ______________________________________________________________________
28
28
 
29
+ ## Table of Contents
30
+
31
+ - [The Problem](#the-problem)
32
+ - [What killpy detects](#what-killpy-detects)
33
+ - [Quickstart](#quickstart)
34
+ - [Interactive TUI](#interactive-tui)
35
+ - [Keyboard shortcuts](#keyboard-shortcuts)
36
+ - [Search / filter](#search--filter)
37
+ - [Multi-select mode](#multi-select-mode)
38
+ - [CLI reference](#cli-reference)
39
+ - [`killpy` — launch TUI or headless delete](#killpy--launch-tui-or-headless-delete)
40
+ - [`killpy list` — inspect environments](#killpy-list--inspect-environments)
41
+ - [`killpy delete` — remove environments](#killpy-delete--remove-environments)
42
+ - [`killpy stats` — disk usage summary](#killpy-stats--disk-usage-summary)
43
+ - [`killpy clean` — remove cache directories](#killpy-clean--remove-cache-directories)
44
+ - [`killpy doctor` — smart health report](#killpy-doctor--smart-health-report)
45
+ - [killpy vs alternatives](#killpy-vs-alternatives)
46
+ - [FAQ](#faq)
47
+ - [Pre-commit hooks](#pre-commit-hooks)
48
+ - [Safety](#safety)
49
+ - [Roadmap](#roadmap)
50
+ - [Contributing](#contributing)
51
+
52
+ ______________________________________________________________________
53
+
29
54
  ## The Problem
30
55
 
31
56
  If you have been writing Python for more than a year, your disk is probably full of environments you forgot about.
@@ -206,8 +231,11 @@ killpy list --type venv --type conda # filter by type (repeatable)
206
231
  killpy list --older-than 90 # not accessed in the last 90 days
207
232
  killpy list --json # output as a JSON array
208
233
  killpy list --json-stream # stream as NDJSON — one line per env
234
+ killpy list --quiet # suppress progress output (scripts/CI)
209
235
  ```
210
236
 
237
+ 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.
238
+
211
239
  `--json` example output:
212
240
 
213
241
  ```json
@@ -409,9 +437,12 @@ Deletes every `.venv` / `pyvenv.cfg` env not accessed in the last 90 days, witho
409
437
  **How do I use killpy in a CI pipeline or script?**
410
438
 
411
439
  ```bash
412
- # List as machine-readable JSON
440
+ # List as machine-readable JSON (progress goes to stderr, JSON to stdout)
413
441
  killpy list --json
414
442
 
443
+ # Suppress progress output entirely with --quiet / -q
444
+ killpy list --json --quiet | jq '.[] | .size_human'
445
+
415
446
  # Stream results as NDJSON in real time
416
447
  killpy list --json-stream | jq '.size_bytes'
417
448
 
@@ -419,6 +450,8 @@ killpy list --json-stream | jq '.size_bytes'
419
450
  killpy --path ./build_artifacts --delete-all --yes
420
451
  ```
421
452
 
453
+ Progress messages always go to **stderr**, so stdout is clean for piping even without `--quiet`. Use `-q` when you want no output at all on stderr.
454
+
422
455
  **How do I skip certain directories?**
423
456
 
424
457
  ```bash
@@ -530,7 +563,7 @@ Typical minimal setup (safe for daily use):
530
563
 
531
564
  ```yaml
532
565
  - repo: https://github.com/Tlaloc-Es/KillPy
533
- rev: 0.22.0
566
+ rev: 0.24.0
534
567
  hooks:
535
568
  - id: killpy
536
569
  ```
@@ -11,6 +11,7 @@ from killpy.cli import TableApp
11
11
  from killpy.commands.clean import clean
12
12
  from killpy.commands.delete import delete_cmd
13
13
  from killpy.commands.doctor import doctor_cmd
14
+ from killpy.commands.find import find_cmd
14
15
  from killpy.commands.list import list_cmd
15
16
  from killpy.commands.stats import stats_cmd
16
17
  from killpy.files import format_size
@@ -120,6 +121,7 @@ cli.add_command(list_cmd, name="list")
120
121
  cli.add_command(delete_cmd, name="delete")
121
122
  cli.add_command(stats_cmd, name="stats")
122
123
  cli.add_command(doctor_cmd, name="doctor")
124
+ cli.add_command(find_cmd, name="find")
123
125
 
124
126
 
125
127
  if __name__ == "__main__":
@@ -0,0 +1,197 @@
1
+ """``killpy find`` – locate environments that have a specific package installed."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+
8
+ import click
9
+ from packaging.requirements import Requirement
10
+ from packaging.version import InvalidVersion
11
+ from rich.console import Console
12
+ from rich.table import Table
13
+
14
+ from killpy.commands._utils import filter_envs
15
+ from killpy.scanner import Scanner
16
+
17
+ # ---------------------------------------------------------------------------
18
+ # Helpers: read installed-package metadata from a venv directory
19
+ # ---------------------------------------------------------------------------
20
+
21
+
22
+ def _site_packages(env_path: Path) -> list[Path]:
23
+ """Return all site-packages directories found inside *env_path*.
24
+
25
+ Supports the Unix layout (``lib/python3.x/site-packages``) and the
26
+ Windows layout (``Lib/site-packages``).
27
+ """
28
+ result: list[Path] = []
29
+
30
+ # Unix: lib/python<ver>/site-packages
31
+ lib = env_path / "lib"
32
+ if lib.is_dir():
33
+ for child in lib.iterdir():
34
+ sp = child / "site-packages"
35
+ if sp.is_dir():
36
+ result.append(sp)
37
+
38
+ # Windows: Lib/site-packages
39
+ win_sp = env_path / "Lib" / "site-packages"
40
+ if win_sp.is_dir():
41
+ result.append(win_sp)
42
+
43
+ return result
44
+
45
+
46
+ def _read_metadata_field(metadata_path: Path, field: str) -> str | None:
47
+ """Return the value of a single RFC 822-style *field* from *metadata_path*."""
48
+ prefix = f"{field}:"
49
+ try:
50
+ with metadata_path.open(encoding="utf-8", errors="replace") as fh:
51
+ for line in fh:
52
+ if line.startswith(prefix):
53
+ return line[len(prefix) :].strip()
54
+ # RFC 822 headers end at the first blank line.
55
+ if not line.strip():
56
+ break
57
+ except OSError:
58
+ pass
59
+ return None
60
+
61
+
62
+ def _normalise_name(name: str) -> str:
63
+ """Normalise a distribution name to a lowercase, underscore form."""
64
+ return name.lower().replace("-", "_").replace(".", "_")
65
+
66
+
67
+ def installed_packages(env_path: Path) -> dict[str, str]:
68
+ """Return ``{normalised_name: version}`` for every package installed in *env_path*.
69
+
70
+ Reads ``*.dist-info/METADATA`` files from all site-packages directories
71
+ found under the environment root. Non-venv paths (e.g. conda envs created
72
+ with ``--prefix``) are supported as long as they follow the same layout.
73
+ """
74
+ packages: dict[str, str] = {}
75
+ for sp in _site_packages(env_path):
76
+ for dist_info in sp.glob("*.dist-info"):
77
+ metadata = dist_info / "METADATA"
78
+ if not metadata.exists():
79
+ continue
80
+ name = _read_metadata_field(metadata, "Name")
81
+ version = _read_metadata_field(metadata, "Version")
82
+ if name and version:
83
+ packages[_normalise_name(name)] = version
84
+ return packages
85
+
86
+
87
+ def package_version_match(
88
+ packages: dict[str, str], requirement: Requirement
89
+ ) -> str | None:
90
+ """Return the installed version string if it satisfies *requirement*, else ``None``.""" # noqa: E501
91
+ norm_name = _normalise_name(requirement.name)
92
+ installed = packages.get(norm_name)
93
+ if installed is None:
94
+ return None
95
+ try:
96
+ if requirement.specifier.contains(installed, prereleases=True):
97
+ return installed
98
+ except InvalidVersion:
99
+ pass
100
+ return None
101
+
102
+
103
+ # ---------------------------------------------------------------------------
104
+ # Click command
105
+ # ---------------------------------------------------------------------------
106
+
107
+
108
+ @click.command("find")
109
+ @click.argument("package")
110
+ @click.option(
111
+ "--path",
112
+ default=Path.cwd,
113
+ type=click.Path(path_type=Path, exists=True, file_okay=False, dir_okay=True),
114
+ help="Root directory to scan.",
115
+ )
116
+ @click.option(
117
+ "--type",
118
+ "types",
119
+ multiple=True,
120
+ metavar="TYPE",
121
+ help=(
122
+ "Limit the scan to these environment types (repeatable). "
123
+ "Example: --type venv --type conda"
124
+ ),
125
+ )
126
+ @click.option(
127
+ "--json",
128
+ "as_json",
129
+ is_flag=True,
130
+ default=False,
131
+ help="Output as a JSON array.",
132
+ )
133
+ def find_cmd(
134
+ package: str,
135
+ path: Path,
136
+ types: tuple[str, ...],
137
+ as_json: bool,
138
+ ) -> None:
139
+ """Find environments that have PACKAGE installed.
140
+
141
+ PACKAGE accepts standard PEP 508 / uv-style version specifiers:
142
+
143
+ \b
144
+ killpy find requests
145
+ killpy find "requests>=2.28"
146
+ killpy find "numpy>=1.24,<2.0"
147
+ killpy find "django==4.2.*"
148
+ killpy find "scipy~=1.11"
149
+ """
150
+ try:
151
+ req = Requirement(package)
152
+ except Exception as exc: # packaging.requirements.InvalidRequirement
153
+ raise click.BadParameter(str(exc), param_hint="PACKAGE") from exc
154
+
155
+ scanner = Scanner(types=set(types) if types else None)
156
+ envs = scanner.scan(path)
157
+ envs = filter_envs(envs, types or None, None)
158
+
159
+ matches: list[tuple] = [] # (Environment, version_string)
160
+ for env in envs:
161
+ pkgs = installed_packages(env.path)
162
+ version = package_version_match(pkgs, req)
163
+ if version is not None:
164
+ matches.append((env, version))
165
+
166
+ if as_json:
167
+ click.echo(
168
+ json.dumps(
169
+ [{**env.to_dict(), "matched_version": ver} for env, ver in matches],
170
+ indent=2,
171
+ )
172
+ )
173
+ return
174
+
175
+ console = Console()
176
+
177
+ if not matches:
178
+ console.print(
179
+ f"[yellow]No environments found with[/yellow] "
180
+ f"[bold]{package}[/bold] installed."
181
+ )
182
+ return
183
+
184
+ table = Table(title=f"Environments with {package!r}", show_lines=False)
185
+ table.add_column("Path", style="cyan")
186
+ table.add_column("Type", style="magenta")
187
+ table.add_column("Installed version", style="green")
188
+ table.add_column("Env size", style="yellow", justify="right")
189
+
190
+ for env, ver in matches:
191
+ table.add_row(str(env.path), env.type, ver, env.size_human)
192
+
193
+ console.print(table)
194
+ console.print(
195
+ f"\n[bold]{len(matches)}[/bold] environment(s) match "
196
+ f"[bold cyan]{package}[/bold cyan]."
197
+ )
@@ -14,6 +14,70 @@ from killpy.files import format_size
14
14
  from killpy.scanner import Scanner
15
15
 
16
16
 
17
+ def _run_json_stream(
18
+ scanner: Scanner,
19
+ path: Path,
20
+ types: tuple[str, ...],
21
+ older_than: int | None,
22
+ quiet: bool,
23
+ stderr_console: Console,
24
+ ) -> None:
25
+ if not quiet:
26
+ stderr_console.print("[dim]Scanning…[/dim]")
27
+
28
+ def _progress(detector, envs):
29
+ if not quiet:
30
+ stderr_console.print(
31
+ f"[dim] {detector.name}[/dim] — [dim]{len(envs)} found[/dim]",
32
+ )
33
+ for env in filter_envs(envs, types or None, older_than):
34
+ click.echo(json.dumps(env.to_dict()))
35
+
36
+ scanner.scan(path, on_progress=_progress)
37
+
38
+
39
+ def _scan_with_progress(
40
+ scanner: Scanner, path: Path, quiet: bool, stderr_console: Console
41
+ ):
42
+ if quiet:
43
+ return scanner.scan(path)
44
+
45
+ status = stderr_console.status("Scanning…", spinner="dots")
46
+ status.start()
47
+
48
+ def _progress(detector, _envs):
49
+ status.update(f"Scanning… [dim]{detector.name}[/dim]")
50
+
51
+ envs = scanner.scan(path, on_progress=_progress)
52
+ status.stop()
53
+ return envs
54
+
55
+
56
+ def _print_table(envs: list, console: Console) -> None:
57
+ table = Table(show_header=True, header_style="bold cyan")
58
+ table.add_column("Type", style="dim", min_width=10)
59
+ table.add_column("Name", min_width=20)
60
+ table.add_column("Last accessed", min_width=12)
61
+ table.add_column("Size", justify="right", min_width=9)
62
+ table.add_column("Path")
63
+
64
+ for env in envs:
65
+ table.add_row(
66
+ env.type,
67
+ env.name,
68
+ env.last_accessed_str,
69
+ env.size_human,
70
+ str(env.path),
71
+ )
72
+
73
+ console.print(table)
74
+ total = sum(e.size_bytes for e in envs)
75
+ console.print(
76
+ f"\n[bold]{len(envs)}[/bold] environment(s) — "
77
+ f"[bold]{format_size(total)}[/bold] total"
78
+ )
79
+
80
+
17
81
  @click.command("list")
18
82
  @click.option(
19
83
  "--path",
@@ -52,26 +116,30 @@ from killpy.scanner import Scanner
52
116
  default=False,
53
117
  help="Output as JSON array.",
54
118
  )
119
+ @click.option(
120
+ "--quiet",
121
+ "-q",
122
+ is_flag=True,
123
+ default=False,
124
+ help="Suppress progress messages (useful in scripts/pipelines).",
125
+ )
55
126
  def list_cmd(
56
127
  path: Path,
57
128
  types: tuple[str, ...],
58
129
  older_than: int | None,
59
130
  as_json: bool,
60
131
  as_json_stream: bool,
132
+ quiet: bool,
61
133
  ) -> None:
62
134
  """List all detected Python environments under PATH."""
63
135
  scanner = Scanner(types=set(types) if types else None)
136
+ stderr_console = Console(stderr=True)
64
137
 
65
138
  if as_json_stream:
66
-
67
- def _stream_progress(_detector, envs):
68
- for env in filter_envs(envs, types or None, older_than):
69
- click.echo(json.dumps(env.to_dict()))
70
-
71
- scanner.scan(path, on_progress=_stream_progress)
139
+ _run_json_stream(scanner, path, types, older_than, quiet, stderr_console)
72
140
  return
73
141
 
74
- envs = scanner.scan(path)
142
+ envs = _scan_with_progress(scanner, path, quiet, stderr_console)
75
143
  envs = filter_envs(envs, types or None, older_than)
76
144
 
77
145
  if as_json:
@@ -82,26 +150,4 @@ def list_cmd(
82
150
  click.echo("No environments found.")
83
151
  return
84
152
 
85
- console = Console()
86
- table = Table(show_header=True, header_style="bold cyan")
87
- table.add_column("Type", style="dim", min_width=10)
88
- table.add_column("Name", min_width=20)
89
- table.add_column("Last accessed", min_width=12)
90
- table.add_column("Size", justify="right", min_width=9)
91
- table.add_column("Path")
92
-
93
- for env in envs:
94
- table.add_row(
95
- env.type,
96
- env.name,
97
- env.last_accessed_str,
98
- env.size_human,
99
- str(env.path),
100
- )
101
-
102
- console.print(table)
103
- total = sum(e.size_bytes for e in envs)
104
- console.print(
105
- f"\n[bold]{len(envs)}[/bold] environment(s) — "
106
- f"[bold]{format_size(total)}[/bold] total"
107
- )
153
+ _print_table(envs, Console())
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: killpy
3
- Version: 0.22.0
3
+ Version: 0.24.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/
@@ -15,6 +15,7 @@ Classifier: Programming Language :: Python :: 3.13
15
15
  Requires-Python: >=3.10
16
16
  Description-Content-Type: text/markdown
17
17
  Requires-Dist: click>=8.1.8
18
+ Requires-Dist: packaging>=24.0
18
19
  Requires-Dist: rich>=13.9.4
19
20
  Requires-Dist: textual>=1.0.0
20
21
 
@@ -46,6 +47,31 @@ uvx killpy --path ~
46
47
 
47
48
  ______________________________________________________________________
48
49
 
50
+ ## Table of Contents
51
+
52
+ - [The Problem](#the-problem)
53
+ - [What killpy detects](#what-killpy-detects)
54
+ - [Quickstart](#quickstart)
55
+ - [Interactive TUI](#interactive-tui)
56
+ - [Keyboard shortcuts](#keyboard-shortcuts)
57
+ - [Search / filter](#search--filter)
58
+ - [Multi-select mode](#multi-select-mode)
59
+ - [CLI reference](#cli-reference)
60
+ - [`killpy` — launch TUI or headless delete](#killpy--launch-tui-or-headless-delete)
61
+ - [`killpy list` — inspect environments](#killpy-list--inspect-environments)
62
+ - [`killpy delete` — remove environments](#killpy-delete--remove-environments)
63
+ - [`killpy stats` — disk usage summary](#killpy-stats--disk-usage-summary)
64
+ - [`killpy clean` — remove cache directories](#killpy-clean--remove-cache-directories)
65
+ - [`killpy doctor` — smart health report](#killpy-doctor--smart-health-report)
66
+ - [killpy vs alternatives](#killpy-vs-alternatives)
67
+ - [FAQ](#faq)
68
+ - [Pre-commit hooks](#pre-commit-hooks)
69
+ - [Safety](#safety)
70
+ - [Roadmap](#roadmap)
71
+ - [Contributing](#contributing)
72
+
73
+ ______________________________________________________________________
74
+
49
75
  ## The Problem
50
76
 
51
77
  If you have been writing Python for more than a year, your disk is probably full of environments you forgot about.
@@ -226,8 +252,11 @@ killpy list --type venv --type conda # filter by type (repeatable)
226
252
  killpy list --older-than 90 # not accessed in the last 90 days
227
253
  killpy list --json # output as a JSON array
228
254
  killpy list --json-stream # stream as NDJSON — one line per env
255
+ killpy list --quiet # suppress progress output (scripts/CI)
229
256
  ```
230
257
 
258
+ 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.
259
+
231
260
  `--json` example output:
232
261
 
233
262
  ```json
@@ -429,9 +458,12 @@ Deletes every `.venv` / `pyvenv.cfg` env not accessed in the last 90 days, witho
429
458
  **How do I use killpy in a CI pipeline or script?**
430
459
 
431
460
  ```bash
432
- # List as machine-readable JSON
461
+ # List as machine-readable JSON (progress goes to stderr, JSON to stdout)
433
462
  killpy list --json
434
463
 
464
+ # Suppress progress output entirely with --quiet / -q
465
+ killpy list --json --quiet | jq '.[] | .size_human'
466
+
435
467
  # Stream results as NDJSON in real time
436
468
  killpy list --json-stream | jq '.size_bytes'
437
469
 
@@ -439,6 +471,8 @@ killpy list --json-stream | jq '.size_bytes'
439
471
  killpy --path ./build_artifacts --delete-all --yes
440
472
  ```
441
473
 
474
+ Progress messages always go to **stderr**, so stdout is clean for piping even without `--quiet`. Use `-q` when you want no output at all on stderr.
475
+
442
476
  **How do I skip certain directories?**
443
477
 
444
478
  ```bash
@@ -550,7 +584,7 @@ Typical minimal setup (safe for daily use):
550
584
 
551
585
  ```yaml
552
586
  - repo: https://github.com/Tlaloc-Es/KillPy
553
- rev: 0.22.0
587
+ rev: 0.24.0
554
588
  hooks:
555
589
  - id: killpy
556
590
  ```
@@ -18,6 +18,7 @@ killpy/commands/_utils.py
18
18
  killpy/commands/clean.py
19
19
  killpy/commands/delete.py
20
20
  killpy/commands/doctor.py
21
+ killpy/commands/find.py
21
22
  killpy/commands/list.py
22
23
  killpy/commands/stats.py
23
24
  killpy/detectors/__init__.py
@@ -1,3 +1,4 @@
1
1
  click>=8.1.8
2
+ packaging>=24.0
2
3
  rich>=13.9.4
3
4
  textual>=1.0.0
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "killpy"
3
- version = "0.22.0"
3
+ version = "0.24.0"
4
4
  description = "List all .venv directories and Conda environments 🐍 on your system and check how much space they are using. You can then choose which ones to delete in order to free up space 🧹."
5
5
  readme = "README.md"
6
6
  classifiers = [
@@ -11,7 +11,12 @@ classifiers = [
11
11
  "Programming Language :: Python :: 3.12",
12
12
  "Programming Language :: Python :: 3.13",
13
13
  ]
14
- dependencies = ["click>=8.1.8", "rich>=13.9.4", "textual>=1.0.0"]
14
+ dependencies = [
15
+ "click>=8.1.8",
16
+ "packaging>=24.0",
17
+ "rich>=13.9.4",
18
+ "textual>=1.0.0",
19
+ ]
15
20
  requires-python = ">=3.10"
16
21
 
17
22
  [project.urls]
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes