pytest-isolated 0.1.0__py3-none-any.whl → 0.2.0__py3-none-any.whl

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.
@@ -1,3 +1,3 @@
1
1
  """pytest-isolated: Run pytest tests in isolated subprocesses."""
2
2
 
3
- __version__ = "0.1.0"
3
+ __version__ = "0.2.0"
pytest_isolated/plugin.py CHANGED
@@ -6,6 +6,7 @@ import os
6
6
  import subprocess
7
7
  import sys
8
8
  import tempfile
9
+ import time
9
10
  from collections import OrderedDict
10
11
  from pathlib import Path
11
12
  from typing import Any
@@ -21,27 +22,27 @@ SUBPROC_REPORT_PATH = "PYTEST_SUBPROCESS_REPORT_PATH"
21
22
 
22
23
  def pytest_addoption(parser: pytest.Parser) -> None:
23
24
  """Add configuration options for subprocess isolation."""
24
- group = parser.getgroup("subprocess")
25
+ group = parser.getgroup("isolated")
25
26
  group.addoption(
26
- "--subprocess-timeout",
27
+ "--isolated-timeout",
27
28
  type=int,
28
29
  default=None,
29
- help="Timeout in seconds for subprocess groups (default: 300)",
30
+ help="Timeout in seconds for isolated test groups (default: 300)",
30
31
  )
31
32
  group.addoption(
32
- "--no-subprocess",
33
+ "--no-isolation",
33
34
  action="store_true",
34
35
  default=False,
35
36
  help="Disable subprocess isolation (for debugging)",
36
37
  )
37
38
  parser.addini(
38
- "subprocess_timeout",
39
+ "isolated_timeout",
39
40
  type="string",
40
41
  default="300",
41
- help="Default timeout in seconds for subprocess groups",
42
+ help="Default timeout in seconds for isolated test groups",
42
43
  )
43
44
  parser.addini(
44
- "subprocess_capture_passed",
45
+ "isolated_capture_passed",
45
46
  type="bool",
46
47
  default=False,
47
48
  help="Capture output for passed tests (default: False)",
@@ -51,8 +52,9 @@ def pytest_addoption(parser: pytest.Parser) -> None:
51
52
  def pytest_configure(config: pytest.Config) -> None:
52
53
  config.addinivalue_line(
53
54
  "markers",
54
- "subprocess(group=None): run this test in a grouped fresh Python subprocess; "
55
- "tests with the same group run together in one subprocess.",
55
+ "isolated(group=None, timeout=None): run this test in a grouped "
56
+ "fresh Python subprocess; tests with the same group run together in "
57
+ "one subprocess. timeout (seconds) overrides global --isolated-timeout.",
56
58
  )
57
59
 
58
60
 
@@ -100,17 +102,18 @@ def pytest_collection_modifyitems(
100
102
  if os.environ.get(SUBPROC_ENV) == "1":
101
103
  return # child should not do grouping
102
104
 
103
- # If --no-subprocess is set, treat all tests as normal (no subprocess isolation)
104
- if config.getoption("no_subprocess", False):
105
+ # If --no-isolation is set, treat all tests as normal (no subprocess isolation)
106
+ if config.getoption("no_isolation", False):
105
107
  config._subprocess_groups = OrderedDict() # type: ignore[attr-defined]
106
108
  config._subprocess_normal_items = items # type: ignore[attr-defined]
107
109
  return
108
110
 
109
111
  groups: OrderedDict[str, list[pytest.Item]] = OrderedDict()
112
+ group_timeouts: dict[str, int | None] = {} # Track timeout per group
110
113
  normal: list[pytest.Item] = []
111
114
 
112
115
  for item in items:
113
- m = item.get_closest_marker("subprocess")
116
+ m = item.get_closest_marker("isolated")
114
117
  if not m:
115
118
  normal.append(item)
116
119
  continue
@@ -119,9 +122,16 @@ def pytest_collection_modifyitems(
119
122
  # Default grouping to module path (so you don't accidentally group everything)
120
123
  if group is None:
121
124
  group = item.nodeid.split("::")[0]
122
- groups.setdefault(str(group), []).append(item)
125
+
126
+ # Store group-specific timeout (first marker wins)
127
+ group_key = str(group)
128
+ if group_key not in group_timeouts:
129
+ group_timeouts[group_key] = m.kwargs.get("timeout")
130
+
131
+ groups.setdefault(group_key, []).append(item)
123
132
 
124
133
  config._subprocess_groups = groups # type: ignore[attr-defined]
134
+ config._subprocess_group_timeouts = group_timeouts # type: ignore[attr-defined]
125
135
  config._subprocess_normal_items = normal # type: ignore[attr-defined]
126
136
 
127
137
 
@@ -143,17 +153,20 @@ def pytest_runtestloop(session: pytest.Session) -> int | None:
143
153
  groups: OrderedDict[str, list[pytest.Item]] = getattr(
144
154
  config, "_subprocess_groups", OrderedDict()
145
155
  )
156
+ group_timeouts: dict[str, int | None] = getattr(
157
+ config, "_subprocess_group_timeouts", {}
158
+ )
146
159
  normal_items: list[pytest.Item] = getattr(
147
160
  config, "_subprocess_normal_items", session.items
148
161
  )
149
162
 
150
- # Get timeout configuration
151
- timeout_opt = config.getoption("subprocess_timeout", None)
152
- timeout_ini = config.getini("subprocess_timeout")
153
- timeout = timeout_opt or (int(timeout_ini) if timeout_ini else 300)
163
+ # Get default timeout configuration
164
+ timeout_opt = config.getoption("isolated_timeout", None)
165
+ timeout_ini = config.getini("isolated_timeout")
166
+ default_timeout = timeout_opt or (int(timeout_ini) if timeout_ini else 300)
154
167
 
155
168
  # Get capture configuration
156
- capture_passed = config.getini("subprocess_capture_passed")
169
+ capture_passed = config.getini("isolated_capture_passed")
157
170
 
158
171
  def emit_report(
159
172
  item: pytest.Item,
@@ -205,6 +218,9 @@ def pytest_runtestloop(session: pytest.Session) -> int | None:
205
218
  for group_name, group_items in groups.items():
206
219
  nodeids = [it.nodeid for it in group_items]
207
220
 
221
+ # Get timeout for this group (marker timeout > global timeout)
222
+ group_timeout = group_timeouts.get(group_name) or default_timeout
223
+
208
224
  # file where the child will append JSONL records
209
225
  with tempfile.NamedTemporaryFile(
210
226
  prefix="pytest-subproc-", suffix=".jsonl", delete=False
@@ -215,12 +231,13 @@ def pytest_runtestloop(session: pytest.Session) -> int | None:
215
231
  env[SUBPROC_ENV] = "1"
216
232
  env[SUBPROC_REPORT_PATH] = report_path
217
233
 
218
- # Run pytest in subprocess with timeout
234
+ # Run pytest in subprocess with timeout, tracking execution time
219
235
  cmd = [sys.executable, "-m", "pytest", *nodeids]
236
+ start_time = time.time()
220
237
 
221
238
  try:
222
239
  proc = subprocess.run(
223
- cmd, env=env, timeout=timeout, capture_output=False, check=False
240
+ cmd, env=env, timeout=group_timeout, capture_output=False, check=False
224
241
  )
225
242
  returncode = proc.returncode
226
243
  timed_out = False
@@ -228,6 +245,8 @@ def pytest_runtestloop(session: pytest.Session) -> int | None:
228
245
  returncode = -1
229
246
  timed_out = True
230
247
 
248
+ execution_time = time.time() - start_time
249
+
231
250
  # Gather results from JSONL file
232
251
  results: dict[str, dict[str, Any]] = {}
233
252
  report_file = Path(report_path)
@@ -250,9 +269,10 @@ def pytest_runtestloop(session: pytest.Session) -> int | None:
250
269
  # Handle timeout or crash
251
270
  if timed_out:
252
271
  msg = (
253
- f"Subprocess group={group_name!r} timed out after {timeout} "
254
- f"seconds. Increase timeout with --subprocess-timeout or "
255
- f"subprocess_timeout ini option."
272
+ f"Subprocess group={group_name!r} timed out after {group_timeout} "
273
+ f"seconds (execution time: {execution_time:.2f}s). "
274
+ f"Increase timeout with --isolated-timeout, isolated_timeout ini, "
275
+ f"or @pytest.mark.isolated(timeout=N)."
256
276
  )
257
277
  for it in group_items:
258
278
  emit_report(it, "call", "failed", longrepr=msg)
File without changes
@@ -1,13 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pytest-isolated
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Run marked pytest tests in grouped subprocesses (cross-platform).
5
5
  Author: pytest-isolated contributors
6
- License: MIT
6
+ License-Expression: MIT
7
7
  Classifier: Development Status :: 4 - Beta
8
8
  Classifier: Framework :: Pytest
9
9
  Classifier: Intended Audience :: Developers
10
- Classifier: License :: OSI Approved :: MIT License
11
10
  Classifier: Operating System :: OS Independent
12
11
  Classifier: Programming Language :: Python :: 3
13
12
  Classifier: Programming Language :: Python :: 3.11
@@ -27,6 +26,9 @@ Dynamic: license-file
27
26
 
28
27
  # pytest-isolated
29
28
 
29
+ [![Tests](https://github.com/dyollb/pytest-isolated/actions/workflows/test.yml/badge.svg)](https://github.com/dyollb/pytest-isolated/actions/workflows/test.yml)
30
+ [![PyPI](https://img.shields.io/pypi/v/pytest-isolated.svg)](https://pypi.org/project/pytest-isolated/)
31
+
30
32
  A pytest plugin that runs marked tests in isolated subprocesses with intelligent grouping.
31
33
 
32
34
  ## Features
@@ -52,7 +54,7 @@ Mark tests to run in isolated subprocesses:
52
54
  ```python
53
55
  import pytest
54
56
 
55
- @pytest.mark.subprocess
57
+ @pytest.mark.isolated
56
58
  def test_isolated():
57
59
  # Runs in a fresh subprocess
58
60
  assert True
@@ -61,16 +63,25 @@ def test_isolated():
61
63
  Tests with the same group run together in one subprocess:
62
64
 
63
65
  ```python
64
- @pytest.mark.subprocess(group="mygroup")
66
+ @pytest.mark.isolated(group="mygroup")
65
67
  def test_one():
66
68
  shared_state.append(1)
67
69
 
68
- @pytest.mark.subprocess(group="mygroup")
70
+ @pytest.mark.isolated(group="mygroup")
69
71
  def test_two():
70
72
  # Sees state from test_one
71
73
  assert len(shared_state) == 2
72
74
  ```
73
75
 
76
+ Set timeout per test group:
77
+
78
+ ```python
79
+ @pytest.mark.isolated(timeout=30)
80
+ def test_with_timeout():
81
+ # This group gets 30 second timeout (overrides global setting)
82
+ expensive_operation()
83
+ ```
84
+
74
85
  Tests without an explicit group are automatically grouped by module.
75
86
 
76
87
  ## Configuration
@@ -78,30 +89,30 @@ Tests without an explicit group are automatically grouped by module.
78
89
  ### Command Line
79
90
 
80
91
  ```bash
81
- # Set subprocess timeout (seconds)
82
- pytest --subprocess-timeout=60
92
+ # Set isolated test timeout (seconds)
93
+ pytest --isolated-timeout=60
83
94
 
84
95
  # Disable subprocess isolation for debugging
85
- pytest --no-subprocess
96
+ pytest --no-isolation
86
97
 
87
98
  # Combine with pytest debugger
88
- pytest --no-subprocess --pdb
99
+ pytest --no-isolation --pdb
89
100
  ```
90
101
 
91
102
  ### pytest.ini / pyproject.toml
92
103
 
93
104
  ```ini
94
105
  [pytest]
95
- subprocess_timeout = 300
96
- subprocess_capture_passed = false
106
+ isolated_timeout = 300
107
+ isolated_capture_passed = false
97
108
  ```
98
109
 
99
110
  Or in `pyproject.toml`:
100
111
 
101
112
  ```toml
102
113
  [tool.pytest.ini_options]
103
- subprocess_timeout = "300"
104
- subprocess_capture_passed = false
114
+ isolated_timeout = "300"
115
+ isolated_capture_passed = false
105
116
  ```
106
117
 
107
118
  ## Use Cases
@@ -109,13 +120,13 @@ subprocess_capture_passed = false
109
120
  ### Testing Global State
110
121
 
111
122
  ```python
112
- @pytest.mark.subprocess
123
+ @pytest.mark.isolated
113
124
  def test_modifies_environ():
114
125
  import os
115
126
  os.environ["MY_VAR"] = "value"
116
127
  # Won't affect other tests
117
128
 
118
- @pytest.mark.subprocess
129
+ @pytest.mark.isolated
119
130
  def test_clean_environ():
120
131
  import os
121
132
  assert "MY_VAR" not in os.environ
@@ -124,13 +135,13 @@ def test_clean_environ():
124
135
  ### Testing Singletons
125
136
 
126
137
  ```python
127
- @pytest.mark.subprocess(group="singleton_tests")
138
+ @pytest.mark.isolated(group="singleton_tests")
128
139
  def test_singleton_init():
129
140
  from myapp import DatabaseConnection
130
141
  db = DatabaseConnection.get_instance()
131
142
  assert db is not None
132
143
 
133
- @pytest.mark.subprocess(group="singleton_tests")
144
+ @pytest.mark.isolated(group="singleton_tests")
134
145
  def test_singleton_reuse():
135
146
  db = DatabaseConnection.get_instance()
136
147
  # Same instance as previous test in group
@@ -139,7 +150,7 @@ def test_singleton_reuse():
139
150
  ### Testing Process Resources
140
151
 
141
152
  ```python
142
- @pytest.mark.subprocess
153
+ @pytest.mark.isolated
143
154
  def test_signal_handlers():
144
155
  import signal
145
156
  signal.signal(signal.SIGTERM, custom_handler)
@@ -151,7 +162,7 @@ def test_signal_handlers():
151
162
  Failed tests automatically capture and display stdout/stderr:
152
163
 
153
164
  ```python
154
- @pytest.mark.subprocess
165
+ @pytest.mark.isolated
155
166
  def test_failing():
156
167
  print("Debug info")
157
168
  assert False
@@ -167,7 +178,7 @@ pytest --junitxml=report.xml --durations=10
167
178
 
168
179
  **Fixtures**: Module/session fixtures run in each subprocess group. Cannot share fixture objects between parent and subprocess.
169
180
 
170
- **Debugging**: Use `--no-subprocess` to run all tests in the main process for easier debugging with `pdb` or IDE debuggers.
181
+ **Debugging**: Use `--no-isolation` to run all tests in the main process for easier debugging with `pdb` or IDE debuggers.
171
182
 
172
183
  **Performance**: Subprocess creation adds ~100-500ms per group. Group related tests to minimize overhead.
173
184
 
@@ -176,7 +187,7 @@ pytest --junitxml=report.xml --durations=10
176
187
  ### Timeout Handling
177
188
 
178
189
  ```bash
179
- pytest --subprocess-timeout=30
190
+ pytest --isolated-timeout=30
180
191
  ```
181
192
 
182
193
  Timeout errors are clearly reported with the group name and timeout duration.
@@ -197,24 +208,18 @@ if os.environ.get("PYTEST_RUNNING_IN_SUBPROCESS") == "1":
197
208
 
198
209
  ## Troubleshooting
199
210
 
200
- **Tests timing out**: Increase timeout with `--subprocess-timeout=600`
211
+ **Tests timing out**: Increase timeout with `--isolated-timeout=600`
201
212
 
202
- **Missing output**: Enable capture for passed tests with `subprocess_capture_passed = true`
213
+ **Missing output**: Enable capture for passed tests with `isolated_capture_passed = true`
203
214
 
204
215
  **Subprocess crashes**: Check for segfaults, OOM, or signal issues. Run with `-v` for details.
205
216
 
206
- ## License
207
-
208
- MIT License - see LICENSE file for details.
217
+ ## Contributing
209
218
 
210
- ## Changelog
219
+ 1. Install pre-commit: `pip install pre-commit && pre-commit install`
220
+ 1. Run tests: `pytest tests/ -v`
221
+ 1. Open an issue before submitting PRs for new features
211
222
 
212
- ### 0.1.0 (2026-01-12)
223
+ ## License
213
224
 
214
- - Initial release
215
- - Process isolation with subprocess marker
216
- - Smart grouping by module or explicit group names
217
- - Timeout support
218
- - Complete test phase capture (setup/call/teardown)
219
- - JUnit XML and standard reporter integration
220
- - Comprehensive error handling and reporting
225
+ MIT License - see LICENSE file for details.
@@ -0,0 +1,9 @@
1
+ pytest_isolated/__init__.py,sha256=PN_IEdfxCUz6vL1vf0Ka9CGmCq9ppFk33fVGirSVtMc,89
2
+ pytest_isolated/plugin.py,sha256=0AKRTWmdLaiwZdwRicRd3TMLrIeisGBMlFqCDOnJRX0,12206
3
+ pytest_isolated/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ pytest_isolated-0.2.0.dist-info/licenses/LICENSE,sha256=WECJyowi685PZSnKcA4Tqs7jukfzbnk7iMPLnm_q4JI,1067
5
+ pytest_isolated-0.2.0.dist-info/METADATA,sha256=q_E02kvtbnt1QgQc-ATrghLcJka7Y_2Zx50tmatVGCM,5384
6
+ pytest_isolated-0.2.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
7
+ pytest_isolated-0.2.0.dist-info/entry_points.txt,sha256=HgRNPjIGoPBF1pkhma4UtaSwhpOVB8oZRZ0L1FcZXgk,45
8
+ pytest_isolated-0.2.0.dist-info/top_level.txt,sha256=FAtpozhvI-YaiFoZMepi9JAm6e87mW-TM1Ovu5xLOxg,16
9
+ pytest_isolated-0.2.0.dist-info/RECORD,,
@@ -1,8 +0,0 @@
1
- pytest_isolated/__init__.py,sha256=iqPlqv1sT1c10XcWOeYbJyRyIzeQk0Sa-OhOS14IMfg,89
2
- pytest_isolated/plugin.py,sha256=lLNjY5Jm5SmllySvVPRCF87fpQTnRx_RMtloKeM1m9U,11296
3
- pytest_isolated-0.1.0.dist-info/licenses/LICENSE,sha256=WECJyowi685PZSnKcA4Tqs7jukfzbnk7iMPLnm_q4JI,1067
4
- pytest_isolated-0.1.0.dist-info/METADATA,sha256=qfVoG6VGZCb-Dj5Wl12sGQH7WO-U2Gj4KcGV2H_FXzU,5131
5
- pytest_isolated-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
6
- pytest_isolated-0.1.0.dist-info/entry_points.txt,sha256=HgRNPjIGoPBF1pkhma4UtaSwhpOVB8oZRZ0L1FcZXgk,45
7
- pytest_isolated-0.1.0.dist-info/top_level.txt,sha256=FAtpozhvI-YaiFoZMepi9JAm6e87mW-TM1Ovu5xLOxg,16
8
- pytest_isolated-0.1.0.dist-info/RECORD,,