pytest-subprocess 1.5.2__tar.gz → 1.5.3__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.2 → pytest_subprocess-1.5.3}/HISTORY.rst +13 -0
- {pytest_subprocess-1.5.2/pytest_subprocess.egg-info → pytest_subprocess-1.5.3}/PKG-INFO +50 -5
- {pytest_subprocess-1.5.2 → pytest_subprocess-1.5.3}/README.rst +34 -3
- {pytest_subprocess-1.5.2 → pytest_subprocess-1.5.3}/pytest_subprocess/__init__.py +1 -0
- {pytest_subprocess-1.5.2 → pytest_subprocess-1.5.3}/pytest_subprocess/fake_popen.py +75 -19
- {pytest_subprocess-1.5.2 → pytest_subprocess-1.5.3}/pytest_subprocess/fake_process.py +1 -0
- {pytest_subprocess-1.5.2 → pytest_subprocess-1.5.3}/pytest_subprocess/process_dispatcher.py +15 -2
- {pytest_subprocess-1.5.2 → pytest_subprocess-1.5.3/pytest_subprocess.egg-info}/PKG-INFO +50 -5
- {pytest_subprocess-1.5.2 → pytest_subprocess-1.5.3}/pytest_subprocess.egg-info/requires.txt +0 -1
- {pytest_subprocess-1.5.2 → pytest_subprocess-1.5.3}/setup.py +3 -2
- {pytest_subprocess-1.5.2 → pytest_subprocess-1.5.3}/tests/example_script.py +1 -0
- {pytest_subprocess-1.5.2 → pytest_subprocess-1.5.3}/tests/test_asyncio.py +43 -8
- {pytest_subprocess-1.5.2 → pytest_subprocess-1.5.3}/tests/test_subprocess.py +53 -5
- {pytest_subprocess-1.5.2 → pytest_subprocess-1.5.3}/tests/test_typing.py +1 -0
- {pytest_subprocess-1.5.2 → pytest_subprocess-1.5.3}/LICENSE +0 -0
- {pytest_subprocess-1.5.2 → pytest_subprocess-1.5.3}/MANIFEST.in +0 -0
- {pytest_subprocess-1.5.2 → pytest_subprocess-1.5.3}/docs/Makefile +0 -0
- {pytest_subprocess-1.5.2 → pytest_subprocess-1.5.3}/docs/api.rst +0 -0
- {pytest_subprocess-1.5.2 → pytest_subprocess-1.5.3}/docs/conf.py +0 -0
- {pytest_subprocess-1.5.2 → pytest_subprocess-1.5.3}/docs/index.rst +0 -0
- {pytest_subprocess-1.5.2 → pytest_subprocess-1.5.3}/docs/make.bat +0 -0
- {pytest_subprocess-1.5.2 → pytest_subprocess-1.5.3}/docs/usage.rst +0 -0
- {pytest_subprocess-1.5.2 → pytest_subprocess-1.5.3}/pytest.ini +0 -0
- {pytest_subprocess-1.5.2 → pytest_subprocess-1.5.3}/pytest_subprocess/asyncio_subprocess.py +0 -0
- {pytest_subprocess-1.5.2 → pytest_subprocess-1.5.3}/pytest_subprocess/exceptions.py +0 -0
- {pytest_subprocess-1.5.2 → pytest_subprocess-1.5.3}/pytest_subprocess/fixtures.py +0 -0
- {pytest_subprocess-1.5.2 → pytest_subprocess-1.5.3}/pytest_subprocess/process_recorder.py +0 -0
- {pytest_subprocess-1.5.2 → pytest_subprocess-1.5.3}/pytest_subprocess/py.typed +0 -0
- {pytest_subprocess-1.5.2 → pytest_subprocess-1.5.3}/pytest_subprocess/types.py +0 -0
- {pytest_subprocess-1.5.2 → pytest_subprocess-1.5.3}/pytest_subprocess/utils.py +0 -0
- {pytest_subprocess-1.5.2 → pytest_subprocess-1.5.3}/pytest_subprocess.egg-info/SOURCES.txt +0 -0
- {pytest_subprocess-1.5.2 → pytest_subprocess-1.5.3}/pytest_subprocess.egg-info/dependency_links.txt +0 -0
- {pytest_subprocess-1.5.2 → pytest_subprocess-1.5.3}/pytest_subprocess.egg-info/entry_points.txt +0 -0
- {pytest_subprocess-1.5.2 → pytest_subprocess-1.5.3}/pytest_subprocess.egg-info/top_level.txt +0 -0
- {pytest_subprocess-1.5.2 → pytest_subprocess-1.5.3}/setup.cfg +0 -0
- {pytest_subprocess-1.5.2 → pytest_subprocess-1.5.3}/tests/__init__.py +0 -0
- {pytest_subprocess-1.5.2 → pytest_subprocess-1.5.3}/tests/conftest.py +0 -0
- {pytest_subprocess-1.5.2 → pytest_subprocess-1.5.3}/tests/test_examples.py +0 -0
- {pytest_subprocess-1.5.2 → pytest_subprocess-1.5.3}/tests/test_utils.py +0 -0
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
History
|
|
2
2
|
=======
|
|
3
3
|
|
|
4
|
+
1.5.3 (2025-01-04)
|
|
5
|
+
------------------
|
|
6
|
+
|
|
7
|
+
Features
|
|
8
|
+
~~~~~~~~
|
|
9
|
+
* `#171 <https://github.com/aklajnert/pytest-subprocess/pull/171>`_, `#178 <https://github.com/aklajnert/pytest-subprocess/pull/178>`_: Allow to access keyword arguments passed to Popen.
|
|
10
|
+
|
|
11
|
+
Bug fixes
|
|
12
|
+
~~~~~~~~~
|
|
13
|
+
* `#180 <https://github.com/aklajnert/pytest-subprocess/pull/180>`_: Fixed an incorrect wait timeout calculation.
|
|
14
|
+
* `#170 <https://github.com/aklajnert/pytest-subprocess/pull/170>`_: Wrapped ProcessDispatcher.dispatch into FakePopenWrapper as it was causing TypeError when Popen is used as a type.
|
|
15
|
+
* `#169 <https://github.com/aklajnert/pytest-subprocess/pull/169>`_: Get rid of using thread in AsyncFakePopen as it causes thread.join() to hang indefinitely.
|
|
16
|
+
|
|
4
17
|
1.5.2 (2024-07-24)
|
|
5
18
|
------------------
|
|
6
19
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: pytest-subprocess
|
|
3
|
-
Version: 1.5.
|
|
3
|
+
Version: 1.5.3
|
|
4
4
|
Summary: A plugin to fake subprocess for pytest
|
|
5
5
|
Author: Andrzej Klajnert
|
|
6
6
|
Author-email: python@aklajnert.pl
|
|
@@ -22,6 +22,8 @@ Classifier: Programming Language :: Python :: 3.8
|
|
|
22
22
|
Classifier: Programming Language :: Python :: 3.9
|
|
23
23
|
Classifier: Programming Language :: Python :: 3.10
|
|
24
24
|
Classifier: Programming Language :: Python :: 3.11
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
26
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
25
27
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
26
28
|
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
27
29
|
Classifier: Operating System :: OS Independent
|
|
@@ -31,7 +33,6 @@ License-File: LICENSE
|
|
|
31
33
|
Requires-Dist: pytest>=4.0.0
|
|
32
34
|
Provides-Extra: test
|
|
33
35
|
Requires-Dist: pytest>=4.0; extra == "test"
|
|
34
|
-
Requires-Dist: coverage; extra == "test"
|
|
35
36
|
Requires-Dist: docutils>=0.12; extra == "test"
|
|
36
37
|
Requires-Dist: Pygments>=2.0; extra == "test"
|
|
37
38
|
Requires-Dist: pytest-rerunfailures; extra == "test"
|
|
@@ -60,9 +61,6 @@ pytest-subprocess
|
|
|
60
61
|
:target: https://pypi.org/project/pytest-subprocess
|
|
61
62
|
:alt: Python versions
|
|
62
63
|
|
|
63
|
-
.. image:: https://codecov.io/gh/aklajnert/pytest-subprocess/branch/master/graph/badge.svg?token=JAU1cGoYL8
|
|
64
|
-
:target: https://codecov.io/gh/aklajnert/pytest-subprocess
|
|
65
|
-
|
|
66
64
|
.. image:: https://readthedocs.org/projects/pytest-subprocess/badge/?version=latest
|
|
67
65
|
:target: https://pytest-subprocess.readthedocs.io/en/latest/?badge=latest
|
|
68
66
|
:alt: Documentation Status
|
|
@@ -402,6 +400,40 @@ how many a command has been called. The latter supports ``fp.any()``.
|
|
|
402
400
|
assert fp.call_count(["cp", fp.any()]) == 3
|
|
403
401
|
|
|
404
402
|
|
|
403
|
+
Check Popen arguments
|
|
404
|
+
---------------------
|
|
405
|
+
|
|
406
|
+
You can use the recorded calls functionality to introspect the keyword
|
|
407
|
+
arguments that were passed to `Popen`.
|
|
408
|
+
|
|
409
|
+
.. code-block:: python
|
|
410
|
+
|
|
411
|
+
def test_process_recorder_kwargs(fp):
|
|
412
|
+
fp.keep_last_process(True)
|
|
413
|
+
recorder = fp.register(["test_script", fp.any()])
|
|
414
|
+
|
|
415
|
+
subprocess.run(
|
|
416
|
+
("test_script", "arg1"), env={"foo": "bar"}, cwd="/home/user"
|
|
417
|
+
)
|
|
418
|
+
subprocess.Popen(
|
|
419
|
+
["test_script", "arg2"],
|
|
420
|
+
env={"foo": "bar1"},
|
|
421
|
+
executable="test_script",
|
|
422
|
+
shell=True,
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
assert recorder.calls[0].args == ("test_script", "arg1")
|
|
426
|
+
assert recorder.calls[0].kwargs == {
|
|
427
|
+
"cwd": "/home/user",
|
|
428
|
+
"env": {"foo": "bar"},
|
|
429
|
+
}
|
|
430
|
+
assert recorder.calls[1].args == ["test_script", "arg2"]
|
|
431
|
+
assert recorder.calls[1].kwargs == {
|
|
432
|
+
"env": {"foo": "bar1"},
|
|
433
|
+
"executable": "test_script",
|
|
434
|
+
"shell": True,
|
|
435
|
+
}
|
|
436
|
+
|
|
405
437
|
Handling signals
|
|
406
438
|
----------------
|
|
407
439
|
|
|
@@ -507,6 +539,19 @@ This `pytest`_ plugin was generated with `Cookiecutter`_ along with `@hackebrot`
|
|
|
507
539
|
History
|
|
508
540
|
=======
|
|
509
541
|
|
|
542
|
+
1.5.3 (2025-01-04)
|
|
543
|
+
------------------
|
|
544
|
+
|
|
545
|
+
Features
|
|
546
|
+
~~~~~~~~
|
|
547
|
+
* `#171 <https://github.com/aklajnert/pytest-subprocess/pull/171>`_, `#178 <https://github.com/aklajnert/pytest-subprocess/pull/178>`_: Allow to access keyword arguments passed to Popen.
|
|
548
|
+
|
|
549
|
+
Bug fixes
|
|
550
|
+
~~~~~~~~~
|
|
551
|
+
* `#180 <https://github.com/aklajnert/pytest-subprocess/pull/180>`_: Fixed an incorrect wait timeout calculation.
|
|
552
|
+
* `#170 <https://github.com/aklajnert/pytest-subprocess/pull/170>`_: Wrapped ProcessDispatcher.dispatch into FakePopenWrapper as it was causing TypeError when Popen is used as a type.
|
|
553
|
+
* `#169 <https://github.com/aklajnert/pytest-subprocess/pull/169>`_: Get rid of using thread in AsyncFakePopen as it causes thread.join() to hang indefinitely.
|
|
554
|
+
|
|
510
555
|
1.5.2 (2024-07-24)
|
|
511
556
|
------------------
|
|
512
557
|
|
|
@@ -10,9 +10,6 @@ pytest-subprocess
|
|
|
10
10
|
:target: https://pypi.org/project/pytest-subprocess
|
|
11
11
|
:alt: Python versions
|
|
12
12
|
|
|
13
|
-
.. image:: https://codecov.io/gh/aklajnert/pytest-subprocess/branch/master/graph/badge.svg?token=JAU1cGoYL8
|
|
14
|
-
:target: https://codecov.io/gh/aklajnert/pytest-subprocess
|
|
15
|
-
|
|
16
13
|
.. image:: https://readthedocs.org/projects/pytest-subprocess/badge/?version=latest
|
|
17
14
|
:target: https://pytest-subprocess.readthedocs.io/en/latest/?badge=latest
|
|
18
15
|
:alt: Documentation Status
|
|
@@ -352,6 +349,40 @@ how many a command has been called. The latter supports ``fp.any()``.
|
|
|
352
349
|
assert fp.call_count(["cp", fp.any()]) == 3
|
|
353
350
|
|
|
354
351
|
|
|
352
|
+
Check Popen arguments
|
|
353
|
+
---------------------
|
|
354
|
+
|
|
355
|
+
You can use the recorded calls functionality to introspect the keyword
|
|
356
|
+
arguments that were passed to `Popen`.
|
|
357
|
+
|
|
358
|
+
.. code-block:: python
|
|
359
|
+
|
|
360
|
+
def test_process_recorder_kwargs(fp):
|
|
361
|
+
fp.keep_last_process(True)
|
|
362
|
+
recorder = fp.register(["test_script", fp.any()])
|
|
363
|
+
|
|
364
|
+
subprocess.run(
|
|
365
|
+
("test_script", "arg1"), env={"foo": "bar"}, cwd="/home/user"
|
|
366
|
+
)
|
|
367
|
+
subprocess.Popen(
|
|
368
|
+
["test_script", "arg2"],
|
|
369
|
+
env={"foo": "bar1"},
|
|
370
|
+
executable="test_script",
|
|
371
|
+
shell=True,
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
assert recorder.calls[0].args == ("test_script", "arg1")
|
|
375
|
+
assert recorder.calls[0].kwargs == {
|
|
376
|
+
"cwd": "/home/user",
|
|
377
|
+
"env": {"foo": "bar"},
|
|
378
|
+
}
|
|
379
|
+
assert recorder.calls[1].args == ["test_script", "arg2"]
|
|
380
|
+
assert recorder.calls[1].kwargs == {
|
|
381
|
+
"env": {"foo": "bar1"},
|
|
382
|
+
"executable": "test_script",
|
|
383
|
+
"shell": True,
|
|
384
|
+
}
|
|
385
|
+
|
|
355
386
|
Handling signals
|
|
356
387
|
----------------
|
|
357
388
|
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
"""FakePopen class declaration"""
|
|
2
|
+
|
|
2
3
|
import asyncio
|
|
3
4
|
import collections.abc
|
|
5
|
+
import concurrent.futures
|
|
6
|
+
import copy
|
|
4
7
|
import io
|
|
5
8
|
import os
|
|
6
9
|
import signal
|
|
@@ -66,16 +69,22 @@ class FakePopen:
|
|
|
66
69
|
msg = f"argument of type {arg.__class__.__name__!r} is not iterable"
|
|
67
70
|
raise TypeError(msg)
|
|
68
71
|
self.args = command
|
|
72
|
+
self.__kwargs: Optional[Dict[str, AnyType]] = None
|
|
69
73
|
self.__stdout: OPTIONAL_TEXT_OR_ITERABLE = stdout
|
|
70
74
|
self.__stderr: OPTIONAL_TEXT_OR_ITERABLE = stderr
|
|
71
|
-
self.__returncode: Optional[int] = returncode
|
|
72
|
-
self.__wait: Optional[float] = wait
|
|
73
75
|
self.__thread: Optional[Thread] = None
|
|
74
|
-
self.__callback: Optional[Optional[Callable]] = callback
|
|
75
|
-
self.__callback_kwargs: Optional[Dict[str, AnyType]] = callback_kwargs
|
|
76
76
|
self.__signal_callback: Optional[Callable] = signal_callback
|
|
77
77
|
self.__stdin_callable: Optional[Optional[Callable]] = stdin_callable
|
|
78
|
+
self.__universal_newlines: Optional[Dict[AnyType, AnyType]] = None
|
|
78
79
|
self._signals: List[int] = []
|
|
80
|
+
self._returncode: Optional[int] = returncode
|
|
81
|
+
self._wait_timeout: Optional[float] = wait
|
|
82
|
+
self._callback: Optional[Optional[Callable]] = callback
|
|
83
|
+
self._callback_kwargs: Optional[Dict[str, AnyType]] = callback_kwargs
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def kwargs(self) -> Optional[Dict[str, AnyType]]:
|
|
87
|
+
return self.__kwargs
|
|
79
88
|
|
|
80
89
|
def __enter__(self) -> "FakePopen":
|
|
81
90
|
return self
|
|
@@ -115,8 +124,8 @@ class FakePopen:
|
|
|
115
124
|
if self.__thread is None:
|
|
116
125
|
return
|
|
117
126
|
self.__thread.join(timeout)
|
|
118
|
-
if self.returncode is None and self.
|
|
119
|
-
self.returncode = self.
|
|
127
|
+
if self.returncode is None and self._returncode is not None:
|
|
128
|
+
self.returncode = self._returncode
|
|
120
129
|
if self.__thread.exception:
|
|
121
130
|
raise self.__thread.exception
|
|
122
131
|
|
|
@@ -132,9 +141,10 @@ class FakePopen:
|
|
|
132
141
|
return self.returncode
|
|
133
142
|
|
|
134
143
|
def wait(self, timeout: Optional[float] = None) -> int:
|
|
135
|
-
if timeout and self.
|
|
136
|
-
self.
|
|
137
|
-
|
|
144
|
+
if timeout and self._wait_timeout:
|
|
145
|
+
self._wait_timeout -= timeout
|
|
146
|
+
if timeout < self._wait_timeout:
|
|
147
|
+
raise subprocess.TimeoutExpired(self.args, timeout)
|
|
138
148
|
self._finalize_thread(timeout)
|
|
139
149
|
if self.returncode is None:
|
|
140
150
|
raise exceptions.PluginInternalError
|
|
@@ -156,6 +166,7 @@ class FakePopen:
|
|
|
156
166
|
|
|
157
167
|
def configure(self, **kwargs: Optional[Dict]) -> None:
|
|
158
168
|
"""Setup the FakePopen instance based on a real Popen arguments."""
|
|
169
|
+
self.__kwargs = self.safe_copy(kwargs)
|
|
159
170
|
self.__universal_newlines = kwargs.get("universal_newlines", None)
|
|
160
171
|
text = kwargs.get("text", None)
|
|
161
172
|
encoding = kwargs.get("encoding", None)
|
|
@@ -192,6 +203,16 @@ class FakePopen:
|
|
|
192
203
|
elif isinstance(stderr, (io.BufferedWriter, io.TextIOWrapper)):
|
|
193
204
|
self._write_to_buffer(self.__stderr, stderr)
|
|
194
205
|
|
|
206
|
+
@staticmethod
|
|
207
|
+
def safe_copy(kwargs: Dict[str, AnyType]) -> Dict[str, AnyType]:
|
|
208
|
+
"""
|
|
209
|
+
Deepcopy can fail if the value is not serializable, fallback to shallow copy.
|
|
210
|
+
"""
|
|
211
|
+
try:
|
|
212
|
+
return copy.deepcopy(kwargs)
|
|
213
|
+
except TypeError:
|
|
214
|
+
return dict(**kwargs)
|
|
215
|
+
|
|
195
216
|
def _prepare_buffer(
|
|
196
217
|
self,
|
|
197
218
|
input: OPTIONAL_TEXT_OR_ITERABLE,
|
|
@@ -284,21 +305,21 @@ class FakePopen:
|
|
|
284
305
|
|
|
285
306
|
def run_thread(self) -> None:
|
|
286
307
|
"""Run the user-defined callback or wait in a thread."""
|
|
287
|
-
if self.
|
|
308
|
+
if self._wait_timeout is None and self._callback is None:
|
|
288
309
|
self._finish_process()
|
|
289
310
|
else:
|
|
290
|
-
if self.
|
|
311
|
+
if self._callback:
|
|
291
312
|
self.__thread = Thread(
|
|
292
|
-
target=self.
|
|
313
|
+
target=self._callback,
|
|
293
314
|
args=(self,),
|
|
294
|
-
kwargs=self.
|
|
315
|
+
kwargs=self._callback_kwargs or {},
|
|
295
316
|
)
|
|
296
317
|
else:
|
|
297
|
-
self.__thread = Thread(target=self._wait, args=(self.
|
|
318
|
+
self.__thread = Thread(target=self._wait, args=(self._wait_timeout,))
|
|
298
319
|
self.__thread.start()
|
|
299
320
|
|
|
300
321
|
def _finish_process(self) -> None:
|
|
301
|
-
self.returncode = self.
|
|
322
|
+
self.returncode = self._returncode
|
|
302
323
|
|
|
303
324
|
self._finalize_streams()
|
|
304
325
|
|
|
@@ -334,16 +355,20 @@ class AsyncFakePopen(FakePopen):
|
|
|
334
355
|
|
|
335
356
|
# feed eof one more time as streams were opened
|
|
336
357
|
self._finalize_streams()
|
|
337
|
-
|
|
338
|
-
self._finalize_thread(timeout)
|
|
339
|
-
|
|
358
|
+
await self._finalize(timeout)
|
|
340
359
|
return (
|
|
341
360
|
await self.stdout.read() if self.stdout else None,
|
|
342
361
|
await self.stderr.read() if self.stderr else None,
|
|
343
362
|
)
|
|
344
363
|
|
|
345
364
|
async def wait(self, timeout: Optional[float] = None) -> int: # type: ignore
|
|
346
|
-
|
|
365
|
+
if timeout and self._wait_timeout and timeout < self._wait_timeout:
|
|
366
|
+
self._wait_timeout -= timeout
|
|
367
|
+
raise subprocess.TimeoutExpired(self.args, timeout)
|
|
368
|
+
await self._finalize(timeout)
|
|
369
|
+
if self.returncode is None:
|
|
370
|
+
raise exceptions.PluginInternalError
|
|
371
|
+
return self.returncode
|
|
347
372
|
|
|
348
373
|
def _get_empty_buffer(self, _: bool) -> asyncio.StreamReader:
|
|
349
374
|
return asyncio.StreamReader()
|
|
@@ -361,3 +386,34 @@ class AsyncFakePopen(FakePopen):
|
|
|
361
386
|
fresh_stream.feed_data(data)
|
|
362
387
|
return fresh_stream
|
|
363
388
|
return None
|
|
389
|
+
|
|
390
|
+
def run_thread(self) -> None:
|
|
391
|
+
"""Async impl should not contain any thread based implementation"""
|
|
392
|
+
|
|
393
|
+
def evaluate(self) -> None:
|
|
394
|
+
"""Check if process needs to be finished."""
|
|
395
|
+
if self._wait_timeout is None and self._callback is None:
|
|
396
|
+
self._finish_process()
|
|
397
|
+
|
|
398
|
+
async def _run_callback_in_executor(self) -> None:
|
|
399
|
+
"""Run in executor the user-defined callback or wait."""
|
|
400
|
+
loop = asyncio.get_running_loop()
|
|
401
|
+
with concurrent.futures.ThreadPoolExecutor() as pool:
|
|
402
|
+
if self._callback:
|
|
403
|
+
kwargs = self._callback_kwargs or {}
|
|
404
|
+
cbk = partial(self._callback, **kwargs)
|
|
405
|
+
await loop.run_in_executor(pool, cbk, self)
|
|
406
|
+
elif self._wait_timeout is not None:
|
|
407
|
+
await loop.run_in_executor(pool, self._wait, self._wait_timeout)
|
|
408
|
+
|
|
409
|
+
async def _finalize(self, timeout: Optional[float] = None) -> None:
|
|
410
|
+
"""Run the user-defined callback or wait. Finish process"""
|
|
411
|
+
if self.returncode is not None:
|
|
412
|
+
return
|
|
413
|
+
if timeout is not None:
|
|
414
|
+
await asyncio.wait_for(self._run_callback_in_executor(), timeout=timeout)
|
|
415
|
+
else:
|
|
416
|
+
await self._run_callback_in_executor()
|
|
417
|
+
if self.returncode is None:
|
|
418
|
+
self.returncode = self._returncode
|
|
419
|
+
self._finalize_streams()
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"""ProcessDispatcher class declaration"""
|
|
2
|
+
|
|
2
3
|
import asyncio
|
|
3
4
|
import subprocess
|
|
4
5
|
import sys
|
|
@@ -7,10 +8,12 @@ from collections import deque
|
|
|
7
8
|
from copy import deepcopy
|
|
8
9
|
from functools import partial
|
|
9
10
|
from typing import Any as AnyType
|
|
11
|
+
from typing import AnyStr
|
|
10
12
|
from typing import Awaitable
|
|
11
13
|
from typing import Callable
|
|
12
14
|
from typing import Deque
|
|
13
15
|
from typing import Dict
|
|
16
|
+
from typing import Generic
|
|
14
17
|
from typing import List
|
|
15
18
|
from typing import Optional
|
|
16
19
|
from typing import Tuple
|
|
@@ -28,6 +31,9 @@ if typing.TYPE_CHECKING:
|
|
|
28
31
|
from .fake_process import FakeProcess
|
|
29
32
|
|
|
30
33
|
|
|
34
|
+
__all__ = ["ProcessDispatcher"]
|
|
35
|
+
|
|
36
|
+
|
|
31
37
|
class ProcessDispatcher:
|
|
32
38
|
"""Main class for handling processes."""
|
|
33
39
|
|
|
@@ -43,7 +49,7 @@ class ProcessDispatcher:
|
|
|
43
49
|
def register(cls, process: "FakeProcess") -> None:
|
|
44
50
|
if not cls.process_list:
|
|
45
51
|
cls.built_in_popen = subprocess.Popen
|
|
46
|
-
subprocess.Popen =
|
|
52
|
+
subprocess.Popen = FakePopenWrapper # type: ignore
|
|
47
53
|
|
|
48
54
|
cls.built_in_async_subprocess = asyncio.subprocess
|
|
49
55
|
asyncio.create_subprocess_shell = cls.async_shell # type: ignore
|
|
@@ -161,7 +167,7 @@ class ProcessDispatcher:
|
|
|
161
167
|
result = cls._prepare_instance(AsyncFakePopen, command, kwargs, process)
|
|
162
168
|
if not isinstance(result, AsyncFakePopen):
|
|
163
169
|
raise exceptions.PluginInternalError
|
|
164
|
-
result.
|
|
170
|
+
result.evaluate()
|
|
165
171
|
return result
|
|
166
172
|
|
|
167
173
|
@classmethod
|
|
@@ -237,3 +243,10 @@ class ProcessDispatcher:
|
|
|
237
243
|
if processes and isinstance(processes, deque):
|
|
238
244
|
return command_instance, processes, process_instance
|
|
239
245
|
return None, None, None
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
class FakePopenWrapper(Generic[AnyStr]):
|
|
249
|
+
def __new__( # type: ignore
|
|
250
|
+
cls, command: COMMAND, **kwargs: Optional[Dict]
|
|
251
|
+
) -> FakePopen:
|
|
252
|
+
return ProcessDispatcher.dispatch(command, **kwargs) # type: ignore
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: pytest-subprocess
|
|
3
|
-
Version: 1.5.
|
|
3
|
+
Version: 1.5.3
|
|
4
4
|
Summary: A plugin to fake subprocess for pytest
|
|
5
5
|
Author: Andrzej Klajnert
|
|
6
6
|
Author-email: python@aklajnert.pl
|
|
@@ -22,6 +22,8 @@ Classifier: Programming Language :: Python :: 3.8
|
|
|
22
22
|
Classifier: Programming Language :: Python :: 3.9
|
|
23
23
|
Classifier: Programming Language :: Python :: 3.10
|
|
24
24
|
Classifier: Programming Language :: Python :: 3.11
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
26
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
25
27
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
26
28
|
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
27
29
|
Classifier: Operating System :: OS Independent
|
|
@@ -31,7 +33,6 @@ License-File: LICENSE
|
|
|
31
33
|
Requires-Dist: pytest>=4.0.0
|
|
32
34
|
Provides-Extra: test
|
|
33
35
|
Requires-Dist: pytest>=4.0; extra == "test"
|
|
34
|
-
Requires-Dist: coverage; extra == "test"
|
|
35
36
|
Requires-Dist: docutils>=0.12; extra == "test"
|
|
36
37
|
Requires-Dist: Pygments>=2.0; extra == "test"
|
|
37
38
|
Requires-Dist: pytest-rerunfailures; extra == "test"
|
|
@@ -60,9 +61,6 @@ pytest-subprocess
|
|
|
60
61
|
:target: https://pypi.org/project/pytest-subprocess
|
|
61
62
|
:alt: Python versions
|
|
62
63
|
|
|
63
|
-
.. image:: https://codecov.io/gh/aklajnert/pytest-subprocess/branch/master/graph/badge.svg?token=JAU1cGoYL8
|
|
64
|
-
:target: https://codecov.io/gh/aklajnert/pytest-subprocess
|
|
65
|
-
|
|
66
64
|
.. image:: https://readthedocs.org/projects/pytest-subprocess/badge/?version=latest
|
|
67
65
|
:target: https://pytest-subprocess.readthedocs.io/en/latest/?badge=latest
|
|
68
66
|
:alt: Documentation Status
|
|
@@ -402,6 +400,40 @@ how many a command has been called. The latter supports ``fp.any()``.
|
|
|
402
400
|
assert fp.call_count(["cp", fp.any()]) == 3
|
|
403
401
|
|
|
404
402
|
|
|
403
|
+
Check Popen arguments
|
|
404
|
+
---------------------
|
|
405
|
+
|
|
406
|
+
You can use the recorded calls functionality to introspect the keyword
|
|
407
|
+
arguments that were passed to `Popen`.
|
|
408
|
+
|
|
409
|
+
.. code-block:: python
|
|
410
|
+
|
|
411
|
+
def test_process_recorder_kwargs(fp):
|
|
412
|
+
fp.keep_last_process(True)
|
|
413
|
+
recorder = fp.register(["test_script", fp.any()])
|
|
414
|
+
|
|
415
|
+
subprocess.run(
|
|
416
|
+
("test_script", "arg1"), env={"foo": "bar"}, cwd="/home/user"
|
|
417
|
+
)
|
|
418
|
+
subprocess.Popen(
|
|
419
|
+
["test_script", "arg2"],
|
|
420
|
+
env={"foo": "bar1"},
|
|
421
|
+
executable="test_script",
|
|
422
|
+
shell=True,
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
assert recorder.calls[0].args == ("test_script", "arg1")
|
|
426
|
+
assert recorder.calls[0].kwargs == {
|
|
427
|
+
"cwd": "/home/user",
|
|
428
|
+
"env": {"foo": "bar"},
|
|
429
|
+
}
|
|
430
|
+
assert recorder.calls[1].args == ["test_script", "arg2"]
|
|
431
|
+
assert recorder.calls[1].kwargs == {
|
|
432
|
+
"env": {"foo": "bar1"},
|
|
433
|
+
"executable": "test_script",
|
|
434
|
+
"shell": True,
|
|
435
|
+
}
|
|
436
|
+
|
|
405
437
|
Handling signals
|
|
406
438
|
----------------
|
|
407
439
|
|
|
@@ -507,6 +539,19 @@ This `pytest`_ plugin was generated with `Cookiecutter`_ along with `@hackebrot`
|
|
|
507
539
|
History
|
|
508
540
|
=======
|
|
509
541
|
|
|
542
|
+
1.5.3 (2025-01-04)
|
|
543
|
+
------------------
|
|
544
|
+
|
|
545
|
+
Features
|
|
546
|
+
~~~~~~~~
|
|
547
|
+
* `#171 <https://github.com/aklajnert/pytest-subprocess/pull/171>`_, `#178 <https://github.com/aklajnert/pytest-subprocess/pull/178>`_: Allow to access keyword arguments passed to Popen.
|
|
548
|
+
|
|
549
|
+
Bug fixes
|
|
550
|
+
~~~~~~~~~
|
|
551
|
+
* `#180 <https://github.com/aklajnert/pytest-subprocess/pull/180>`_: Fixed an incorrect wait timeout calculation.
|
|
552
|
+
* `#170 <https://github.com/aklajnert/pytest-subprocess/pull/170>`_: Wrapped ProcessDispatcher.dispatch into FakePopenWrapper as it was causing TypeError when Popen is used as a type.
|
|
553
|
+
* `#169 <https://github.com/aklajnert/pytest-subprocess/pull/169>`_: Get rid of using thread in AsyncFakePopen as it causes thread.join() to hang indefinitely.
|
|
554
|
+
|
|
510
555
|
1.5.2 (2024-07-24)
|
|
511
556
|
------------------
|
|
512
557
|
|
|
@@ -16,7 +16,7 @@ requirements = ["pytest>=4.0.0"]
|
|
|
16
16
|
|
|
17
17
|
setup(
|
|
18
18
|
name="pytest-subprocess",
|
|
19
|
-
version="1.5.
|
|
19
|
+
version="1.5.3",
|
|
20
20
|
author="Andrzej Klajnert",
|
|
21
21
|
author_email="python@aklajnert.pl",
|
|
22
22
|
maintainer="Andrzej Klajnert",
|
|
@@ -34,7 +34,6 @@ setup(
|
|
|
34
34
|
extras_require={
|
|
35
35
|
"test": [
|
|
36
36
|
"pytest>=4.0",
|
|
37
|
-
"coverage",
|
|
38
37
|
"docutils>=0.12",
|
|
39
38
|
"Pygments>=2.0",
|
|
40
39
|
"pytest-rerunfailures",
|
|
@@ -66,6 +65,8 @@ setup(
|
|
|
66
65
|
"Programming Language :: Python :: 3.9",
|
|
67
66
|
"Programming Language :: Python :: 3.10",
|
|
68
67
|
"Programming Language :: Python :: 3.11",
|
|
68
|
+
"Programming Language :: Python :: 3.12",
|
|
69
|
+
"Programming Language :: Python :: 3.13",
|
|
69
70
|
"Programming Language :: Python :: Implementation :: CPython",
|
|
70
71
|
"Programming Language :: Python :: Implementation :: PyPy",
|
|
71
72
|
"Operating System :: OS Independent",
|
|
@@ -338,10 +338,7 @@ async def test_popen_recorder(fp):
|
|
|
338
338
|
pytest.param(None, id="no-callback"),
|
|
339
339
|
pytest.param(
|
|
340
340
|
lambda process: process,
|
|
341
|
-
id="
|
|
342
|
-
marks=pytest.mark.xfail(
|
|
343
|
-
strict=True, raises=asyncio.TimeoutError, reason="Github #120"
|
|
344
|
-
),
|
|
341
|
+
id="with-callback",
|
|
345
342
|
),
|
|
346
343
|
],
|
|
347
344
|
)
|
|
@@ -353,15 +350,53 @@ async def test_asyncio_subprocess_using_callback(callback, fp):
|
|
|
353
350
|
stderr=asyncio.subprocess.PIPE,
|
|
354
351
|
)
|
|
355
352
|
await process.wait()
|
|
356
|
-
|
|
357
|
-
# This reads forever when passing a callback to fp.register
|
|
358
|
-
# Add a timeout to abort test when condition occurs.
|
|
359
|
-
return await asyncio.wait_for(process.stdout.read(), timeout=1)
|
|
353
|
+
return await process.stdout.read()
|
|
360
354
|
|
|
361
355
|
fp.register(["test"], stdout=b"fizz", callback=callback)
|
|
362
356
|
assert await my_async_func() == b"fizz"
|
|
363
357
|
|
|
364
358
|
|
|
359
|
+
@pytest.mark.asyncio
|
|
360
|
+
async def test_asyncio_subprocess_using_communicate_with_callback_kwargs(fp):
|
|
361
|
+
expected_some_value = 2
|
|
362
|
+
|
|
363
|
+
def cbk(fake_obj, some_value=None):
|
|
364
|
+
assert expected_some_value == some_value
|
|
365
|
+
return fake_obj
|
|
366
|
+
|
|
367
|
+
async def my_async_func():
|
|
368
|
+
process = await asyncio.create_subprocess_exec(
|
|
369
|
+
"test",
|
|
370
|
+
stdout=asyncio.subprocess.PIPE,
|
|
371
|
+
stderr=asyncio.subprocess.PIPE,
|
|
372
|
+
)
|
|
373
|
+
out, _ = await process.communicate()
|
|
374
|
+
return out
|
|
375
|
+
|
|
376
|
+
fp.register(
|
|
377
|
+
["test"],
|
|
378
|
+
stdout=b"fizz",
|
|
379
|
+
callback=cbk,
|
|
380
|
+
callback_kwargs={"some_value": expected_some_value},
|
|
381
|
+
)
|
|
382
|
+
assert await my_async_func() == b"fizz"
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
@pytest.mark.asyncio
|
|
386
|
+
async def test_process_recorder_args(fp):
|
|
387
|
+
fp.keep_last_process(True)
|
|
388
|
+
recorder = fp.register(["test_script", fp.any()])
|
|
389
|
+
await asyncio.create_subprocess_exec(
|
|
390
|
+
"test_script",
|
|
391
|
+
"arg1",
|
|
392
|
+
env={"foo": "bar"},
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
assert recorder.call_count() == 1
|
|
396
|
+
assert recorder.calls[0].args == ["test_script", "arg1"]
|
|
397
|
+
assert recorder.calls[0].kwargs == {"env": {"foo": "bar"}}
|
|
398
|
+
|
|
399
|
+
|
|
365
400
|
@pytest.fixture(autouse=True)
|
|
366
401
|
def skip_on_pypy():
|
|
367
402
|
"""Async test for some reason crash on pypy 3.6 on Windows"""
|
|
@@ -482,15 +482,15 @@ def test_ambiguous_input(fp, fake):
|
|
|
482
482
|
@pytest.mark.parametrize("fake", [False, True])
|
|
483
483
|
def test_multiple_wait(fp, fake):
|
|
484
484
|
"""
|
|
485
|
-
Wait multiple times for 0.2 seconds with process lasting for
|
|
486
|
-
Third wait shall
|
|
485
|
+
Wait multiple times for 0.2 seconds with process lasting for 1s.
|
|
486
|
+
Third wait shall be a bit longer and will not raise an exception,
|
|
487
487
|
due to exceeding the subprocess runtime.
|
|
488
488
|
"""
|
|
489
489
|
fp.allow_unregistered(not fake)
|
|
490
490
|
if fake:
|
|
491
491
|
fp.register(
|
|
492
492
|
[PYTHON, "example_script.py", "wait"],
|
|
493
|
-
wait=
|
|
493
|
+
wait=1,
|
|
494
494
|
)
|
|
495
495
|
|
|
496
496
|
process = subprocess.Popen(
|
|
@@ -500,9 +500,9 @@ def test_multiple_wait(fp, fake):
|
|
|
500
500
|
process.wait(timeout=0.2)
|
|
501
501
|
|
|
502
502
|
with pytest.raises(subprocess.TimeoutExpired):
|
|
503
|
-
process.wait(timeout=0.
|
|
503
|
+
process.wait(timeout=0.2)
|
|
504
504
|
|
|
505
|
-
process.wait(0.
|
|
505
|
+
process.wait(0.9)
|
|
506
506
|
|
|
507
507
|
assert process.returncode == 0
|
|
508
508
|
|
|
@@ -1232,3 +1232,51 @@ def test_process_recorder(fp):
|
|
|
1232
1232
|
recorder.clear()
|
|
1233
1233
|
|
|
1234
1234
|
assert not recorder.was_called()
|
|
1235
|
+
|
|
1236
|
+
|
|
1237
|
+
def test_process_recorder_args(fp):
|
|
1238
|
+
fp.keep_last_process(True)
|
|
1239
|
+
recorder = fp.register(["test_script", fp.any()])
|
|
1240
|
+
|
|
1241
|
+
subprocess.call(("test_script", "arg1"))
|
|
1242
|
+
subprocess.run(("test_script", "arg2"), env={"foo": "bar"}, cwd="/home/user")
|
|
1243
|
+
subprocess.Popen(
|
|
1244
|
+
["test_script", "arg3"],
|
|
1245
|
+
env={"foo": "bar1"},
|
|
1246
|
+
executable="test_script",
|
|
1247
|
+
shell=True,
|
|
1248
|
+
)
|
|
1249
|
+
|
|
1250
|
+
assert recorder.call_count() == 3
|
|
1251
|
+
assert recorder.calls[0].args == ("test_script", "arg1")
|
|
1252
|
+
assert recorder.calls[0].kwargs == {}
|
|
1253
|
+
assert recorder.calls[1].args == ("test_script", "arg2")
|
|
1254
|
+
assert recorder.calls[1].kwargs == {"cwd": "/home/user", "env": {"foo": "bar"}}
|
|
1255
|
+
assert recorder.calls[2].args == ["test_script", "arg3"]
|
|
1256
|
+
assert recorder.calls[2].kwargs == {
|
|
1257
|
+
"env": {"foo": "bar1"},
|
|
1258
|
+
"executable": "test_script",
|
|
1259
|
+
"shell": True,
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
|
|
1263
|
+
def test_fake_popen_is_typed(fp):
|
|
1264
|
+
fp.allow_unregistered(True)
|
|
1265
|
+
fp.register(
|
|
1266
|
+
[PYTHON, "example_script.py"],
|
|
1267
|
+
stdout=b"Stdout line 1\r\nStdout line 2\r\n",
|
|
1268
|
+
)
|
|
1269
|
+
|
|
1270
|
+
def spawn_process() -> subprocess.Popen[str]:
|
|
1271
|
+
import subprocess
|
|
1272
|
+
|
|
1273
|
+
return subprocess.Popen(
|
|
1274
|
+
(PYTHON, "example_script.py"),
|
|
1275
|
+
universal_newlines=True,
|
|
1276
|
+
stdout=subprocess.PIPE,
|
|
1277
|
+
)
|
|
1278
|
+
|
|
1279
|
+
proc = spawn_process()
|
|
1280
|
+
proc.wait()
|
|
1281
|
+
|
|
1282
|
+
assert proc.stdout.read() == "Stdout line 1\nStdout line 2\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
|
{pytest_subprocess-1.5.2 → pytest_subprocess-1.5.3}/pytest_subprocess.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{pytest_subprocess-1.5.2 → pytest_subprocess-1.5.3}/pytest_subprocess.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{pytest_subprocess-1.5.2 → pytest_subprocess-1.5.3}/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
|