pytest-subprocess 1.5.4__tar.gz → 1.6.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.
- {pytest_subprocess-1.5.4 → pytest_subprocess-1.6.0}/HISTORY.rst +12 -0
- {pytest_subprocess-1.5.4/pytest_subprocess.egg-info → pytest_subprocess-1.6.0}/PKG-INFO +57 -2
- {pytest_subprocess-1.5.4 → pytest_subprocess-1.6.0}/README.rst +44 -1
- {pytest_subprocess-1.5.4 → pytest_subprocess-1.6.0}/pytest_subprocess/fake_popen.py +14 -4
- {pytest_subprocess-1.5.4 → pytest_subprocess-1.6.0}/pytest_subprocess/fake_process.py +2 -0
- {pytest_subprocess-1.5.4 → pytest_subprocess-1.6.0}/pytest_subprocess/utils.py +53 -1
- {pytest_subprocess-1.5.4 → pytest_subprocess-1.6.0/pytest_subprocess.egg-info}/PKG-INFO +57 -2
- {pytest_subprocess-1.5.4 → pytest_subprocess-1.6.0}/setup.py +1 -1
- {pytest_subprocess-1.5.4 → pytest_subprocess-1.6.0}/tests/test_subprocess.py +188 -0
- {pytest_subprocess-1.5.4 → pytest_subprocess-1.6.0}/LICENSE +0 -0
- {pytest_subprocess-1.5.4 → pytest_subprocess-1.6.0}/MANIFEST.in +0 -0
- {pytest_subprocess-1.5.4 → pytest_subprocess-1.6.0}/docs/Makefile +0 -0
- {pytest_subprocess-1.5.4 → pytest_subprocess-1.6.0}/docs/api.rst +0 -0
- {pytest_subprocess-1.5.4 → pytest_subprocess-1.6.0}/docs/conf.py +0 -0
- {pytest_subprocess-1.5.4 → pytest_subprocess-1.6.0}/docs/index.rst +0 -0
- {pytest_subprocess-1.5.4 → pytest_subprocess-1.6.0}/docs/make.bat +0 -0
- {pytest_subprocess-1.5.4 → pytest_subprocess-1.6.0}/docs/usage.rst +0 -0
- {pytest_subprocess-1.5.4 → pytest_subprocess-1.6.0}/pytest.ini +0 -0
- {pytest_subprocess-1.5.4 → pytest_subprocess-1.6.0}/pytest_subprocess/__init__.py +0 -0
- {pytest_subprocess-1.5.4 → pytest_subprocess-1.6.0}/pytest_subprocess/asyncio_subprocess.py +0 -0
- {pytest_subprocess-1.5.4 → pytest_subprocess-1.6.0}/pytest_subprocess/exceptions.py +0 -0
- {pytest_subprocess-1.5.4 → pytest_subprocess-1.6.0}/pytest_subprocess/fixtures.py +0 -0
- {pytest_subprocess-1.5.4 → pytest_subprocess-1.6.0}/pytest_subprocess/process_dispatcher.py +0 -0
- {pytest_subprocess-1.5.4 → pytest_subprocess-1.6.0}/pytest_subprocess/process_recorder.py +0 -0
- {pytest_subprocess-1.5.4 → pytest_subprocess-1.6.0}/pytest_subprocess/py.typed +0 -0
- {pytest_subprocess-1.5.4 → pytest_subprocess-1.6.0}/pytest_subprocess/types.py +0 -0
- {pytest_subprocess-1.5.4 → pytest_subprocess-1.6.0}/pytest_subprocess.egg-info/SOURCES.txt +0 -0
- {pytest_subprocess-1.5.4 → pytest_subprocess-1.6.0}/pytest_subprocess.egg-info/dependency_links.txt +0 -0
- {pytest_subprocess-1.5.4 → pytest_subprocess-1.6.0}/pytest_subprocess.egg-info/entry_points.txt +0 -0
- {pytest_subprocess-1.5.4 → pytest_subprocess-1.6.0}/pytest_subprocess.egg-info/requires.txt +0 -0
- {pytest_subprocess-1.5.4 → pytest_subprocess-1.6.0}/pytest_subprocess.egg-info/top_level.txt +0 -0
- {pytest_subprocess-1.5.4 → pytest_subprocess-1.6.0}/setup.cfg +0 -0
- {pytest_subprocess-1.5.4 → pytest_subprocess-1.6.0}/tests/__init__.py +0 -0
- {pytest_subprocess-1.5.4 → pytest_subprocess-1.6.0}/tests/conftest.py +0 -0
- {pytest_subprocess-1.5.4 → pytest_subprocess-1.6.0}/tests/example_script.py +0 -0
- {pytest_subprocess-1.5.4 → pytest_subprocess-1.6.0}/tests/imported_popen_fixture.py +0 -0
- {pytest_subprocess-1.5.4 → pytest_subprocess-1.6.0}/tests/test_asyncio.py +0 -0
- {pytest_subprocess-1.5.4 → pytest_subprocess-1.6.0}/tests/test_examples.py +0 -0
- {pytest_subprocess-1.5.4 → pytest_subprocess-1.6.0}/tests/test_typing.py +0 -0
- {pytest_subprocess-1.5.4 → pytest_subprocess-1.6.0}/tests/test_utils.py +0 -0
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
History
|
|
2
2
|
=======
|
|
3
3
|
|
|
4
|
+
1.6.0 (2026-05-10)
|
|
5
|
+
------------------
|
|
6
|
+
|
|
7
|
+
Features
|
|
8
|
+
~~~~~~~~
|
|
9
|
+
* `#206 <https://github.com/aklajnert/pytest-subprocess/pull/206>`_: Add fp.regex() for regex-based command argument matching.
|
|
10
|
+
|
|
11
|
+
Bug fixes
|
|
12
|
+
~~~~~~~~~
|
|
13
|
+
* `#207 <https://github.com/aklajnert/pytest-subprocess/pull/207>`_: Fix poll() not reflecting returncode when a callback is registered.
|
|
14
|
+
* `#205 <https://github.com/aklajnert/pytest-subprocess/pull/205>`_: Fix stdin not respecting text mode when text=True or encoding is set.
|
|
15
|
+
|
|
4
16
|
1.5.4 (2026-03-21)
|
|
5
17
|
------------------
|
|
6
18
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pytest-subprocess
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.6.0
|
|
4
4
|
Summary: A plugin to fake subprocess for pytest
|
|
5
5
|
Author: Andrzej Klajnert
|
|
6
6
|
Author-email: python@aklajnert.pl
|
|
@@ -377,10 +377,53 @@ the same name, regardless of the location. This is accomplished with
|
|
|
377
377
|
|
|
378
378
|
def test_any_matching_program(fp):
|
|
379
379
|
# define a command that can come from anywhere
|
|
380
|
-
|
|
380
|
+
executable_name = os.path.basename(sys.executable)
|
|
381
|
+
fp.register([fp.program(executable_name)])
|
|
381
382
|
assert subprocess.check_call(sys.executable) == 0
|
|
382
383
|
|
|
383
384
|
|
|
385
|
+
Regex argument matching
|
|
386
|
+
-----------------------
|
|
387
|
+
|
|
388
|
+
When a command argument has a predictable *structure* but an unpredictable
|
|
389
|
+
*value* (e.g. a path determined at runtime), you can match it with a regular
|
|
390
|
+
expression using ``fp.regex()``:
|
|
391
|
+
|
|
392
|
+
.. code-block:: python
|
|
393
|
+
|
|
394
|
+
def test_cmake_varying_paths(fp):
|
|
395
|
+
# Register once; the pattern matches any -S<path> and -B<path> argument.
|
|
396
|
+
fp.register(
|
|
397
|
+
["cmake", fp.regex(r"-S.+"), fp.regex(r"-B.+")],
|
|
398
|
+
occurrences=2,
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
# Both calls are matched, even though the paths differ.
|
|
402
|
+
subprocess.run(
|
|
403
|
+
["cmake", "-S/tmp/generated_data", "-B/tmp/build_1234"]
|
|
404
|
+
)
|
|
405
|
+
subprocess.run(["cmake", "-S/other/source", "-B/other/build_5678"])
|
|
406
|
+
|
|
407
|
+
# This would raise ProcessNotRegisteredError — different subcommand:
|
|
408
|
+
# subprocess.run(["cmake", "--build", "/tmp/build_1234"])
|
|
409
|
+
|
|
410
|
+
``fp.regex()`` accepts an optional second argument for ``re`` module flags:
|
|
411
|
+
|
|
412
|
+
.. code-block:: python
|
|
413
|
+
|
|
414
|
+
import re
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def test_case_insensitive(fp):
|
|
418
|
+
fp.register(["git", fp.regex(r"commit", re.IGNORECASE)])
|
|
419
|
+
subprocess.run(["git", "COMMIT"]) # matches
|
|
420
|
+
|
|
421
|
+
.. note::
|
|
422
|
+
``fp.regex()`` uses ``re.fullmatch()``, so the pattern must cover the
|
|
423
|
+
**entire** argument string. Use ``.*`` at the start or end if you need
|
|
424
|
+
partial matching.
|
|
425
|
+
|
|
426
|
+
|
|
384
427
|
Check if process was called
|
|
385
428
|
---------------------------
|
|
386
429
|
|
|
@@ -555,6 +598,18 @@ This `pytest`_ plugin was generated with `Cookiecutter`_ along with `@hackebrot`
|
|
|
555
598
|
History
|
|
556
599
|
=======
|
|
557
600
|
|
|
601
|
+
1.6.0 (2026-05-10)
|
|
602
|
+
------------------
|
|
603
|
+
|
|
604
|
+
Features
|
|
605
|
+
~~~~~~~~
|
|
606
|
+
* `#206 <https://github.com/aklajnert/pytest-subprocess/pull/206>`_: Add fp.regex() for regex-based command argument matching.
|
|
607
|
+
|
|
608
|
+
Bug fixes
|
|
609
|
+
~~~~~~~~~
|
|
610
|
+
* `#207 <https://github.com/aklajnert/pytest-subprocess/pull/207>`_: Fix poll() not reflecting returncode when a callback is registered.
|
|
611
|
+
* `#205 <https://github.com/aklajnert/pytest-subprocess/pull/205>`_: Fix stdin not respecting text mode when text=True or encoding is set.
|
|
612
|
+
|
|
558
613
|
1.5.4 (2026-03-21)
|
|
559
614
|
------------------
|
|
560
615
|
|
|
@@ -311,10 +311,53 @@ the same name, regardless of the location. This is accomplished with
|
|
|
311
311
|
|
|
312
312
|
def test_any_matching_program(fp):
|
|
313
313
|
# define a command that can come from anywhere
|
|
314
|
-
|
|
314
|
+
executable_name = os.path.basename(sys.executable)
|
|
315
|
+
fp.register([fp.program(executable_name)])
|
|
315
316
|
assert subprocess.check_call(sys.executable) == 0
|
|
316
317
|
|
|
317
318
|
|
|
319
|
+
Regex argument matching
|
|
320
|
+
-----------------------
|
|
321
|
+
|
|
322
|
+
When a command argument has a predictable *structure* but an unpredictable
|
|
323
|
+
*value* (e.g. a path determined at runtime), you can match it with a regular
|
|
324
|
+
expression using ``fp.regex()``:
|
|
325
|
+
|
|
326
|
+
.. code-block:: python
|
|
327
|
+
|
|
328
|
+
def test_cmake_varying_paths(fp):
|
|
329
|
+
# Register once; the pattern matches any -S<path> and -B<path> argument.
|
|
330
|
+
fp.register(
|
|
331
|
+
["cmake", fp.regex(r"-S.+"), fp.regex(r"-B.+")],
|
|
332
|
+
occurrences=2,
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
# Both calls are matched, even though the paths differ.
|
|
336
|
+
subprocess.run(
|
|
337
|
+
["cmake", "-S/tmp/generated_data", "-B/tmp/build_1234"]
|
|
338
|
+
)
|
|
339
|
+
subprocess.run(["cmake", "-S/other/source", "-B/other/build_5678"])
|
|
340
|
+
|
|
341
|
+
# This would raise ProcessNotRegisteredError — different subcommand:
|
|
342
|
+
# subprocess.run(["cmake", "--build", "/tmp/build_1234"])
|
|
343
|
+
|
|
344
|
+
``fp.regex()`` accepts an optional second argument for ``re`` module flags:
|
|
345
|
+
|
|
346
|
+
.. code-block:: python
|
|
347
|
+
|
|
348
|
+
import re
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def test_case_insensitive(fp):
|
|
352
|
+
fp.register(["git", fp.regex(r"commit", re.IGNORECASE)])
|
|
353
|
+
subprocess.run(["git", "COMMIT"]) # matches
|
|
354
|
+
|
|
355
|
+
.. note::
|
|
356
|
+
``fp.regex()`` uses ``re.fullmatch()``, so the pattern must cover the
|
|
357
|
+
**entire** argument string. Use ``.*`` at the start or end if you need
|
|
358
|
+
partial matching.
|
|
359
|
+
|
|
360
|
+
|
|
318
361
|
Check if process was called
|
|
319
362
|
---------------------------
|
|
320
363
|
|
|
@@ -139,6 +139,16 @@ class FakePopen:
|
|
|
139
139
|
return None
|
|
140
140
|
|
|
141
141
|
def poll(self) -> Optional[int]:
|
|
142
|
+
if (
|
|
143
|
+
self.returncode is None
|
|
144
|
+
and self.__thread is not None
|
|
145
|
+
and not self.__thread.is_alive()
|
|
146
|
+
):
|
|
147
|
+
# The callback/wait thread has finished but _finalize_thread() has
|
|
148
|
+
# not been called yet (e.g. the caller uses poll() instead of
|
|
149
|
+
# wait()/communicate()). Finalise now so returncode is updated,
|
|
150
|
+
# mirroring real subprocess.Popen.poll() behaviour.
|
|
151
|
+
self._finalize_thread(None)
|
|
142
152
|
return self.returncode
|
|
143
153
|
|
|
144
154
|
def wait(self, timeout: Optional[float] = None) -> int:
|
|
@@ -170,10 +180,6 @@ class FakePopen:
|
|
|
170
180
|
self.__kwargs = self.safe_copy(kwargs)
|
|
171
181
|
self.__universal_newlines = kwargs.get("universal_newlines", None)
|
|
172
182
|
|
|
173
|
-
stdin = kwargs.get("stdin")
|
|
174
|
-
if stdin == subprocess.PIPE:
|
|
175
|
-
self.stdin = self._get_empty_buffer(False)
|
|
176
|
-
|
|
177
183
|
text = kwargs.get("text", None)
|
|
178
184
|
encoding = kwargs.get("encoding", None)
|
|
179
185
|
errors = kwargs.get("errors", None)
|
|
@@ -183,6 +189,10 @@ class FakePopen:
|
|
|
183
189
|
|
|
184
190
|
self.text_mode = bool(text or self.__universal_newlines or encoding or errors)
|
|
185
191
|
|
|
192
|
+
stdin = kwargs.get("stdin")
|
|
193
|
+
if stdin == subprocess.PIPE:
|
|
194
|
+
self.stdin = self._get_empty_buffer(self.text_mode)
|
|
195
|
+
|
|
186
196
|
# validation taken from the real subprocess
|
|
187
197
|
if (
|
|
188
198
|
text is not None
|
|
@@ -21,6 +21,7 @@ from .types import OPTIONAL_TEXT_OR_ITERABLE
|
|
|
21
21
|
from .utils import Any
|
|
22
22
|
from .utils import Command
|
|
23
23
|
from .utils import Program
|
|
24
|
+
from .utils import Regex
|
|
24
25
|
|
|
25
26
|
|
|
26
27
|
class FakeProcess:
|
|
@@ -28,6 +29,7 @@ class FakeProcess:
|
|
|
28
29
|
|
|
29
30
|
any: ClassVar[Type[Any]] = Any
|
|
30
31
|
program: ClassVar[Type[Program]] = Program
|
|
32
|
+
regex: ClassVar[Type[Regex]] = Regex
|
|
31
33
|
|
|
32
34
|
def __init__(self) -> None:
|
|
33
35
|
self.definitions: DefaultDict[Command, Deque[Union[Dict, bool]]] = defaultdict(
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import os
|
|
2
|
+
import re
|
|
2
3
|
import shlex
|
|
3
4
|
import sys
|
|
4
5
|
import threading
|
|
@@ -15,7 +16,7 @@ from typing import Union
|
|
|
15
16
|
if TYPE_CHECKING:
|
|
16
17
|
from .types import COMMAND
|
|
17
18
|
|
|
18
|
-
ARGUMENT = Union[str, "Any", os.PathLike, "Program"]
|
|
19
|
+
ARGUMENT = Union[str, "Any", "Regex", os.PathLike, "Program"]
|
|
19
20
|
|
|
20
21
|
|
|
21
22
|
class Thread(threading.Thread):
|
|
@@ -189,3 +190,54 @@ class Program:
|
|
|
189
190
|
|
|
190
191
|
def __hash__(self) -> int:
|
|
191
192
|
return hash(self.program)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
class Regex:
|
|
196
|
+
"""Match a single command argument against a regular expression.
|
|
197
|
+
|
|
198
|
+
Uses :func:`re.fullmatch`, so the pattern must cover the *entire* argument
|
|
199
|
+
string. Use ``.*`` at the start or end if partial matching is desired.
|
|
200
|
+
|
|
201
|
+
Example::
|
|
202
|
+
|
|
203
|
+
def test_cmake_varying_paths(fp):
|
|
204
|
+
fp.register(["cmake", fp.regex(r"-S.+"), fp.regex(r"-B.+")])
|
|
205
|
+
|
|
206
|
+
# All of these would match:
|
|
207
|
+
subprocess.run(["cmake", "-S/tmp/src", "-B/tmp/build"])
|
|
208
|
+
subprocess.run(["cmake", "-S/other/src", "-B/other/build"])
|
|
209
|
+
|
|
210
|
+
# This would NOT match (wrong argument order or missing args):
|
|
211
|
+
# subprocess.run(["cmake", "-B/tmp/build", "-S/tmp/src"])
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
pattern: Regular expression pattern to match against.
|
|
215
|
+
flags: Optional :mod:`re` flags (e.g. ``re.IGNORECASE``).
|
|
216
|
+
"""
|
|
217
|
+
|
|
218
|
+
def __init__(self, pattern: str, flags: int = 0) -> None:
|
|
219
|
+
self._compiled = re.compile(pattern, flags)
|
|
220
|
+
|
|
221
|
+
@property
|
|
222
|
+
def pattern(self) -> str:
|
|
223
|
+
"""The original pattern string."""
|
|
224
|
+
return self._compiled.pattern
|
|
225
|
+
|
|
226
|
+
@property
|
|
227
|
+
def flags(self) -> int:
|
|
228
|
+
"""The compiled regex flags."""
|
|
229
|
+
return self._compiled.flags
|
|
230
|
+
|
|
231
|
+
def __eq__(self, other: AnyType) -> bool:
|
|
232
|
+
if isinstance(other, str):
|
|
233
|
+
return bool(self._compiled.fullmatch(other))
|
|
234
|
+
return NotImplemented
|
|
235
|
+
|
|
236
|
+
def __hash__(self) -> int:
|
|
237
|
+
return hash((self._compiled.pattern, self._compiled.flags))
|
|
238
|
+
|
|
239
|
+
def __repr__(self) -> str:
|
|
240
|
+
return f"Regex({self._compiled.pattern!r})"
|
|
241
|
+
|
|
242
|
+
def __str__(self) -> str:
|
|
243
|
+
return repr(self)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pytest-subprocess
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.6.0
|
|
4
4
|
Summary: A plugin to fake subprocess for pytest
|
|
5
5
|
Author: Andrzej Klajnert
|
|
6
6
|
Author-email: python@aklajnert.pl
|
|
@@ -377,10 +377,53 @@ the same name, regardless of the location. This is accomplished with
|
|
|
377
377
|
|
|
378
378
|
def test_any_matching_program(fp):
|
|
379
379
|
# define a command that can come from anywhere
|
|
380
|
-
|
|
380
|
+
executable_name = os.path.basename(sys.executable)
|
|
381
|
+
fp.register([fp.program(executable_name)])
|
|
381
382
|
assert subprocess.check_call(sys.executable) == 0
|
|
382
383
|
|
|
383
384
|
|
|
385
|
+
Regex argument matching
|
|
386
|
+
-----------------------
|
|
387
|
+
|
|
388
|
+
When a command argument has a predictable *structure* but an unpredictable
|
|
389
|
+
*value* (e.g. a path determined at runtime), you can match it with a regular
|
|
390
|
+
expression using ``fp.regex()``:
|
|
391
|
+
|
|
392
|
+
.. code-block:: python
|
|
393
|
+
|
|
394
|
+
def test_cmake_varying_paths(fp):
|
|
395
|
+
# Register once; the pattern matches any -S<path> and -B<path> argument.
|
|
396
|
+
fp.register(
|
|
397
|
+
["cmake", fp.regex(r"-S.+"), fp.regex(r"-B.+")],
|
|
398
|
+
occurrences=2,
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
# Both calls are matched, even though the paths differ.
|
|
402
|
+
subprocess.run(
|
|
403
|
+
["cmake", "-S/tmp/generated_data", "-B/tmp/build_1234"]
|
|
404
|
+
)
|
|
405
|
+
subprocess.run(["cmake", "-S/other/source", "-B/other/build_5678"])
|
|
406
|
+
|
|
407
|
+
# This would raise ProcessNotRegisteredError — different subcommand:
|
|
408
|
+
# subprocess.run(["cmake", "--build", "/tmp/build_1234"])
|
|
409
|
+
|
|
410
|
+
``fp.regex()`` accepts an optional second argument for ``re`` module flags:
|
|
411
|
+
|
|
412
|
+
.. code-block:: python
|
|
413
|
+
|
|
414
|
+
import re
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def test_case_insensitive(fp):
|
|
418
|
+
fp.register(["git", fp.regex(r"commit", re.IGNORECASE)])
|
|
419
|
+
subprocess.run(["git", "COMMIT"]) # matches
|
|
420
|
+
|
|
421
|
+
.. note::
|
|
422
|
+
``fp.regex()`` uses ``re.fullmatch()``, so the pattern must cover the
|
|
423
|
+
**entire** argument string. Use ``.*`` at the start or end if you need
|
|
424
|
+
partial matching.
|
|
425
|
+
|
|
426
|
+
|
|
384
427
|
Check if process was called
|
|
385
428
|
---------------------------
|
|
386
429
|
|
|
@@ -555,6 +598,18 @@ This `pytest`_ plugin was generated with `Cookiecutter`_ along with `@hackebrot`
|
|
|
555
598
|
History
|
|
556
599
|
=======
|
|
557
600
|
|
|
601
|
+
1.6.0 (2026-05-10)
|
|
602
|
+
------------------
|
|
603
|
+
|
|
604
|
+
Features
|
|
605
|
+
~~~~~~~~
|
|
606
|
+
* `#206 <https://github.com/aklajnert/pytest-subprocess/pull/206>`_: Add fp.regex() for regex-based command argument matching.
|
|
607
|
+
|
|
608
|
+
Bug fixes
|
|
609
|
+
~~~~~~~~~
|
|
610
|
+
* `#207 <https://github.com/aklajnert/pytest-subprocess/pull/207>`_: Fix poll() not reflecting returncode when a callback is registered.
|
|
611
|
+
* `#205 <https://github.com/aklajnert/pytest-subprocess/pull/205>`_: Fix stdin not respecting text mode when text=True or encoding is set.
|
|
612
|
+
|
|
558
613
|
1.5.4 (2026-03-21)
|
|
559
614
|
------------------
|
|
560
615
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import contextlib
|
|
2
2
|
import getpass
|
|
3
|
+
import io
|
|
3
4
|
import os
|
|
4
5
|
import platform
|
|
5
6
|
import signal
|
|
@@ -913,6 +914,49 @@ def test_callback_and_return_code(fp):
|
|
|
913
914
|
assert process.returncode == 5
|
|
914
915
|
|
|
915
916
|
|
|
917
|
+
def test_poll_reflects_returncode_after_callback(fp):
|
|
918
|
+
"""
|
|
919
|
+
poll() must return the registered returncode once the callback thread has
|
|
920
|
+
finished, without requiring the caller to invoke wait() or communicate().
|
|
921
|
+
|
|
922
|
+
Before the fix, poll() always returned None when a callback was registered.
|
|
923
|
+
"""
|
|
924
|
+
|
|
925
|
+
def slow_callback(process):
|
|
926
|
+
time.sleep(0.1)
|
|
927
|
+
|
|
928
|
+
fp.register(["my-tool"], returncode=1, callback=slow_callback)
|
|
929
|
+
proc = subprocess.Popen(["my-tool"])
|
|
930
|
+
|
|
931
|
+
# While the callback is still running, poll() must return None.
|
|
932
|
+
assert proc.poll() is None
|
|
933
|
+
|
|
934
|
+
# Wait for the callback thread to finish, then poll() must return the
|
|
935
|
+
# registered returncode.
|
|
936
|
+
proc.wait()
|
|
937
|
+
assert proc.poll() == 1
|
|
938
|
+
|
|
939
|
+
|
|
940
|
+
def test_poll_reflects_returncode_after_callback_polling_loop(fp):
|
|
941
|
+
"""
|
|
942
|
+
Callers that spin on proc.poll() is None (a common real-subprocess idiom)
|
|
943
|
+
must eventually observe a non-None returncode once the callback finishes.
|
|
944
|
+
"""
|
|
945
|
+
|
|
946
|
+
def callback_with_delay(process):
|
|
947
|
+
time.sleep(0.05)
|
|
948
|
+
|
|
949
|
+
fp.register(["my-tool"], returncode=42, callback=callback_with_delay)
|
|
950
|
+
proc = subprocess.Popen(["my-tool"])
|
|
951
|
+
|
|
952
|
+
deadline = time.monotonic() + 5.0 # generous upper bound
|
|
953
|
+
while proc.poll() is None:
|
|
954
|
+
assert time.monotonic() < deadline, "poll() never reflected returncode"
|
|
955
|
+
time.sleep(0.01)
|
|
956
|
+
|
|
957
|
+
assert proc.returncode == 42
|
|
958
|
+
|
|
959
|
+
|
|
916
960
|
@pytest.mark.skipif(
|
|
917
961
|
sys.version_info <= (3, 6),
|
|
918
962
|
reason="encoding and errors has been introduced in 3.6",
|
|
@@ -1351,6 +1395,62 @@ def test_stdin_pipe(fp):
|
|
|
1351
1395
|
process.stdin.write(b"more data")
|
|
1352
1396
|
|
|
1353
1397
|
|
|
1398
|
+
def test_stdin_pipe_text_mode(fp):
|
|
1399
|
+
"""
|
|
1400
|
+
Test that stdin is a StringIO (text) buffer when using
|
|
1401
|
+
subprocess.PIPE with text=True.
|
|
1402
|
+
|
|
1403
|
+
From GitHub #204
|
|
1404
|
+
"""
|
|
1405
|
+
fp.register(["my-command"], stdout="hello\n")
|
|
1406
|
+
|
|
1407
|
+
process = subprocess.Popen(
|
|
1408
|
+
["my-command"],
|
|
1409
|
+
stdin=subprocess.PIPE,
|
|
1410
|
+
stdout=subprocess.PIPE,
|
|
1411
|
+
stderr=subprocess.PIPE,
|
|
1412
|
+
text=True,
|
|
1413
|
+
)
|
|
1414
|
+
|
|
1415
|
+
assert process.stdin is not None
|
|
1416
|
+
assert isinstance(process.stdin, io.StringIO)
|
|
1417
|
+
assert process.stdin.writable()
|
|
1418
|
+
|
|
1419
|
+
# Should accept str, not bytes, in text mode.
|
|
1420
|
+
process.stdin.write("some input")
|
|
1421
|
+
process.stdin.flush()
|
|
1422
|
+
|
|
1423
|
+
process.stdin.seek(0)
|
|
1424
|
+
assert process.stdin.read() == "some input"
|
|
1425
|
+
|
|
1426
|
+
process.stdin.close()
|
|
1427
|
+
with pytest.raises(ValueError):
|
|
1428
|
+
process.stdin.write("more data")
|
|
1429
|
+
|
|
1430
|
+
|
|
1431
|
+
def test_stdin_pipe_encoding_mode(fp):
|
|
1432
|
+
"""
|
|
1433
|
+
Test that stdin is a StringIO (text) buffer when
|
|
1434
|
+
using subprocess.PIPE with encoding set.
|
|
1435
|
+
|
|
1436
|
+
From GitHub #204
|
|
1437
|
+
"""
|
|
1438
|
+
fp.register(["my-command"])
|
|
1439
|
+
|
|
1440
|
+
process = subprocess.Popen(
|
|
1441
|
+
["my-command"],
|
|
1442
|
+
stdin=subprocess.PIPE,
|
|
1443
|
+
encoding="utf-8",
|
|
1444
|
+
)
|
|
1445
|
+
|
|
1446
|
+
assert process.stdin is not None
|
|
1447
|
+
assert isinstance(process.stdin, io.StringIO)
|
|
1448
|
+
assert process.stdin.writable()
|
|
1449
|
+
|
|
1450
|
+
process.stdin.write("some input")
|
|
1451
|
+
process.stdin.close()
|
|
1452
|
+
|
|
1453
|
+
|
|
1354
1454
|
def test_stdout_stderr_as_file_bug(fp):
|
|
1355
1455
|
"""
|
|
1356
1456
|
Test that no TypeError is raised when stdout/stderr is a file
|
|
@@ -1401,3 +1501,91 @@ def test_imported_popen_is_patched(fp):
|
|
|
1401
1501
|
|
|
1402
1502
|
assert process.returncode == 0
|
|
1403
1503
|
assert out == b"\x00"
|
|
1504
|
+
|
|
1505
|
+
|
|
1506
|
+
# ---------------------------------------------------------------------------
|
|
1507
|
+
# Regex command matching – issue #154
|
|
1508
|
+
# ---------------------------------------------------------------------------
|
|
1509
|
+
|
|
1510
|
+
|
|
1511
|
+
def test_regex_matches_varying_argument(fp):
|
|
1512
|
+
"""
|
|
1513
|
+
fp.regex() must match a single command argument against a regex pattern,
|
|
1514
|
+
allowing commands with variable argument values to be registered once.
|
|
1515
|
+
"""
|
|
1516
|
+
fp.register(["cmake", fp.regex(r"-S.+"), fp.regex(r"-B.+")], occurrences=2)
|
|
1517
|
+
|
|
1518
|
+
# Both calls use the same registration; argument values differ.
|
|
1519
|
+
proc1 = subprocess.run(["cmake", "-S/tmp/source1", "-B/tmp/build1"])
|
|
1520
|
+
proc2 = subprocess.run(["cmake", "-S/other/source", "-B/other/build"])
|
|
1521
|
+
|
|
1522
|
+
assert proc1.returncode == 0
|
|
1523
|
+
assert proc2.returncode == 0
|
|
1524
|
+
|
|
1525
|
+
|
|
1526
|
+
def test_regex_does_not_match_wrong_argument(fp):
|
|
1527
|
+
"""
|
|
1528
|
+
Commands whose arguments do NOT match the registered regex pattern must
|
|
1529
|
+
raise ProcessNotRegisteredError, not silently succeed.
|
|
1530
|
+
"""
|
|
1531
|
+
fp.register(["cmake", fp.regex(r"-S.+"), fp.regex(r"-B.+")])
|
|
1532
|
+
|
|
1533
|
+
# Wrong subcommand — does not match the -S/-B pattern.
|
|
1534
|
+
with pytest.raises(fp.exceptions.ProcessNotRegisteredError):
|
|
1535
|
+
subprocess.run(["cmake", "--build", "/tmp/build"])
|
|
1536
|
+
|
|
1537
|
+
|
|
1538
|
+
def test_regex_case_insensitive_flag(fp):
|
|
1539
|
+
"""
|
|
1540
|
+
fp.regex() must accept re module flags such as re.IGNORECASE.
|
|
1541
|
+
"""
|
|
1542
|
+
import re
|
|
1543
|
+
|
|
1544
|
+
fp.register(["git", fp.regex(r"commit", re.IGNORECASE)], stdout="sha: abc123\n")
|
|
1545
|
+
|
|
1546
|
+
# Exact case
|
|
1547
|
+
proc1 = subprocess.run(["git", "commit"], capture_output=True)
|
|
1548
|
+
assert proc1.stdout == b"sha: abc123\n"
|
|
1549
|
+
|
|
1550
|
+
# Different case
|
|
1551
|
+
fp.register(["git", fp.regex(r"commit", re.IGNORECASE)], stdout="sha: abc123\n")
|
|
1552
|
+
proc2 = subprocess.run(["git", "COMMIT"], capture_output=True)
|
|
1553
|
+
assert proc2.stdout == b"sha: abc123\n"
|
|
1554
|
+
|
|
1555
|
+
|
|
1556
|
+
def test_regex_fullmatch_semantics(fp):
|
|
1557
|
+
"""
|
|
1558
|
+
fp.regex() uses fullmatch: the pattern must cover the *entire* argument
|
|
1559
|
+
string. A pattern that only matches a substring must NOT match.
|
|
1560
|
+
"""
|
|
1561
|
+
# "-S" alone does not fullmatch "-S/some/path" (pattern too short)
|
|
1562
|
+
fp.register(["cmake", fp.regex(r"-S"), fp.regex(r"-B")])
|
|
1563
|
+
with pytest.raises(fp.exceptions.ProcessNotRegisteredError):
|
|
1564
|
+
# "-S/tmp/source" does not fullmatch the pattern "-S" (no extra chars)
|
|
1565
|
+
subprocess.run(["cmake", "-S/tmp/source", "-B/tmp/build"])
|
|
1566
|
+
|
|
1567
|
+
|
|
1568
|
+
def test_regex_repr(fp):
|
|
1569
|
+
"""
|
|
1570
|
+
fp.regex() instances have a helpful repr for debugging.
|
|
1571
|
+
"""
|
|
1572
|
+
r = fp.regex(r"-S.+")
|
|
1573
|
+
assert "-S.+" in repr(r)
|
|
1574
|
+
|
|
1575
|
+
|
|
1576
|
+
def test_regex_combined_with_any(fp):
|
|
1577
|
+
"""
|
|
1578
|
+
fp.regex() can be combined freely with fp.any() and exact strings in
|
|
1579
|
+
the same command registration.
|
|
1580
|
+
"""
|
|
1581
|
+
# Exact "cmake", then any number of arguments matching the -D prefix
|
|
1582
|
+
fp.register(
|
|
1583
|
+
["cmake", fp.regex(r"-D.+=.+"), fp.any()],
|
|
1584
|
+
stdout="configured\n",
|
|
1585
|
+
)
|
|
1586
|
+
|
|
1587
|
+
proc = subprocess.run(
|
|
1588
|
+
["cmake", "-DCMAKE_BUILD_TYPE=Release", "-S/tmp/src", "-B/tmp/build"],
|
|
1589
|
+
capture_output=True,
|
|
1590
|
+
)
|
|
1591
|
+
assert proc.stdout == b"configured\n"
|
|
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
|
{pytest_subprocess-1.5.4 → pytest_subprocess-1.6.0}/pytest_subprocess.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{pytest_subprocess-1.5.4 → pytest_subprocess-1.6.0}/pytest_subprocess.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|
{pytest_subprocess-1.5.4 → pytest_subprocess-1.6.0}/pytest_subprocess.egg-info/top_level.txt
RENAMED
|
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
|