killpy 1.0.0__tar.gz → 1.0.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 (45) hide show
  1. {killpy-1.0.0 → killpy-1.0.1}/PKG-INFO +4 -4
  2. {killpy-1.0.0 → killpy-1.0.1}/README.md +2 -2
  3. killpy-1.0.1/killpy/detectors/_shared_walk.py +110 -0
  4. {killpy-1.0.0 → killpy-1.0.1}/killpy/detectors/artifacts.py +1 -0
  5. {killpy-1.0.0 → killpy-1.0.1}/killpy/detectors/base.py +14 -0
  6. {killpy-1.0.0 → killpy-1.0.1}/killpy/detectors/cache.py +5 -0
  7. {killpy-1.0.0 → killpy-1.0.1}/killpy/detectors/tox.py +1 -0
  8. {killpy-1.0.0 → killpy-1.0.1}/killpy/detectors/venv.py +1 -0
  9. {killpy-1.0.0 → killpy-1.0.1}/killpy/scanner.py +69 -22
  10. {killpy-1.0.0 → killpy-1.0.1}/killpy.egg-info/PKG-INFO +4 -4
  11. {killpy-1.0.0 → killpy-1.0.1}/killpy.egg-info/SOURCES.txt +1 -0
  12. {killpy-1.0.0 → killpy-1.0.1}/pyproject.toml +9 -2
  13. {killpy-1.0.0 → killpy-1.0.1}/killpy/__init__.py +0 -0
  14. {killpy-1.0.0 → killpy-1.0.1}/killpy/__main__.py +0 -0
  15. {killpy-1.0.0 → killpy-1.0.1}/killpy/cleaner.py +0 -0
  16. {killpy-1.0.0 → killpy-1.0.1}/killpy/cleaners/__init__.py +0 -0
  17. {killpy-1.0.0 → killpy-1.0.1}/killpy/cli.py +0 -0
  18. {killpy-1.0.0 → killpy-1.0.1}/killpy/commands/__init__.py +0 -0
  19. {killpy-1.0.0 → killpy-1.0.1}/killpy/commands/_utils.py +0 -0
  20. {killpy-1.0.0 → killpy-1.0.1}/killpy/commands/clean.py +0 -0
  21. {killpy-1.0.0 → killpy-1.0.1}/killpy/commands/delete.py +0 -0
  22. {killpy-1.0.0 → killpy-1.0.1}/killpy/commands/doctor.py +0 -0
  23. {killpy-1.0.0 → killpy-1.0.1}/killpy/commands/find.py +0 -0
  24. {killpy-1.0.0 → killpy-1.0.1}/killpy/commands/list.py +0 -0
  25. {killpy-1.0.0 → killpy-1.0.1}/killpy/commands/stats.py +0 -0
  26. {killpy-1.0.0 → killpy-1.0.1}/killpy/detectors/__init__.py +0 -0
  27. {killpy-1.0.0 → killpy-1.0.1}/killpy/detectors/conda.py +0 -0
  28. {killpy-1.0.0 → killpy-1.0.1}/killpy/detectors/hatch.py +0 -0
  29. {killpy-1.0.0 → killpy-1.0.1}/killpy/detectors/pipenv.py +0 -0
  30. {killpy-1.0.0 → killpy-1.0.1}/killpy/detectors/pipx.py +0 -0
  31. {killpy-1.0.0 → killpy-1.0.1}/killpy/detectors/poetry.py +0 -0
  32. {killpy-1.0.0 → killpy-1.0.1}/killpy/detectors/pyenv.py +0 -0
  33. {killpy-1.0.0 → killpy-1.0.1}/killpy/detectors/uv.py +0 -0
  34. {killpy-1.0.0 → killpy-1.0.1}/killpy/files/__init__.py +0 -0
  35. {killpy-1.0.0 → killpy-1.0.1}/killpy/intelligence/__init__.py +0 -0
  36. {killpy-1.0.0 → killpy-1.0.1}/killpy/intelligence/git_analyzer.py +0 -0
  37. {killpy-1.0.0 → killpy-1.0.1}/killpy/intelligence/scoring.py +0 -0
  38. {killpy-1.0.0 → killpy-1.0.1}/killpy/intelligence/suggestions.py +0 -0
  39. {killpy-1.0.0 → killpy-1.0.1}/killpy/intelligence/tracker.py +0 -0
  40. {killpy-1.0.0 → killpy-1.0.1}/killpy/models.py +0 -0
  41. {killpy-1.0.0 → killpy-1.0.1}/killpy.egg-info/dependency_links.txt +0 -0
  42. {killpy-1.0.0 → killpy-1.0.1}/killpy.egg-info/entry_points.txt +0 -0
  43. {killpy-1.0.0 → killpy-1.0.1}/killpy.egg-info/requires.txt +0 -0
  44. {killpy-1.0.0 → killpy-1.0.1}/killpy.egg-info/top_level.txt +0 -0
  45. {killpy-1.0.0 → killpy-1.0.1}/setup.cfg +0 -0
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: killpy
3
- Version: 1.0.0
3
+ Version: 1.0.1
4
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
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
10
+ Classifier: Development Status :: 5 - Production/Stable
11
11
  Classifier: Environment :: Console
12
12
  Classifier: Environment :: Console :: Curses
13
13
  Classifier: Intended Audience :: Developers
@@ -603,7 +603,7 @@ ______________________________________________________________________
603
603
 
604
604
  ```yaml
605
605
  - repo: https://github.com/Tlaloc-Es/KillPy
606
- rev: 1.0.0
606
+ rev: 1.0.1
607
607
  hooks:
608
608
  - id: killpy # remove __pycache__ on every commit
609
609
  - id: killpy-clean-caches # also removes .mypy_cache, .pytest_cache, .ruff_cache
@@ -628,7 +628,7 @@ Typical minimal setup (safe for daily use):
628
628
 
629
629
  ```yaml
630
630
  - repo: https://github.com/Tlaloc-Es/KillPy
631
- rev: 1.0.0
631
+ rev: 1.0.1
632
632
  hooks:
633
633
  - id: killpy
634
634
  ```
@@ -574,7 +574,7 @@ ______________________________________________________________________
574
574
 
575
575
  ```yaml
576
576
  - repo: https://github.com/Tlaloc-Es/KillPy
577
- rev: 1.0.0
577
+ rev: 1.0.1
578
578
  hooks:
579
579
  - id: killpy # remove __pycache__ on every commit
580
580
  - id: killpy-clean-caches # also removes .mypy_cache, .pytest_cache, .ruff_cache
@@ -599,7 +599,7 @@ Typical minimal setup (safe for daily use):
599
599
 
600
600
  ```yaml
601
601
  - repo: https://github.com/Tlaloc-Es/KillPy
602
- rev: 1.0.0
602
+ rev: 1.0.1
603
603
  hooks:
604
604
  - id: killpy
605
605
  ```
@@ -0,0 +1,110 @@
1
+ """Single shared filesystem walk for the tree-walking detectors.
2
+
3
+ ``venv`` / ``cache`` / ``artifacts`` / ``tox`` each used to run their own
4
+ ``os.walk`` over the scan root, and ``get_total_size`` re-walked every
5
+ environment they found — walking a large tree several times over.
6
+
7
+ This module performs **one** traversal: each directory is classified as an
8
+ environment *container* (a venv, a cache dir, a build artifact, or ``.tox``);
9
+ a match is recorded, its size is summed once, and its subtree is pruned.
10
+ Environment pruning is always applied — once a venv is found, nothing inside it
11
+ is scanned again — which also collapses the cache/artifact double-counting that
12
+ used to happen inside environments.
13
+
14
+ Callers pass the set of detector names whose results they want (``active``).
15
+ Pruning happens on every container regardless of ``active``, so asking for a
16
+ subset yields exactly what running those detectors alone would.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import logging
22
+ import os
23
+ from datetime import datetime, timezone
24
+ from pathlib import Path
25
+
26
+ from killpy.detectors.base import VCS_PRUNE_DIRS
27
+ from killpy.files import get_total_size
28
+ from killpy.models import Environment
29
+
30
+ logger = logging.getLogger(__name__)
31
+
32
+ _CACHE_DIRS = frozenset({"__pycache__", ".mypy_cache", ".pytest_cache", ".ruff_cache"})
33
+ _ARTIFACT_EXACT = frozenset({"dist", "build"})
34
+ _ARTIFACT_SUFFIXES = (".egg-info", ".dist-info")
35
+
36
+ #: Map every ``Environment.type`` the shared walk (and the cache global scan)
37
+ #: can produce back to the detector that owns it — used to group results per
38
+ #: detector for progress callbacks.
39
+ TYPE_TO_DETECTOR: dict[str, str] = {
40
+ ".venv": "venv",
41
+ "pyvenv.cfg": "venv",
42
+ "__pycache__": "cache",
43
+ ".mypy_cache": "cache",
44
+ ".pytest_cache": "cache",
45
+ ".ruff_cache": "cache",
46
+ "pip-cache": "cache",
47
+ "uv-cache": "cache",
48
+ "artifacts": "artifacts",
49
+ "tox": "tox",
50
+ }
51
+
52
+
53
+ def _classify(basename: str, filenames: list[str]) -> tuple[str, str] | None:
54
+ """Return ``(detector_name, env_type)`` if the dir is a container, else ``None``."""
55
+ if basename == ".venv" or "pyvenv.cfg" in filenames:
56
+ return ("venv", ".venv" if basename == ".venv" else "pyvenv.cfg")
57
+ if basename in _CACHE_DIRS:
58
+ return ("cache", basename)
59
+ if basename in _ARTIFACT_EXACT or any(
60
+ basename.endswith(suffix) for suffix in _ARTIFACT_SUFFIXES
61
+ ):
62
+ return ("artifacts", "artifacts")
63
+ if basename == ".tox":
64
+ return ("tox", "tox")
65
+ return None
66
+
67
+
68
+ def _make_env(path: Path, env_type: str) -> Environment | None:
69
+ """Build an :class:`Environment` for *path*, summing its size once."""
70
+ try:
71
+ stat = path.stat()
72
+ return Environment(
73
+ path=path,
74
+ name=str(path),
75
+ type=env_type,
76
+ last_modified=datetime.fromtimestamp(stat.st_mtime, tz=timezone.utc),
77
+ size_bytes=get_total_size(path),
78
+ )
79
+ except (FileNotFoundError, OSError) as exc:
80
+ logger.debug("Skipping %s: %s", path, exc)
81
+ return None
82
+
83
+
84
+ def walk_environments(root: Path, active: set[str]) -> list[Environment]:
85
+ """Walk *root* once and return environments for the ``active`` detector set.
86
+
87
+ Each directory that matches a container type is reported (when its detector
88
+ is in *active*) and its subtree pruned; VCS and ``site-packages`` trees are
89
+ never descended into.
90
+ """
91
+ envs: list[Environment] = []
92
+ for current, dirnames, filenames in os.walk(root, topdown=True):
93
+ dirnames[:] = [d for d in dirnames if d not in VCS_PRUNE_DIRS]
94
+ current_path = Path(current)
95
+ if current_path != root:
96
+ match = _classify(current_path.name, filenames)
97
+ if match is not None:
98
+ detector_name, env_type = match
99
+ if detector_name in active:
100
+ env = _make_env(current_path, env_type)
101
+ if env is not None:
102
+ envs.append(env)
103
+ dirnames[:] = [] # env-pruning: never descend into a container
104
+ continue
105
+ # A bare ``site-packages`` (e.g. a conda env, which has no pyvenv.cfg) is
106
+ # not a container but must not be scanned for caches/artifacts. ``.venv``
107
+ # IS a container — detected on entry — so it is deliberately not pruned
108
+ # here (that would stop us from ever reporting it).
109
+ dirnames[:] = [d for d in dirnames if d != "site-packages"]
110
+ return envs
@@ -42,6 +42,7 @@ class ArtifactsDetector(AbstractDetector):
42
42
 
43
43
  name = "artifacts"
44
44
  always_available = True # pure filesystem walk
45
+ shared_walk = True # tree scan served by killpy.detectors._shared_walk
45
46
 
46
47
  def detect(self, path: Path) -> list[Environment]:
47
48
  envs: list[Environment] = []
@@ -62,6 +62,11 @@ class AbstractDetector(ABC):
62
62
  required_tool: ClassVar[str | None] = None
63
63
  #: ``True`` for detectors that always apply (pure filesystem walks).
64
64
  always_available: ClassVar[bool] = False
65
+ #: ``True`` when this detector's tree scan is served by the single shared
66
+ #: walk (``killpy.detectors._shared_walk``) instead of its own ``os.walk``.
67
+ #: The :class:`~killpy.scanner.Scanner` batches all such detectors into one
68
+ #: traversal; ``detect`` is kept for direct use and tests.
69
+ shared_walk: ClassVar[bool] = False
65
70
 
66
71
  @abstractmethod
67
72
  def detect(self, path: Path) -> list[Environment]:
@@ -78,6 +83,15 @@ class AbstractDetector(ABC):
78
83
  use it as the walk root.
79
84
  """
80
85
 
86
+ def scan_global(self, path: Path) -> list[Environment]:
87
+ """Environments found outside the shared tree walk (e.g. global caches).
88
+
89
+ Empty by default; :class:`~killpy.detectors.cache.CacheDetector` overrides
90
+ it to report the global pip/uv caches. Called by the Scanner only for
91
+ detectors with ``shared_walk = True``.
92
+ """
93
+ return []
94
+
81
95
  def _candidate_dirs(self) -> tuple[Path, ...]:
82
96
  """Directories whose existence makes this detector applicable.
83
97
 
@@ -72,6 +72,7 @@ class CacheDetector(AbstractDetector):
72
72
 
73
73
  name = "cache"
74
74
  always_available = True # pure filesystem walk
75
+ shared_walk = True # local tree scan served by killpy.detectors._shared_walk
75
76
 
76
77
  def detect(self, path: Path) -> list[Environment]:
77
78
  envs: list[Environment] = []
@@ -79,6 +80,10 @@ class CacheDetector(AbstractDetector):
79
80
  envs.extend(self._scan_global(path))
80
81
  return envs
81
82
 
83
+ def scan_global(self, path: Path) -> list[Environment]:
84
+ """Global pip/uv caches — the part not covered by the shared tree walk."""
85
+ return self._scan_global(path)
86
+
82
87
  # ------------------------------------------------------------------ #
83
88
 
84
89
  def _scan_local(self, root: Path) -> list[Environment]:
@@ -19,6 +19,7 @@ class ToxDetector(AbstractDetector):
19
19
 
20
20
  name = "tox"
21
21
  always_available = True # pure filesystem walk
22
+ shared_walk = True # tree scan served by killpy.detectors._shared_walk
22
23
 
23
24
  def detect(self, path: Path) -> list[Environment]:
24
25
  envs: list[Environment] = []
@@ -57,6 +57,7 @@ class VenvDetector(AbstractDetector):
57
57
 
58
58
  name = "venv"
59
59
  always_available = True # pure filesystem walk
60
+ shared_walk = True # tree scan served by killpy.detectors._shared_walk
60
61
 
61
62
  def detect(self, path: Path) -> list[Environment]:
62
63
  # Inner dedup: the ".venv" scan and the pyvenv.cfg scan below can both
@@ -22,6 +22,7 @@ from collections.abc import AsyncIterator, Callable
22
22
  from pathlib import Path
23
23
 
24
24
  from killpy.detectors import ALL_DETECTORS, AbstractDetector
25
+ from killpy.detectors._shared_walk import TYPE_TO_DETECTOR, walk_environments
25
26
  from killpy.detectors.pyenv import _pyenv_root
26
27
  from killpy.models import Environment
27
28
 
@@ -85,24 +86,28 @@ class Scanner:
85
86
  seen: set[Path] = set()
86
87
  results: list[Environment] = []
87
88
 
88
- for detector in self._detectors:
89
- if not detector.can_handle():
90
- logger.debug("Skipping %s (can_handle=False)", detector.name)
91
- continue
89
+ applicable = [d for d in self._detectors if d.can_handle()]
90
+ shared = [d for d in applicable if d.shared_walk]
91
+ others = [d for d in applicable if not d.shared_walk]
92
+
93
+ # One traversal shared by every filesystem-walking detector.
94
+ for detector, found in self._shared_walk_groups(shared, path):
95
+ processed = self._process(found, seen)
96
+ results.extend(processed)
97
+ if on_progress is not None:
98
+ on_progress(detector, processed)
99
+
100
+ # The remaining detectors scan their own global directories.
101
+ for detector in others:
92
102
  try:
93
103
  found = detector.detect(path)
94
104
  except Exception as exc: # noqa: BLE001
95
105
  logger.warning("Detector %s raised: %s", detector.name, exc)
96
106
  found = []
97
-
98
- deduped = self._deduplicate(found, seen)
99
- deduped = self._apply_exclusions(deduped)
100
- for env in deduped:
101
- self._mark_system_critical(env)
102
- results.extend(deduped)
103
-
107
+ processed = self._process(found, seen)
108
+ results.extend(processed)
104
109
  if on_progress is not None:
105
- on_progress(detector, deduped)
110
+ on_progress(detector, processed)
106
111
 
107
112
  results.sort(key=lambda e: e.size_bytes, reverse=True)
108
113
  return results
@@ -123,28 +128,70 @@ class Scanner:
123
128
  table.add_row(env)
124
129
  """
125
130
  applicable = [d for d in self._detectors if d.can_handle()]
131
+ shared = [d for d in applicable if d.shared_walk]
132
+ others = [d for d in applicable if not d.shared_walk]
126
133
  seen: set[Path] = set()
127
134
 
128
- async def _run(detector: AbstractDetector):
135
+ async def _run_shared() -> list[tuple[AbstractDetector, list[Environment]]]:
129
136
  try:
130
- return detector, await asyncio.to_thread(detector.detect, path)
137
+ return await asyncio.to_thread(self._shared_walk_groups, shared, path)
138
+ except Exception as exc: # noqa: BLE001
139
+ logger.warning("Shared walk raised: %s", exc)
140
+ return [(d, []) for d in shared]
141
+
142
+ async def _run_one(
143
+ detector: AbstractDetector,
144
+ ) -> list[tuple[AbstractDetector, list[Environment]]]:
145
+ try:
146
+ return [(detector, await asyncio.to_thread(detector.detect, path))]
131
147
  except Exception as exc: # noqa: BLE001
132
148
  logger.warning("Detector %s raised: %s", detector.name, exc)
133
- return detector, []
149
+ return [(detector, [])]
150
+
151
+ tasks = [asyncio.create_task(_run_one(d)) for d in others]
152
+ if shared:
153
+ tasks.append(asyncio.create_task(_run_shared()))
134
154
 
135
- tasks = [asyncio.create_task(_run(d)) for d in applicable]
136
155
  for coro in asyncio.as_completed(tasks):
137
- detector, found = await coro
138
- deduped = self._deduplicate(found, seen)
139
- deduped = self._apply_exclusions(deduped)
140
- for env in deduped:
141
- self._mark_system_critical(env)
142
- yield detector, deduped
156
+ for detector, found in await coro:
157
+ deduped = self._process(found, seen)
158
+ yield detector, deduped
143
159
 
144
160
  # ------------------------------------------------------------------ #
145
161
  # Helpers #
146
162
  # ------------------------------------------------------------------ #
147
163
 
164
+ def _shared_walk_groups(
165
+ self, shared: list[AbstractDetector], path: Path
166
+ ) -> list[tuple[AbstractDetector, list[Environment]]]:
167
+ """Run the one shared walk, returning ``(detector, envs)`` per detector.
168
+
169
+ The local tree is walked once for the union of *shared* detector names;
170
+ each detector also contributes its own global scan (pip/uv caches).
171
+ Results are grouped back per detector via :data:`TYPE_TO_DETECTOR` so the
172
+ per-detector progress contract is preserved.
173
+ """
174
+ if not shared:
175
+ return []
176
+ active = {d.name for d in shared}
177
+ found = walk_environments(path, active)
178
+ for detector in shared:
179
+ found.extend(detector.scan_global(path))
180
+ by_name: dict[str, list[Environment]] = {d.name: [] for d in shared}
181
+ for env in found:
182
+ name = TYPE_TO_DETECTOR.get(env.type)
183
+ if name in by_name:
184
+ by_name[name].append(env)
185
+ return [(d, by_name[d.name]) for d in shared]
186
+
187
+ def _process(self, found: list[Environment], seen: set[Path]) -> list[Environment]:
188
+ """Deduplicate, apply exclusions, and flag system-critical envs."""
189
+ deduped = self._deduplicate(found, seen)
190
+ deduped = self._apply_exclusions(deduped)
191
+ for env in deduped:
192
+ self._mark_system_critical(env)
193
+ return deduped
194
+
148
195
  @staticmethod
149
196
  def _deduplicate(envs: list[Environment], seen: set[Path]) -> list[Environment]:
150
197
  """Filter out environments whose resolved path has already been seen.
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: killpy
3
- Version: 1.0.0
3
+ Version: 1.0.1
4
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
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
10
+ Classifier: Development Status :: 5 - Production/Stable
11
11
  Classifier: Environment :: Console
12
12
  Classifier: Environment :: Console :: Curses
13
13
  Classifier: Intended Audience :: Developers
@@ -603,7 +603,7 @@ ______________________________________________________________________
603
603
 
604
604
  ```yaml
605
605
  - repo: https://github.com/Tlaloc-Es/KillPy
606
- rev: 1.0.0
606
+ rev: 1.0.1
607
607
  hooks:
608
608
  - id: killpy # remove __pycache__ on every commit
609
609
  - id: killpy-clean-caches # also removes .mypy_cache, .pytest_cache, .ruff_cache
@@ -628,7 +628,7 @@ Typical minimal setup (safe for daily use):
628
628
 
629
629
  ```yaml
630
630
  - repo: https://github.com/Tlaloc-Es/KillPy
631
- rev: 1.0.0
631
+ rev: 1.0.1
632
632
  hooks:
633
633
  - id: killpy
634
634
  ```
@@ -22,6 +22,7 @@ killpy/commands/find.py
22
22
  killpy/commands/list.py
23
23
  killpy/commands/stats.py
24
24
  killpy/detectors/__init__.py
25
+ killpy/detectors/_shared_walk.py
25
26
  killpy/detectors/artifacts.py
26
27
  killpy/detectors/base.py
27
28
  killpy/detectors/cache.py
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "killpy"
3
- version = "1.0.0"
3
+ version = "1.0.1"
4
4
  description = "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
  readme = "README.md"
6
6
  keywords = [
@@ -21,7 +21,7 @@ keywords = [
21
21
  "developer-tools",
22
22
  ]
23
23
  classifiers = [
24
- "Development Status :: 4 - Beta",
24
+ "Development Status :: 5 - Production/Stable",
25
25
  "Environment :: Console",
26
26
  "Environment :: Console :: Curses",
27
27
  "Intended Audience :: Developers",
@@ -75,6 +75,9 @@ package = true
75
75
  [tool.pytest.ini_options]
76
76
  testpaths = ["tests"]
77
77
  addopts = "-v --tb=short --cov=killpy --cov-report=term-missing --cov-report=xml"
78
+ markers = [
79
+ "integration: system-level end-to-end tests (real envs + real deletion); require KILLPY_INTEGRATION=1",
80
+ ]
78
81
 
79
82
  [tool.coverage.run]
80
83
  branch = true
@@ -103,5 +106,9 @@ lint = "ruff check killpy tests"
103
106
  format = "ruff format killpy tests"
104
107
  type = "mypy killpy"
105
108
 
109
+ [tool.poe.tasks.integration]
110
+ cmd = "pytest -m integration --no-cov tests/integration"
111
+ env = { KILLPY_INTEGRATION = "1" }
112
+
106
113
  [project.scripts]
107
114
  killpy = "killpy.__main__:cli"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes