killpy 0.24.1__tar.gz → 0.24.2__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 (45) hide show
  1. {killpy-0.24.1 → killpy-0.24.2}/PKG-INFO +2 -2
  2. {killpy-0.24.1 → killpy-0.24.2}/README.md +1 -1
  3. {killpy-0.24.1 → killpy-0.24.2}/killpy/__main__.py +12 -6
  4. {killpy-0.24.1 → killpy-0.24.2}/killpy/cleaner.py +19 -2
  5. {killpy-0.24.1 → killpy-0.24.2}/killpy/cli.py +2 -1
  6. {killpy-0.24.1 → killpy-0.24.2}/killpy/detectors/artifacts.py +14 -3
  7. {killpy-0.24.1 → killpy-0.24.2}/killpy/detectors/cache.py +40 -13
  8. killpy-0.24.2/killpy/detectors/uv.py +96 -0
  9. {killpy-0.24.1 → killpy-0.24.2}/killpy/intelligence/suggestions.py +0 -8
  10. {killpy-0.24.1 → killpy-0.24.2}/killpy/models.py +2 -2
  11. {killpy-0.24.1 → killpy-0.24.2}/killpy.egg-info/PKG-INFO +2 -2
  12. {killpy-0.24.1 → killpy-0.24.2}/pyproject.toml +1 -1
  13. killpy-0.24.1/killpy/detectors/uv.py +0 -64
  14. {killpy-0.24.1 → killpy-0.24.2}/killpy/__init__.py +0 -0
  15. {killpy-0.24.1 → killpy-0.24.2}/killpy/cleaners/__init__.py +0 -0
  16. {killpy-0.24.1 → killpy-0.24.2}/killpy/commands/__init__.py +0 -0
  17. {killpy-0.24.1 → killpy-0.24.2}/killpy/commands/_utils.py +0 -0
  18. {killpy-0.24.1 → killpy-0.24.2}/killpy/commands/clean.py +0 -0
  19. {killpy-0.24.1 → killpy-0.24.2}/killpy/commands/delete.py +0 -0
  20. {killpy-0.24.1 → killpy-0.24.2}/killpy/commands/doctor.py +0 -0
  21. {killpy-0.24.1 → killpy-0.24.2}/killpy/commands/find.py +0 -0
  22. {killpy-0.24.1 → killpy-0.24.2}/killpy/commands/list.py +0 -0
  23. {killpy-0.24.1 → killpy-0.24.2}/killpy/commands/stats.py +0 -0
  24. {killpy-0.24.1 → killpy-0.24.2}/killpy/detectors/__init__.py +0 -0
  25. {killpy-0.24.1 → killpy-0.24.2}/killpy/detectors/base.py +0 -0
  26. {killpy-0.24.1 → killpy-0.24.2}/killpy/detectors/conda.py +0 -0
  27. {killpy-0.24.1 → killpy-0.24.2}/killpy/detectors/hatch.py +0 -0
  28. {killpy-0.24.1 → killpy-0.24.2}/killpy/detectors/pipenv.py +0 -0
  29. {killpy-0.24.1 → killpy-0.24.2}/killpy/detectors/pipx.py +0 -0
  30. {killpy-0.24.1 → killpy-0.24.2}/killpy/detectors/poetry.py +0 -0
  31. {killpy-0.24.1 → killpy-0.24.2}/killpy/detectors/pyenv.py +0 -0
  32. {killpy-0.24.1 → killpy-0.24.2}/killpy/detectors/tox.py +0 -0
  33. {killpy-0.24.1 → killpy-0.24.2}/killpy/detectors/venv.py +0 -0
  34. {killpy-0.24.1 → killpy-0.24.2}/killpy/files/__init__.py +0 -0
  35. {killpy-0.24.1 → killpy-0.24.2}/killpy/intelligence/__init__.py +0 -0
  36. {killpy-0.24.1 → killpy-0.24.2}/killpy/intelligence/git_analyzer.py +0 -0
  37. {killpy-0.24.1 → killpy-0.24.2}/killpy/intelligence/scoring.py +0 -0
  38. {killpy-0.24.1 → killpy-0.24.2}/killpy/intelligence/tracker.py +0 -0
  39. {killpy-0.24.1 → killpy-0.24.2}/killpy/scanner.py +0 -0
  40. {killpy-0.24.1 → killpy-0.24.2}/killpy.egg-info/SOURCES.txt +0 -0
  41. {killpy-0.24.1 → killpy-0.24.2}/killpy.egg-info/dependency_links.txt +0 -0
  42. {killpy-0.24.1 → killpy-0.24.2}/killpy.egg-info/entry_points.txt +0 -0
  43. {killpy-0.24.1 → killpy-0.24.2}/killpy.egg-info/requires.txt +0 -0
  44. {killpy-0.24.1 → killpy-0.24.2}/killpy.egg-info/top_level.txt +0 -0
  45. {killpy-0.24.1 → killpy-0.24.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: killpy
3
- Version: 0.24.1
3
+ Version: 0.24.2
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/
@@ -587,7 +587,7 @@ Typical minimal setup (safe for daily use):
587
587
 
588
588
  ```yaml
589
589
  - repo: https://github.com/Tlaloc-Es/KillPy
590
- rev: 0.24.1
590
+ rev: 0.24.2
591
591
  hooks:
592
592
  - id: killpy
593
593
  ```
@@ -566,7 +566,7 @@ Typical minimal setup (safe for daily use):
566
566
 
567
567
  ```yaml
568
568
  - repo: https://github.com/Tlaloc-Es/KillPy
569
- rev: 0.24.1
569
+ rev: 0.24.2
570
570
  hooks:
571
571
  - id: killpy
572
572
  ```
@@ -1,4 +1,5 @@
1
1
  import logging
2
+ import sys
2
3
  from pathlib import Path
3
4
 
4
5
  import click
@@ -6,7 +7,7 @@ from rich.console import Console
6
7
  from rich.progress import Progress, SpinnerColumn, TextColumn
7
8
  from rich.prompt import Confirm
8
9
 
9
- from killpy.cleaner import Cleaner
10
+ from killpy.cleaner import Cleaner, CleanerError
10
11
  from killpy.cli import TableApp
11
12
  from killpy.commands.clean import clean
12
13
  from killpy.commands.delete import delete_cmd
@@ -62,17 +63,19 @@ def _run_delete_all(path: Path, excluded: set[str], yes: bool) -> None:
62
63
 
63
64
  deleted = 0
64
65
  freed = 0
66
+ errors = 0
65
67
  for env in envs:
66
- size = env.size_bytes
67
- if cleaner.delete(env):
68
- freed += size
68
+ try:
69
+ freed += cleaner.delete(env)
69
70
  deleted += 1
70
- else:
71
- console.print(f"[red]Failed to delete:[/red] {env.path}")
71
+ except CleanerError as exc:
72
+ console.print(f" [red]Failed to delete:[/red] {env.path}: {exc}")
73
+ errors += 1
72
74
 
73
75
  console.print(
74
76
  f"\n[bold green]Done.[/bold green] Deleted {deleted}/{len(envs)} environment(s), " # noqa: E501
75
77
  f"freed [bold]{format_size(freed)}[/bold]."
78
+ + (f" [red]{errors} error(s).[/red]" if errors else "")
76
79
  )
77
80
 
78
81
  # Best-effort: update the history record created above with freed bytes.
@@ -81,6 +84,9 @@ def _run_delete_all(path: Path, excluded: set[str], yes: bool) -> None:
81
84
  except Exception: # noqa: BLE001
82
85
  pass
83
86
 
87
+ if errors:
88
+ sys.exit(1)
89
+
84
90
 
85
91
  @click.group(invoke_without_command=True)
86
92
  @click.option(
@@ -11,8 +11,8 @@ Usage::
11
11
  The :class:`Cleaner` dispatches to the correct removal strategy based on
12
12
  :attr:`~killpy.models.Environment.managed_by`. Environments with
13
13
  ``managed_by="conda"`` are removed via ``conda env remove``;
14
- ``managed_by="pipx"`` via ``pipx uninstall``; all others via
15
- :func:`shutil.rmtree`.
14
+ ``managed_by="pipx"`` via ``pipx uninstall``; ``managed_by="uv"`` via
15
+ ``uv tool uninstall``; all others via :func:`shutil.rmtree`.
16
16
  """
17
17
 
18
18
  from __future__ import annotations
@@ -77,6 +77,8 @@ class Cleaner:
77
77
  self._remove_conda(env.name)
78
78
  elif env.managed_by == "pipx":
79
79
  self._remove_pipx(env.name)
80
+ elif env.managed_by == "uv":
81
+ self._remove_uv_tool(env.name)
80
82
  else:
81
83
  self._remove_filesystem(env.path)
82
84
  except CleanerError:
@@ -161,3 +163,18 @@ class Cleaner:
161
163
  raise CleanerError(
162
164
  f"pipx uninstall failed for '{package_name}': {result.stderr.strip()}"
163
165
  )
166
+
167
+ @staticmethod
168
+ def _remove_uv_tool(tool_name: str) -> None:
169
+ if shutil.which("uv") is None:
170
+ raise CleanerError("uv not found on PATH")
171
+ result = subprocess.run(
172
+ ["uv", "tool", "uninstall", tool_name],
173
+ check=False,
174
+ capture_output=True,
175
+ text=True,
176
+ )
177
+ if result.returncode != 0:
178
+ raise CleanerError(
179
+ f"uv tool uninstall failed for '{tool_name}': {result.stderr.strip()}"
180
+ )
@@ -714,8 +714,9 @@ class TableApp(App):
714
714
  )
715
715
  self.bell()
716
716
 
717
- @is_venv_tab
718
717
  def delete_environment(self, environment: Environment) -> bool:
718
+ # Shared helper — no tab gating here: it is called from both the
719
+ # venv tab (delete actions) and the pipx tab (uninstall action).
719
720
  try:
720
721
  self.cleaner.delete(environment)
721
722
  return True
@@ -14,6 +14,10 @@ from killpy.models import Environment
14
14
  logger = logging.getLogger(__name__)
15
15
 
16
16
  _PRUNED: frozenset[str] = frozenset({".git", ".hg", ".svn", "node_modules"})
17
+ # Environment internals are not build artifacts: every installed package has
18
+ # a ``site-packages/*.dist-info`` directory whose deletion corrupts the env.
19
+ # Environments are VenvDetector territory.
20
+ _ENV_DIRS: frozenset[str] = frozenset({".venv", "site-packages"})
17
21
  _EXACT_NAMES: frozenset[str] = frozenset({"dist", "build"})
18
22
  _SUFFIXES: tuple[str, ...] = (".egg-info", ".dist-info")
19
23
 
@@ -28,7 +32,10 @@ class ArtifactsDetector(AbstractDetector):
28
32
  """Detects Python build artifact directories.
29
33
 
30
34
  Finds ``dist/``, ``build/``, ``*.egg-info/``, and ``*.dist-info/``
31
- directories under the scan root.
35
+ directories under the scan root. Virtual environments (``.venv``,
36
+ any directory containing ``pyvenv.cfg``) and ``site-packages`` trees
37
+ are skipped: their ``*.dist-info`` entries are package metadata, not
38
+ build output.
32
39
  """
33
40
 
34
41
  name = "artifacts"
@@ -38,10 +45,14 @@ class ArtifactsDetector(AbstractDetector):
38
45
 
39
46
  def detect(self, path: Path) -> list[Environment]:
40
47
  envs: list[Environment] = []
41
- for current_root, directories, _ in os.walk(path, topdown=True):
48
+ for current_root, directories, files in os.walk(path, topdown=True):
49
+ if "pyvenv.cfg" in files:
50
+ # Inside a virtual environment (whatever its name) — skip it.
51
+ directories[:] = []
52
+ continue
42
53
  pruned = set()
43
54
  for d in directories:
44
- if d in _PRUNED:
55
+ if d in _PRUNED or d in _ENV_DIRS:
45
56
  pruned.add(d)
46
57
  continue
47
58
  if _is_artifact_dir(d):
@@ -5,8 +5,8 @@ Covers:
5
5
  * ``.mypy_cache``
6
6
  * ``.pytest_cache``
7
7
  * ``.ruff_cache``
8
- * Global pip cache (``~/.cache/pip``)
9
- * Global uv cache (``~/.cache/uv``)
8
+ * Global pip cache (``~/.cache/pip``) — only when inside the scanned path
9
+ * Global uv cache (``~/.cache/uv``) — only when inside the scanned path
10
10
  """
11
11
 
12
12
  from __future__ import annotations
@@ -33,6 +33,11 @@ _LOCAL_CACHE_DIRS: tuple[str, ...] = (
33
33
  # Directories to prune from the walk so we don't descend into them.
34
34
  _PRUNED: frozenset[str] = frozenset({".git", ".hg", ".svn", "node_modules"})
35
35
 
36
+ # Caches inside an environment belong to the environment: VenvDetector
37
+ # already reports the whole tree, so listing them separately double-counts
38
+ # their size in stats/list totals.
39
+ _ENV_DIRS: frozenset[str] = frozenset({".venv", "site-packages"})
40
+
36
41
 
37
42
  class CacheDetector(AbstractDetector):
38
43
  """Detects local and global Python cache directories."""
@@ -45,18 +50,26 @@ class CacheDetector(AbstractDetector):
45
50
  def detect(self, path: Path) -> list[Environment]:
46
51
  envs: list[Environment] = []
47
52
  envs.extend(self._scan_local(path))
48
- envs.extend(self._scan_global())
53
+ envs.extend(self._scan_global(path))
49
54
  return envs
50
55
 
51
56
  # ------------------------------------------------------------------ #
52
57
 
53
58
  def _scan_local(self, root: Path) -> list[Environment]:
54
- """Walk *root* and collect all known local cache directories."""
59
+ """Walk *root* and collect all known local cache directories.
60
+
61
+ Virtual environments (``.venv``, any directory containing
62
+ ``pyvenv.cfg``) and ``site-packages`` trees are skipped.
63
+ """
55
64
  results: list[Environment] = []
56
- for current_root, directories, _ in os.walk(root, topdown=True):
65
+ for current_root, directories, files in os.walk(root, topdown=True):
66
+ if "pyvenv.cfg" in files:
67
+ # Inside a virtual environment (whatever its name) — skip it.
68
+ directories[:] = []
69
+ continue
57
70
  prune = set()
58
71
  for d in directories:
59
- if d in _PRUNED:
72
+ if d in _PRUNED or d in _ENV_DIRS:
60
73
  prune.add(d)
61
74
  continue
62
75
  if d in _LOCAL_CACHE_DIRS:
@@ -70,19 +83,33 @@ class CacheDetector(AbstractDetector):
70
83
  directories[:] = [d for d in directories if d not in prune]
71
84
  return results
72
85
 
73
- def _scan_global(self) -> list[Environment]:
74
- """Return global pip and uv cache directories if they exist."""
86
+ def _scan_global(self, root: Path) -> list[Environment]:
87
+ """Return global pip/uv cache directories that live under *root*.
88
+
89
+ A scan scoped to a project directory (e.g. the pre-commit hooks or
90
+ ``killpy delete --type cache --path <repo>``) must never surface —
91
+ and therefore never delete — caches outside that directory. The
92
+ global caches are still reported when the scan root contains them
93
+ (e.g. ``killpy --path ~``).
94
+ """
95
+ try:
96
+ scan_root = root.resolve()
97
+ except OSError:
98
+ scan_root = root
75
99
  results: list[Environment] = []
76
100
  candidates = [
77
101
  (Path.home() / ".cache" / "pip", "pip-cache"),
78
102
  (Path.home() / ".cache" / "uv", "uv-cache"),
79
103
  ]
80
104
  for cache_path, tag in candidates:
81
- if cache_path.exists():
82
- try:
83
- results.append(_make_cache_env(cache_path, tag))
84
- except (FileNotFoundError, OSError) as exc:
85
- logger.debug("Skipping global cache %s: %s", cache_path, exc)
105
+ if not cache_path.exists():
106
+ continue
107
+ try:
108
+ if not cache_path.resolve().is_relative_to(scan_root):
109
+ continue
110
+ results.append(_make_cache_env(cache_path, tag))
111
+ except (FileNotFoundError, OSError) as exc:
112
+ logger.debug("Skipping global cache %s: %s", cache_path, exc)
86
113
  return results
87
114
 
88
115
 
@@ -0,0 +1,96 @@
1
+ """Detector for disk space owned by uv itself (tool envs and Pythons)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import os
7
+ import shutil
8
+ from datetime import datetime, timezone
9
+ from pathlib import Path
10
+
11
+ from killpy.detectors.base import AbstractDetector
12
+ from killpy.files import get_total_size
13
+ from killpy.models import Environment
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ def _uv_data_dir() -> Path:
19
+ xdg = os.environ.get("XDG_DATA_HOME")
20
+ base = Path(xdg) if xdg else Path.home() / ".local" / "share"
21
+ return base / "uv"
22
+
23
+
24
+ def _uv_tools_dir() -> Path:
25
+ override = os.environ.get("UV_TOOL_DIR")
26
+ return Path(override) if override else _uv_data_dir() / "tools"
27
+
28
+
29
+ def _uv_python_dir() -> Path:
30
+ override = os.environ.get("UV_PYTHON_INSTALL_DIR")
31
+ return Path(override) if override else _uv_data_dir() / "python"
32
+
33
+
34
+ class UvDetector(AbstractDetector):
35
+ """Detects environments managed by uv itself.
36
+
37
+ Covers:
38
+
39
+ * Tool environments installed with ``uv tool install`` (uv's pipx
40
+ equivalent), stored under ``~/.local/share/uv/tools``. Marked
41
+ ``managed_by="uv"`` so deletion goes through ``uv tool uninstall``
42
+ and the ``~/.local/bin`` shims are cleaned up too.
43
+ * Python versions installed with ``uv python install``, stored under
44
+ ``~/.local/share/uv/python``.
45
+
46
+ ``UV_TOOL_DIR``, ``UV_PYTHON_INSTALL_DIR`` and ``XDG_DATA_HOME`` are
47
+ honoured. Project virtualenvs created by ``uv venv`` / ``uv sync``
48
+ are regular ``pyvenv.cfg`` environments reported by
49
+ :class:`~killpy.detectors.venv.VenvDetector`, and the global uv cache
50
+ is reported by the cache detector. The scan *path* argument is
51
+ ignored.
52
+ """
53
+
54
+ name = "uv"
55
+
56
+ def can_handle(self) -> bool:
57
+ return (
58
+ shutil.which("uv") is not None
59
+ or _uv_tools_dir().exists()
60
+ or _uv_python_dir().exists()
61
+ )
62
+
63
+ def detect(self, path: Path) -> list[Environment]: # noqa: ARG002
64
+ envs: list[Environment] = []
65
+ envs.extend(self._scan_dir(_uv_tools_dir(), managed_by="uv"))
66
+ envs.extend(self._scan_dir(_uv_python_dir(), managed_by=None))
67
+ return envs
68
+
69
+ @staticmethod
70
+ def _scan_dir(root: Path, managed_by: str | None) -> list[Environment]:
71
+ if not root.exists():
72
+ return []
73
+ envs: list[Environment] = []
74
+ try:
75
+ for env_dir in root.iterdir():
76
+ if not env_dir.is_dir():
77
+ continue
78
+ try:
79
+ stat = env_dir.stat()
80
+ size = get_total_size(env_dir)
81
+ mtime = datetime.fromtimestamp(stat.st_mtime, tz=timezone.utc)
82
+ envs.append(
83
+ Environment(
84
+ path=env_dir,
85
+ name=env_dir.name,
86
+ type="uv",
87
+ last_accessed=mtime,
88
+ size_bytes=size,
89
+ managed_by=managed_by,
90
+ )
91
+ )
92
+ except (FileNotFoundError, OSError) as exc:
93
+ logger.debug("Skipping uv entry %s: %s", env_dir, exc)
94
+ except OSError as exc:
95
+ logger.error("Cannot inspect uv dir %s: %s", root, exc)
96
+ return envs
@@ -9,7 +9,6 @@ from killpy.models import ScoredEnvironment, Suggestion
9
9
 
10
10
  # Age thresholds in days.
11
11
  _HIGH_AGE_ORPHAN_THRESHOLD = 180 # orphan + age >= this → HIGH
12
- _HIGH_AGE_NO_PROJECT_THRESHOLD = 365 # no project files + age >= this → HIGH
13
12
  _MEDIUM_AGE_THRESHOLD = 120 # age >= this → MEDIUM
14
13
  _LOW_AGE_THRESHOLD = 120 # age < this → LOW
15
14
 
@@ -73,7 +72,6 @@ class SuggestionEngine:
73
72
  reasons: list[str],
74
73
  ) -> tuple[Literal["HIGH", "MEDIUM", "LOW"], str]:
75
74
  is_orphan = scored.is_orphan
76
- has_project_files = scored.has_project_files
77
75
  is_active_git = (
78
76
  scored.git_info is not None
79
77
  and scored.git_info.is_git_repo
@@ -86,12 +84,6 @@ class SuggestionEngine:
86
84
  reasons.append(f"Not used in {age_days} days")
87
85
  return "HIGH", _ACTION_HIGH
88
86
 
89
- # ── Rule 1b: HIGH — no project files and very old ──────────────────────
90
- if not has_project_files and age_days >= _HIGH_AGE_NO_PROJECT_THRESHOLD:
91
- reasons.append("Orphan — no project files found nearby")
92
- reasons.append(f"Not used in {age_days} days")
93
- return "HIGH", _ACTION_HIGH
94
-
95
87
  # ── Rule 2: LOW — active git or very recent ─────────────────────────────
96
88
  if is_active_git or age_days < _LOW_AGE_THRESHOLD:
97
89
  if is_active_git:
@@ -39,8 +39,8 @@ class Environment:
39
39
  Total size in bytes (recursive directory sum).
40
40
  managed_by:
41
41
  If not ``None``, the external tool that manages deletion. Supported
42
- values: ``"conda"`` and ``"pipx"``. When ``None`` deletion is
43
- performed via :func:`shutil.rmtree`.
42
+ values: ``"conda"``, ``"pipx"`` and ``"uv"`` (uv tool environments).
43
+ When ``None`` deletion is performed via :func:`shutil.rmtree`.
44
44
  """
45
45
 
46
46
  path: Path
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: killpy
3
- Version: 0.24.1
3
+ Version: 0.24.2
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/
@@ -587,7 +587,7 @@ Typical minimal setup (safe for daily use):
587
587
 
588
588
  ```yaml
589
589
  - repo: https://github.com/Tlaloc-Es/KillPy
590
- rev: 0.24.1
590
+ rev: 0.24.2
591
591
  hooks:
592
592
  - id: killpy
593
593
  ```
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "killpy"
3
- version = "0.24.1"
3
+ version = "0.24.2"
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 = [
@@ -1,64 +0,0 @@
1
- """Detector for uv-managed virtual environments."""
2
-
3
- from __future__ import annotations
4
-
5
- import logging
6
- import os
7
- import shutil
8
- from datetime import datetime, timezone
9
- from pathlib import Path
10
-
11
- from killpy.detectors.base import AbstractDetector
12
- from killpy.files import get_total_size
13
- from killpy.models import Environment
14
-
15
- logger = logging.getLogger(__name__)
16
-
17
- _PRUNED: frozenset[str] = frozenset({".git", ".hg", ".svn", "node_modules"})
18
-
19
-
20
- class UvDetector(AbstractDetector):
21
- """Detects ``.uv`` directories created by `uv venv` inside projects.
22
-
23
- Also reports the global uv cache at ``~/.cache/uv`` – but only when
24
- the cache detector is *not* included in the same scan (the scanner
25
- handles deduplication by path).
26
- """
27
-
28
- name = "uv"
29
-
30
- def can_handle(self) -> bool:
31
- return shutil.which("uv") is not None
32
-
33
- def detect(self, path: Path) -> list[Environment]:
34
- envs: list[Environment] = []
35
- for current_root, directories, _ in os.walk(path, topdown=True):
36
- pruned = set()
37
- for d in directories:
38
- if d in _PRUNED:
39
- pruned.add(d)
40
- continue
41
- if d == ".uv":
42
- uv_path = Path(current_root) / d
43
- try:
44
- stat = uv_path.stat()
45
- size = get_total_size(uv_path)
46
- mtime = datetime.fromtimestamp(
47
- stat.st_mtime,
48
- tz=timezone.utc,
49
- )
50
- envs.append(
51
- Environment(
52
- path=uv_path,
53
- name=str(uv_path),
54
- type="uv",
55
- last_accessed=mtime,
56
- size_bytes=size,
57
- )
58
- )
59
- except (FileNotFoundError, OSError) as exc:
60
- logger.debug("Skipping %s: %s", uv_path, exc)
61
- pruned.add(d)
62
- directories[:] = [d for d in directories if d not in pruned]
63
-
64
- return envs
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes