aklajnert / pytest-subprocess Goto Github PK
View Code? Open in Web Editor NEWPytest plugin to fake subprocess.
License: MIT License
Pytest plugin to fake subprocess.
License: MIT License
Version info:
mypy 0.812
pytest-subprocess 1.1.0
Error:
main.py:34: error: Argument 1 to "register_subprocess" of "FakeProcess" has incompatible type "List[str]"; expected "Union[List[Union[str, Any]], Tuple[Union[str, Any], ...], str, Command]"
main.py:34: note: "List" is invariant -- see http://mypy.readthedocs.io/en/latest/common_issues.html#variance
main.py:34: note: Consider using "Sequence" instead, which is covariant
main.py:34: error: Argument "stdout" to "register_subprocess" of "FakeProcess" has incompatible type "List[str]"; expected "Union[str, bytes, List[Union[str, bytes]], Tuple[Union[str, bytes], ...], None]"
https://mypy-play.net/?mypy=latest&python=3.9&gist=89232479f9811be76ee54460a5f1a895
Fixable if you annotate the arguments as List[Union[str, bytes]]
.
Does register_subprocess
really require the ability to mutate the input list? If it only needs read-only access, then using Sequence
would be helpful to avoid the unnecessary upcast.
Hello,
this is an extension of #11.
Say you have a module, with a function:
file: mod1.py
def f():
try:
x = subprocess.run('asdfasf')
return True
except subprocess.CalledProcessError:
return False
and you want to test the except part; i wrote:
def test_f(fake_process):
def callback_exception(process):
raise subprocess.CalledProcessError(returncode=0, cmd=["bad"])
with fake_process.context() as nested_process:
nested_process.register_subprocess(['yadayada', 'lallalla'], callback=callback_exception)
y = mod1.f()
assert not y
but turns out subprocess.run
gets executed just fine in mod1.f but just with a returncode=None
, which is not what i'd expect
it would be great if, instead of using a callback function, we could have an extra argument to register_subprocess
to actually make it raise an exception, even if you're testing code not in the current module.
Thanks!
Python: 3.8.12
pytest-subprocess: 1.3.1
The function signature for for asyncio.create_subprocess_exec
takes an executable and *args as arguments.
If I register a fake_process to mock a call with just an executable, the test will run fine.
If I register a fake_process that needs to handle arguments, the test fails with a TypeError that says for example:
async_shell() takes 2 positional arguments but 5 were given
I've attached an MWE that demonstrates this. Please change .txt to .py. Github would not let me upload .py files.
main.py should run execute when called with python3
text.py will produce two passes and a failure. The failing test will be test_exec_args
.
I have a number of callers that do..
proc = subprocess.Popen(cmdline)
while proc.poll() is None:
time.sleep(1)
I register with:
def my_callback():
print("called")
time.sleep(1)
fp.register(cmdline, returncode=1, callback=my_callback)
But proc.poll()
seems to always return None
I have some code with uses subprocess
with a file handle as the stdout
argument. The subprocess
module will write the output of the command to the file handle, and my code does not directly call communicate
. When I register a command with fake_process
in the tests with some stdout
content specified, the file doesn't seem to have the expected content written to it. Have I overlooked something, or is that not supported by this library?
Example:
# tests/test_sp.py
import subprocess
def test_sp(fake_process):
fake_process.register_subprocess(["echo", "hello"], stdout="hello\n")
filename = "file.txt"
with open(filename, "w") as handle:
subprocess.run(["echo", "hello"], stdout=handle)
assert open(filename).read() == "hello\n"
Result:
╰─❯ poetry run pytest
============================= test session starts ==============================
platform darwin -- Python 3.9.7, pytest-5.4.3, py-1.10.0, pluggy-0.13.1
rootdir: [...]/fake_sp_test
plugins: subprocess-1.3.0
collected 1 item
tests/test_sp.py F [100%]
=================================== FAILURES ===================================
___________________________________ test_sp ____________________________________
fake_process = <pytest_subprocess.core.FakeProcess object at 0x10ee90190>
def test_sp(fake_process):
fake_process.register_subprocess(["echo", "hello"], stdout="hello\n")
filename = "file.txt"
with open(filename, "w") as handle:
subprocess.run(["echo", "hello"], stdout=handle)
> assert open(filename).read() == "hello\n"
E AssertionError: assert '' == 'hello\n'
E - hello
tests/test_sp.py:11: AssertionError
=========================== short test summary info ============================
FAILED tests/test_sp.py::test_sp - AssertionError: assert '' == 'hello\n'
============================== 1 failed in 0.06s ===============================
An TypeError
is raised at fake_popen.py
:271 when calling with a file passed to argument stderr
if no stderr
was given during registration
> buffer.write(data_type(data))
E TypeError: encoding without a string argument
pyvenv\Lib\site-packages\pytest_subprocess\fake_popen.py:271: TypeError
Below is an example demonstrating the error.
def test_fp_bug(fp):
with NamedTemporaryFile("wb", delete=False) as f:
fp.register(
["git", 'rev-parse', 'HEAD'],
stdout=["47c77698dd8e0e35af00da56806fe98fcb9a1056"],
)
p = subprocess.Popen(('git', 'rev-parse', 'HEAD'), stdout=f.file, stderr=f.file)
p.wait()
fp.allow_unregistered(True)
p = subprocess.Popen(('git', 'rev-parse', 'HEAD'), stdout=f.file, stderr=f.file)
p.wait()
This raises the type error. I can correct this:
--- fake_popen.py 2024-02-10 06:09:01.897909800 -0500
+++ fake_popen.py 2024-02-10 06:08:49.610943500 -0500
@@ -273,7 +267,7 @@
)
if isinstance(data, (list, tuple)):
buffer.writelines([data_type(line + "\n") for line in data])
- elif data is not None:
+ else:
buffer.write(data_type(data))
def _convert(self, input: Union[str, bytes]) -> Union[str, bytes]:
fake_process appears to be incompatible with code that uses asyncio.subprocess.DEVNULL
.
Example test:
def test_devnull(fake_process):
async def impl():
process = await asyncio.create_subprocess_exec(
"cat",
stdin=oasyncio.subprocess.DEVNULL,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
fake_process.register_subprocess("cat")
asyncio.get_event_loop().run_until_complete(impl())
Output:
E AttributeError: module 'pytest_subprocess.asyncio_subprocess' has no attribute 'DEVNULL'
From looking at asyncio_subprocess.py
, my guess would be that DEVNULL
needs to be imported here (and probably STDOUT
too) .
I have some code along the lines of
subprocess.run(['thing'], env={'KEY': some_carefully_constructed_value()})
I can fake_process.register_subprocess(['thing'])
but there doesn't appear to be a way to check the env that was passed to the process
I also had
subprocess.run(f'KEY={some_carefully_constructed_value()} thing', shell=True)
but I couldn't find a way to grab that either
I'm packaging your module as an rpm package so I'm using the typical PEP517 based build, install and test cycle used on building packages from non-root account.
python3 -sBm build -w --no-isolation
build
with --no-isolation
I'm using during all processes only locally installed modulesThe terminate()
, kill()
, and send_signal()
methods of subprocess.Popen
don't appear to be implemented in FakePopen
. Is this intentional?
It would be nice to support anything that supports PathLike. Most of Python supports it except the subprocess.run calls, so I'd understand if you didn't want to, but it's extremely handy (and, as I said, supported pretty much everywhere else - including the latest main branch of nox). It's easy to do - os.fspath
passes through strings and bytes already and calls __fspath__
on anything else. (Correction: 3.8+ does support it)
(Currently, it just silently accepts it and doesn't match, which was confusing me for some time until I realized I'd forgotten to convert to a string on one test register call).
FYI thanks for creating this package. clean and pytest way of testing subprocess commands.
I have followed the simple guide in the readme and created a stderr arg for fake_process.register_subprocess
. However, process.stderr
always returns None
.
Is there something I am missing in generating the stderr? I can get stdout just fine it seems.
According to the asyncio.subprocess.PIPE docs:
If PIPE is passed to stdin argument, the Process.stdin attribute will point to a StreamWriter instance.
If PIPE is passed to stdout or stderr arguments, the Process.stdout and Process.stderr attributes will point to StreamReader instances.
It looks like the object that fake_subprocess returns from create_process_exec
(and probably create_process_shell
) don't mimic this correctly. Here's an example test:
import asyncio
import os
def test_stdio_and_stderr(fake_process):
async def impl():
process = await asyncio.create_subprocess_exec(
"ls", "/",
stdin=open(os.devnull, "r"), # work around #64
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
# wait for the process
async def _read_stream(
stream: asyncio.StreamReader, logtype: str,
):
while True:
line = await stream.readline()
if not line:
break
# do something with line
await asyncio.wait(
[
asyncio.create_task(
_read_stream(process.stdout, "stdout")
),
asyncio.create_task(
_read_stream(process.stderr, "stderr")
),
asyncio.create_task(process.wait()),
]
)
fake_process.register_subprocess(["ls", "/"], stdout=[b"one", b"two"])
asyncio.get_event_loop().run_until_complete(impl())
# Force pytest to display the asyncio exception messages
assert False
The expected output is for there to be a failed assertion -- but nothing else. Here's the actual output:
------------------------------------------- Captured log call --------------------------------------------
ERROR asyncio:base_events.py:1707 Task exception was never retrieved
future: <Task finished name='Task-2' coro=<test_stdio_and_stderr.<locals>.impl.<locals>._read_stream() done, defined at /elm0/jranieri/src/gtirb-server2/server/test/test_stdio.py:15> exception=TypeError("object bytes can't be used in 'await' expression")>
Traceback (most recent call last):
File "/elm0/jranieri/src/gtirb-server2/server/test/test_stdio.py", line 19, in _read_stream
line = await stream.readline()
TypeError: object bytes can't be used in 'await' expression
ERROR asyncio:base_events.py:1707 Task exception was never retrieved
future: <Task finished name='Task-3' coro=<test_stdio_and_stderr.<locals>.impl.<locals>._read_stream() done, defined at /elm0/jranieri/src/gtirb-server2/server/test/test_stdio.py:15> exception=TypeError("object bytes can't be used in 'await' expression")>
Traceback (most recent call last):
File "/elm0/jranieri/src/gtirb-server2/server/test/test_stdio.py", line 19, in _read_stream
line = await stream.readline()
TypeError: object bytes can't be used in 'await' expression
======================================== short test summary info =========================================
FAILED test_stdio.py::test_stdio_and_stderr - assert False
If I add this test to tests/test_subprocess.py
:
def test_raise_exception_check_output(fake_process):
def callback_function(process):
process.returncode = 1
raise PermissionError("exception raised by subprocess")
fake_process.register_subprocess(["test"], callback=callback_function)
with pytest.raises(PermissionError, match="exception raised by subprocess"):
process = subprocess.check_output(["test"])
assert process.returncode == 1
The test fails.
This should behave the same as the code in test_raise_exception
does, shouldn't it?
I'm trying to test a part of code, in which a call to subprocess.run() raises a PermissionError. How can this be done with the pytest-subprocess fixture?
Thanks for the great piece of code and your support in advance ;)
We intend to update pytest-asyncio
to the latest release (0.23.5.post1) in Fedora. Testing packages that depend on it, showed pytest-subprocess
tests failing, because warnings are turned into errors as per:
Lines 3 to 5 in cc26247
resulting in:
==================================== ERRORS ====================================
__________________ ERROR at setup of test_basic_usage[shell] ___________________
fixturedef = <FixtureDef argname='event_loop' scope='function' baseid='tests/test_asyncio.py'>
@pytest.hookimpl(hookwrapper=True)
def pytest_fixture_setup(
fixturedef: FixtureDef,
) -> Generator[None, Any, None]:
"""Adjust the event loop policy when an event loop is produced."""
if fixturedef.argname == "event_loop":
# The use of a fixture finalizer is preferred over the
# pytest_fixture_post_finalizer hook. The fixture finalizer is invoked once
# for each fixture, whereas the hook may be invoked multiple times for
# any specific fixture.
# see https://github.com/pytest-dev/pytest/issues/5848
_add_finalizers(
fixturedef,
_close_event_loop,
_restore_event_loop_policy(asyncio.get_event_loop_policy()),
_provide_clean_event_loop,
)
outcome = yield
loop: asyncio.AbstractEventLoop = outcome.get_result()
# Weird behavior was observed when checking for an attribute of FixtureDef.func
# Instead, we now check for a special attribute of the returned event loop
fixture_filename = inspect.getsourcefile(fixturedef.func)
if not getattr(loop, "__original_fixture_loop", False):
_, fixture_line_number = inspect.getsourcelines(fixturedef.func)
> warnings.warn(
_REDEFINED_EVENT_LOOP_FIXTURE_WARNING
% (fixture_filename, fixture_line_number),
DeprecationWarning,
)
E DeprecationWarning: The event_loop fixture provided by pytest-asyncio has been redefined in
E /builddir/build/BUILD/pytest-subprocess-1.5.0/tests/test_asyncio.py:14
E Replacing the event_loop fixture with a custom implementation is deprecated
E and will lead to errors in the future.
E If you want to request an asyncio event loop with a scope other than function
E scope, use the "scope" argument to the asyncio mark when marking the tests.
E If you want to return different types of event loops, use the event_loop_policy
E fixture.
/usr/lib/python3.12/site-packages/pytest_asyncio/plugin.py:769: DeprecationWarning
_________________ ERROR at teardown of test_basic_usage[shell] _________________
def _close_event_loop() -> None:
policy = asyncio.get_event_loop_policy()
try:
> loop = policy.get_event_loop()
/usr/lib/python3.12/site-packages/pytest_asyncio/plugin.py:823:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <asyncio.unix_events._UnixDefaultEventLoopPolicy object at 0x7fd70d9466c0>
def get_event_loop(self):
"""Get the event loop for the current context.
Returns an instance of EventLoop or raises an exception.
"""
if (self._local._loop is None and
not self._local._set_called and
threading.current_thread() is threading.main_thread()):
stacklevel = 2
try:
f = sys._getframe(1)
except AttributeError:
pass
else:
# Move up the call stack so that the warning is attached
# to the line outside asyncio itself.
while f:
module = f.f_globals.get('__name__')
if not (module == 'asyncio' or module.startswith('asyncio.')):
break
f = f.f_back
stacklevel += 1
import warnings
> warnings.warn('There is no current event loop',
DeprecationWarning, stacklevel=stacklevel)
E DeprecationWarning: There is no current event loop
/usr/lib64/python3.12/asyncio/events.py:697: DeprecationWarning
(Above is a sample. The same happens for all tests from test_asyncio.py
)
We've turned off that setting for now.
This is how I am invoking a process in the code I am testing:
output = subprocess.check_output(
'/usr/lib/update-notifier/apt-check',
stderr=subprocess.STDOUT).decode('utf8')
This is how I am registering the process:
fake_process.register_subprocess('/usr/lib/update-notifier/apt-check',
stdout=("0;0",))
This is what I get when I try to run my test:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
client/plugins/os_updates.py:255: in ubuntu_checker
output = subprocess.check_output(
/usr/lib/python3.8/subprocess.py:411: in check_output
return run(*popenargs, stdout=PIPE, timeout=timeout, check=True,
/usr/lib/python3.8/subprocess.py:489: in run
with Popen(*popenargs, **kwargs) as process:
../../.virtualenvs/PenguinDome/lib/python3.8/site-packages/pytest_subprocess/core.py:262: in dispatch
result.configure(**kwargs)
../../.virtualenvs/PenguinDome/lib/python3.8/site-packages/pytest_subprocess/core.py:126: in configure
self.stdout = self._prepare_buffer(self.__stderr, self.stdout)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <pytest_subprocess.core.FakePopen object at 0x7f4608c8cdc0>, input = None
io_base = <_io.BytesIO object at 0x7f46081dc4f0>
def _prepare_buffer(self, input, io_base=None):
linesep = self._convert(os.linesep)
if isinstance(input, (list, tuple)):
input = linesep.join(map(self._convert, input))
if isinstance(input, str) and not self.text_mode:
input = input.encode()
if isinstance(input, bytes) and self.text_mode:
input = input.decode()
if input:
if not input.endswith(linesep):
input += linesep
if self.text_mode and self.__universal_newlines:
input = input.replace("\r\n", "\n")
if io_base is not None:
> input = io_base.getvalue() + input
E TypeError: can't concat NoneType to bytes
../../.virtualenvs/PenguinDome/lib/python3.8/site-packages/pytest_subprocess/core.py:149: TypeError
It is impossible to properly unit-test functionalities like sending a signal, when the tested class facades subprocess.Popen
without exposing the resulting process object. Rummaging in class internals during unit tests is a bad practice which leads to brittle testcases.
It would be nice if pytest-subprocess
had a way of getting the result of faked Popen
(or its proxy). That way we could assert some side effect without having to extract the Popen
result from the unit under test:
def test_kills_the_process(fake_subprocess):
process = fake_subprocess.register(["command"])
run_and_kill("command")
assert process.received_signals() == (signals.SIGKILL,)
I'm packaging your module as an rpm package so I'm using the typical PEP517 based build, install and test cycle used on building packages from non-root account.
python3 -sBm build -w --no-isolation
build
with --no-isolation
I'm using during all processes only locally installed modulesinstaller
modulecut off from access to the public network
(pytest is executed with -m "not network"
)Python 3.9.18 and pytest 8.1.1.
Package Version
----------------------------- -----------
alabaster 0.7.16
anyio 4.3.0
Babel 2.14.0
build 1.1.1
changelogd 0.1.7
charset-normalizer 3.3.2
click 8.1.7
docutils 0.20.1
exceptiongroup 1.1.3
idna 3.6
imagesize 1.4.1
importlib_metadata 7.1.0
iniconfig 2.0.0
installer 0.7.0
Jinja2 3.1.3
MarkupSafe 2.1.3
packaging 24.0
pluggy 1.4.0
Pygments 2.17.2
pyproject_hooks 1.0.0
pytest 8.1.1
pytest-asyncio 0.23.6
pytest-rerunfailures 12.0
python-dateutil 2.9.0.post0
requests 2.31.0
ruamel.yaml 0.18.5
ruamel.yaml.clib 0.2.8
setuptools 69.1.1
sniffio 1.3.0
snowballstemmer 2.2.0
Sphinx 7.2.6
sphinx-autodoc-typehints 2.0.0
sphinxcontrib-applehelp 1.0.8
sphinxcontrib-devhelp 1.0.5
sphinxcontrib-htmlhelp 2.0.5
sphinxcontrib-jsmath 1.0.1
sphinxcontrib-qthelp 1.0.7
sphinxcontrib-serializinghtml 1.1.10
tokenize_rt 5.2.0
toml 0.10.2
tomli 2.0.1
typing_extensions 4.10.0
urllib3 1.26.18
wheel 0.43.0
zipp 3.17.0
Please let me know if you need more details or want me to perform some diagnostics.
Currently a finite number of occurrences per command is allowed, e.g. for pass_command
. Would be nice if an arbitrary number of occurrences is supported.
Seems like currently this behavior is implemented using a deque. Didn't read the code very thoroughly so I'm not sure how the deque is used, but one approach could be storing an iterator instead which would support an infinite iterator e.g. via itertools.repeat
.
i have a function that executes:
def f():
return subprocess.check_output('secret_command', encoding='utf-8').strip()
but when i test it with:
retvalue = '1029384756'
with fake_process.context() as nested_process:
nested_process.register_subprocess('secret_command', stdout=retvalue)
node_id = f()
assert node_id == retvalue
it fails with:
> assert node_id == retvalue
E AssertionError: assert b'1029384756' == '1029384756'
i believe because stdout
is passed to _prepare_buffer()
https://github.com/aklajnert/pytest-subprocess/blob/master/pytest_subprocess/core.py#L135 which will encode() the output. there's a chance to avoid it by setting self.text
but i couldnt find a way from register_subprocess
to set that
Hello,
I am having troubles with one of our mocked processes.
Specifically the args attribute.
I'm running into a problem where instead of getting a tuple from .args
we get an object of type pytest_subprocess.utils.Command
. We need to iterate over args
which should be a tuple but your Command class which was introduced in 1.0.0
is not iterable.
When you print p.args
(where p
is the returned process) it prints what looks to be a tuple but is not. The tuple itself is in p.args.command
.
See the below:
============================
contents and type of p.args
============================
('command', 'being', 'run', 'plus', 'passed', 'args')
<class 'pytest_subprocess.utils.Command'>
============================
contents and type of p.args.command
============================
('command', 'being', 'run', 'plus', 'passed', 'args')
<class 'tuple'>
pytest_subprocess.utils.Command
can be made iterable and solve this problem if an __iter__
method is added to the Command class, adding the below locally fixes the problem:
def __iter__(self):
return iter(self.command)
Kind Regards
Gino Cubeddu
First of all currently on use sphinx-build
command to build documentation out of source tree sphinx cannot find pytest-subprocess
code
+ /usr/bin/sphinx-build -n -T -b man docs build/sphinx/man
Running Sphinx v7.2.6
WARNING:root:Generated changelog file to history.rst
making output directory... done
building [mo]: targets for 0 po files that are out of date
writing output...
building [man]: all manpages
updating environment: [new config] 4 added, 0 changed, 0 removed
reading sources... [100%] usage
WARNING: autodoc: failed to import class 'fake_process.FakeProcess' from module 'pytest_subprocess'; the following exception was raised:
No module named 'pytest_subprocess'
WARNING: autodoc: failed to import class 'utils.Any' from module 'pytest_subprocess'; the following exception was raised:
No module named 'pytest_subprocess'
looking for now-outdated files... none found
pickling environment... done
checking consistency... done
writing... python-pytest-subprocess.3 { usage api history } done
build succeeded, 2 warnings.
This can be fixed by patch like below:
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -8,9 +8,10 @@
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
-# import os
-# import sys
-# sys.path.insert(0, os.path.abspath('.'))
+import os
+import sys
+sys.path.insert(0, os.path.abspath(".."))
+
import datetime
from pathlib import Path
This patch fixes what is in the comment and that can of fix is suggested in sphinx example copy.py https://www.sphinx-doc.org/en/master/usage/configuration.html#example-of-configuration-file
Than .. on building my packages I'm using sphinx-build
command with -n
switch which shows warmings about missing references. These are not critical issues.
+ /usr/bin/sphinx-build -n -T -b man docs build/sphinx/man
Running Sphinx v7.2.6
WARNING:root:Generated changelog file to history.rst
making output directory... done
building [mo]: targets for 0 po files that are out of date
writing output...
building [man]: all manpages
updating environment: [new config] 4 added, 0 changed, 0 removed
reading sources... [100%] usage
looking for now-outdated files... none found
pickling environment... done
checking consistency... done
writing... python-pytest-subprocess.3 { usage api history } <unknown>:1: WARNING: py:data reference target not found: typing.Union
<unknown>:1: WARNING: py:class reference target not found: collections.abc.Sequence
<unknown>:1: WARNING: py:data reference target not found: typing.Union
<unknown>:1: WARNING: py:class reference target not found: os.PathLike
<unknown>:1: WARNING: py:class reference target not found: pytest_subprocess.utils.Program
<unknown>:1: WARNING: py:class reference target not found: pytest_subprocess.utils.Command
<unknown>:1: WARNING: py:data reference target not found: typing.Union
<unknown>:1: WARNING: py:class reference target not found: collections.abc.Sequence
<unknown>:1: WARNING: py:data reference target not found: typing.Union
<unknown>:1: WARNING: py:class reference target not found: os.PathLike
<unknown>:1: WARNING: py:class reference target not found: pytest_subprocess.utils.Program
<unknown>:1: WARNING: py:class reference target not found: pytest_subprocess.utils.Command
<unknown>:1: WARNING: py:class reference target not found: pytest_subprocess.utils.Program
<unknown>:1: WARNING: py:data reference target not found: typing.Union
<unknown>:1: WARNING: py:class reference target not found: collections.abc.Sequence
<unknown>:1: WARNING: py:data reference target not found: typing.Union
<unknown>:1: WARNING: py:class reference target not found: os.PathLike
<unknown>:1: WARNING: py:class reference target not found: pytest_subprocess.utils.Program
<unknown>:1: WARNING: py:class reference target not found: pytest_subprocess.utils.Command
<unknown>:1: WARNING: py:data reference target not found: typing.Union
<unknown>:1: WARNING: py:class reference target not found: collections.abc.Sequence
<unknown>:1: WARNING: py:data reference target not found: typing.Union
<unknown>:1: WARNING: py:data reference target not found: typing.Union
<unknown>:1: WARNING: py:class reference target not found: collections.abc.Sequence
<unknown>:1: WARNING: py:data reference target not found: typing.Union
<unknown>:1: WARNING: py:data reference target not found: typing.Optional
<unknown>:1: WARNING: py:data reference target not found: typing.Optional
<unknown>:1: WARNING: py:class reference target not found: collections.abc.Callable
<unknown>:1: WARNING: py:data reference target not found: typing.Optional
<unknown>:1: WARNING: py:data reference target not found: typing.Any
<unknown>:1: WARNING: py:data reference target not found: typing.Optional
<unknown>:1: WARNING: py:class reference target not found: collections.abc.Callable
<unknown>:1: WARNING: py:data reference target not found: typing.Optional
<unknown>:1: WARNING: py:class reference target not found: collections.abc.Callable
<unknown>:1: WARNING: py:class reference target not found: pytest_subprocess.process_recorder.ProcessRecorder
<unknown>:1: WARNING: py:data reference target not found: typing.Union
<unknown>:1: WARNING: py:class reference target not found: collections.abc.Sequence
<unknown>:1: WARNING: py:data reference target not found: typing.Union
<unknown>:1: WARNING: py:class reference target not found: os.PathLike
<unknown>:1: WARNING: py:class reference target not found: pytest_subprocess.utils.Program
<unknown>:1: WARNING: py:class reference target not found: pytest_subprocess.utils.Command
<unknown>:1: WARNING: py:data reference target not found: typing.Union
<unknown>:1: WARNING: py:class reference target not found: collections.abc.Sequence
<unknown>:1: WARNING: py:data reference target not found: typing.Union
<unknown>:1: WARNING: py:data reference target not found: typing.Union
<unknown>:1: WARNING: py:class reference target not found: collections.abc.Sequence
<unknown>:1: WARNING: py:data reference target not found: typing.Union
<unknown>:1: WARNING: py:data reference target not found: typing.Optional
<unknown>:1: WARNING: py:data reference target not found: typing.Optional
<unknown>:1: WARNING: py:class reference target not found: collections.abc.Callable
<unknown>:1: WARNING: py:data reference target not found: typing.Optional
<unknown>:1: WARNING: py:data reference target not found: typing.Any
<unknown>:1: WARNING: py:data reference target not found: typing.Optional
<unknown>:1: WARNING: py:class reference target not found: collections.abc.Callable
<unknown>:1: WARNING: py:data reference target not found: typing.Optional
<unknown>:1: WARNING: py:class reference target not found: collections.abc.Callable
<unknown>:1: WARNING: py:class reference target not found: pytest_subprocess.process_recorder.ProcessRecorder
<unknown>:1: WARNING: py:data reference target not found: typing.Optional
<unknown>:1: WARNING: py:data reference target not found: typing.Optional
done
build succeeded, 59 warnings.
You can peak on fixes that kind of issues in other projects
RDFLib/rdflib-sqlalchemy#95
RDFLib/rdflib#2036
click-contrib/sphinx-click@abc31069
frostming/unearth#14
jaraco/cssutils#21
latchset/jwcrypto#289
latchset/jwcrypto#289
pypa/distlib@98b9b89f
pywbem/pywbem#2895
sissaschool/elementpath@bf869d9e
sissaschool/xmlschema@42ea98f2
sqlalchemy/sqlalchemy@5e88e6e8
__________________________________________________________________________________________ ERROR at setup of test_multiple_wait[True] __________________________________________________________________________________________
self = <flaky.flaky_pytest_plugin.FlakyPlugin object at 0x8cfd165e0>, item = <Function test_multiple_wait[True]>
def pytest_runtest_setup(self, item):
"""
Pytest hook to modify the test before it's run.
:param item:
The test item.
"""
if not self._has_flaky_attributes(item):
if hasattr(item, 'iter_markers'):
for marker in item.iter_markers(name='flaky'):
> self._make_test_flaky(item, *marker.args, **marker.kwargs)
E TypeError: _make_test_flaky() got an unexpected keyword argument 'reruns'
/usr/local/lib/python3.9/site-packages/flaky/flaky_pytest_plugin.py:244: TypeError
_____________________________________________________________________________________ ERROR at setup of test_raise_exception_check_output ______________________________________________________________________________________
self = <flaky.flaky_pytest_plugin.FlakyPlugin object at 0x8cfd165e0>, item = <Function test_raise_exception_check_output>
def pytest_runtest_setup(self, item):
"""
Pytest hook to modify the test before it's run.
:param item:
The test item.
"""
if not self._has_flaky_attributes(item):
if hasattr(item, 'iter_markers'):
for marker in item.iter_markers(name='flaky'):
> self._make_test_flaky(item, *marker.args, **marker.kwargs)
E TypeError: _make_test_flaky() got an unexpected keyword argument 'reruns'
/usr/local/lib/python3.9/site-packages/flaky/flaky_pytest_plugin.py:244: TypeError
_________________________________________________________________________________________ ERROR at setup of test_multiple_wait[False] __________________________________________________________________________________________
self = <flaky.flaky_pytest_plugin.FlakyPlugin object at 0x8cfd165e0>, item = <Function test_multiple_wait[False]>
def pytest_runtest_setup(self, item):
"""
Pytest hook to modify the test before it's run.
:param item:
The test item.
"""
if not self._has_flaky_attributes(item):
if hasattr(item, 'iter_markers'):
for marker in item.iter_markers(name='flaky'):
> self._make_test_flaky(item, *marker.args, **marker.kwargs)
E TypeError: _make_test_flaky() got an unexpected keyword argument 'reruns'
/usr/local/lib/python3.9/site-packages/flaky/flaky_pytest_plugin.py:244: TypeError
=========================================================================================================== FAILURES ===========================================================================================================
______________________________________________________________________________________________ test_documentation[docs/index.rst] ______________________________________________________________________________________________
testdir = <Testdir local('/tmp/pytest-of-yuri/pytest-12/test_documentation0')>, rst_file = 'docs/index.rst'
@pytest.mark.parametrize("rst_file", ("docs/index.rst", "README.rst"))
def test_documentation(testdir, rst_file):
imports = "\n".join(
[
"import asyncio",
"import os",
"import sys",
"",
"import pytest",
"import pytest_subprocess",
"import subprocess",
]
)
setup_fixture = (
"\n\n"
"@pytest.fixture(autouse=True)\n"
"def setup():\n"
" os.chdir(os.path.dirname(__file__))\n\n"
)
event_loop_fixture = (
"\n\n"
"@pytest.fixture(autouse=True)\n"
"def event_loop(request):\n"
" policy = asyncio.get_event_loop_policy()\n"
' if sys.platform == "win32":\n'
" loop = asyncio.ProactorEventLoop()\n"
" else:\n"
" loop = policy.get_event_loop()\n"
" yield loop\n"
" loop.close()\n"
)
code_blocks = "\n".join(get_code_blocks(ROOT_DIR / rst_file))
testdir.makepyfile(
imports + setup_fixture + event_loop_fixture + "\n" + code_blocks
)
> result = testdir.inline_run()
/usr/ports/devel/py-pytest-subprocess/work-py39/pytest-subprocess-1.5.0/tests/test_examples.py:60:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/usr/local/lib/python3.9/site-packages/_pytest/legacypath.py:173: in inline_run
return self._pytester.inline_run(
/usr/local/lib/python3.9/site-packages/_pytest/pytester.py:1135: in inline_run
ret = main([str(x) for x in args], plugins=plugins)
/usr/local/lib/python3.9/site-packages/_pytest/config/__init__.py:147: in main
config = _prepareconfig(args, plugins)
/usr/local/lib/python3.9/site-packages/_pytest/config/__init__.py:328: in _prepareconfig
config = pluginmanager.hook.pytest_cmdline_parse(
/usr/local/lib/python3.9/site-packages/pluggy/_hooks.py:265: in __call__
return self._hookexec(self.name, self.get_hookimpls(), kwargs, firstresult)
/usr/local/lib/python3.9/site-packages/pluggy/_manager.py:80: in _hookexec
return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
/usr/local/lib/python3.9/site-packages/_pytest/helpconfig.py:103: in pytest_cmdline_parse
config: Config = outcome.get_result()
/usr/local/lib/python3.9/site-packages/_pytest/config/__init__.py:1067: in pytest_cmdline_parse
self.parse(args)
/usr/local/lib/python3.9/site-packages/_pytest/config/__init__.py:1354: in parse
self._preparse(args, addopts=addopts)
/usr/local/lib/python3.9/site-packages/_pytest/config/__init__.py:1237: in _preparse
self.pluginmanager.load_setuptools_entrypoints("pytest11")
/usr/local/lib/python3.9/site-packages/pluggy/_manager.py:288: in load_setuptools_entrypoints
self.register(plugin, name=ep.name)
/usr/local/lib/python3.9/site-packages/_pytest/config/__init__.py:488: in register
ret: Optional[str] = super().register(plugin, name)
/usr/local/lib/python3.9/site-packages/pluggy/_manager.py:103: in register
hookimpl_opts = self.parse_hookimpl_opts(plugin, name)
/usr/local/lib/python3.9/site-packages/_pytest/config/__init__.py:459: in parse_hookimpl_opts
return _get_legacy_hook_marks(
/usr/local/lib/python3.9/site-packages/_pytest/config/__init__.py:373: in _get_legacy_hook_marks
warn_explicit_for(cast(FunctionType, method), message)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
method = <function pytest_load_initial_conftests at 0x8744c2820>
message = PytestDeprecationWarning('The hookimpl pytest_load_initial_conftests uses old-style configuration options (marks or at...igure the hooks.\n See https://docs.pytest.org/en/latest/deprecations.html#configuring-hook-specs-impls-using-markers')
def warn_explicit_for(method: FunctionType, message: PytestWarning) -> None:
"""
Issue the warning :param:`message` for the definition of the given :param:`method`
this helps to log warnigns for functions defined prior to finding an issue with them
(like hook wrappers being marked in a legacy mechanism)
"""
lineno = method.__code__.co_firstlineno
filename = inspect.getfile(method)
module = method.__module__
mod_globals = method.__globals__
try:
warnings.warn_explicit(
message,
type(message),
filename=filename,
module=module,
registry=mod_globals.setdefault("__warningregistry__", {}),
lineno=lineno,
)
except Warning as w:
# If warnings are errors (e.g. -Werror), location information gets lost, so we add it to the message.
> raise type(w)(f"{w}\n at {filename}:{lineno}") from None
E pytest.PytestDeprecationWarning: The hookimpl pytest_load_initial_conftests uses old-style configuration options (marks or attributes).
E Please use the pytest.hookimpl(tryfirst=True) decorator instead
E to configure the hooks.
E See https://docs.pytest.org/en/latest/deprecations.html#configuring-hook-specs-impls-using-markers
E at /usr/local/lib/python3.9/site-packages/pytest_cov/plugin.py:115
/usr/local/lib/python3.9/site-packages/_pytest/warning_types.py:170: PytestDeprecationWarning
----------------------------------------------------------------------------------------------------- Captured stderr call -----------------------------------------------------------------------------------------------------
<string>:30: (ERROR/3) Unknown directive type "toctree".
.. toctree::
:maxdepth: 2
usage
api
history
<string>:42: (ERROR/3) Unknown interpreted text role "ref".
<string>:43: (ERROR/3) Unknown interpreted text role "ref".
<string>:44: (ERROR/3) Unknown interpreted text role "ref".
________________________________________________________________________________________________ test_documentation[README.rst] ________________________________________________________________________________________________
testdir = <Testdir local('/tmp/pytest-of-yuri/pytest-12/test_documentation1')>, rst_file = 'README.rst'
@pytest.mark.parametrize("rst_file", ("docs/index.rst", "README.rst"))
def test_documentation(testdir, rst_file):
imports = "\n".join(
[
"import asyncio",
"import os",
"import sys",
"",
"import pytest",
"import pytest_subprocess",
"import subprocess",
]
)
setup_fixture = (
"\n\n"
"@pytest.fixture(autouse=True)\n"
"def setup():\n"
" os.chdir(os.path.dirname(__file__))\n\n"
)
event_loop_fixture = (
"\n\n"
"@pytest.fixture(autouse=True)\n"
"def event_loop(request):\n"
" policy = asyncio.get_event_loop_policy()\n"
' if sys.platform == "win32":\n'
" loop = asyncio.ProactorEventLoop()\n"
" else:\n"
" loop = policy.get_event_loop()\n"
" yield loop\n"
" loop.close()\n"
)
code_blocks = "\n".join(get_code_blocks(ROOT_DIR / rst_file))
testdir.makepyfile(
imports + setup_fixture + event_loop_fixture + "\n" + code_blocks
)
> result = testdir.inline_run()
/usr/ports/devel/py-pytest-subprocess/work-py39/pytest-subprocess-1.5.0/tests/test_examples.py:60:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/usr/local/lib/python3.9/site-packages/_pytest/legacypath.py:173: in inline_run
return self._pytester.inline_run(
/usr/local/lib/python3.9/site-packages/_pytest/pytester.py:1135: in inline_run
ret = main([str(x) for x in args], plugins=plugins)
/usr/local/lib/python3.9/site-packages/_pytest/config/__init__.py:147: in main
config = _prepareconfig(args, plugins)
/usr/local/lib/python3.9/site-packages/_pytest/config/__init__.py:328: in _prepareconfig
config = pluginmanager.hook.pytest_cmdline_parse(
/usr/local/lib/python3.9/site-packages/pluggy/_hooks.py:265: in __call__
return self._hookexec(self.name, self.get_hookimpls(), kwargs, firstresult)
/usr/local/lib/python3.9/site-packages/pluggy/_manager.py:80: in _hookexec
return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
/usr/local/lib/python3.9/site-packages/_pytest/helpconfig.py:103: in pytest_cmdline_parse
config: Config = outcome.get_result()
/usr/local/lib/python3.9/site-packages/_pytest/config/__init__.py:1067: in pytest_cmdline_parse
self.parse(args)
/usr/local/lib/python3.9/site-packages/_pytest/config/__init__.py:1354: in parse
self._preparse(args, addopts=addopts)
/usr/local/lib/python3.9/site-packages/_pytest/config/__init__.py:1237: in _preparse
self.pluginmanager.load_setuptools_entrypoints("pytest11")
/usr/local/lib/python3.9/site-packages/pluggy/_manager.py:288: in load_setuptools_entrypoints
self.register(plugin, name=ep.name)
/usr/local/lib/python3.9/site-packages/_pytest/config/__init__.py:488: in register
ret: Optional[str] = super().register(plugin, name)
/usr/local/lib/python3.9/site-packages/pluggy/_manager.py:103: in register
hookimpl_opts = self.parse_hookimpl_opts(plugin, name)
/usr/local/lib/python3.9/site-packages/_pytest/config/__init__.py:459: in parse_hookimpl_opts
return _get_legacy_hook_marks(
/usr/local/lib/python3.9/site-packages/_pytest/config/__init__.py:373: in _get_legacy_hook_marks
warn_explicit_for(cast(FunctionType, method), message)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
method = <function pytest_load_initial_conftests at 0x8744c2820>
message = PytestDeprecationWarning('The hookimpl pytest_load_initial_conftests uses old-style configuration options (marks or at...igure the hooks.\n See https://docs.pytest.org/en/latest/deprecations.html#configuring-hook-specs-impls-using-markers')
def warn_explicit_for(method: FunctionType, message: PytestWarning) -> None:
"""
Issue the warning :param:`message` for the definition of the given :param:`method`
this helps to log warnigns for functions defined prior to finding an issue with them
(like hook wrappers being marked in a legacy mechanism)
"""
lineno = method.__code__.co_firstlineno
filename = inspect.getfile(method)
module = method.__module__
mod_globals = method.__globals__
try:
warnings.warn_explicit(
message,
type(message),
filename=filename,
module=module,
registry=mod_globals.setdefault("__warningregistry__", {}),
lineno=lineno,
)
except Warning as w:
# If warnings are errors (e.g. -Werror), location information gets lost, so we add it to the message.
> raise type(w)(f"{w}\n at {filename}:{lineno}") from None
E pytest.PytestDeprecationWarning: The hookimpl pytest_load_initial_conftests uses old-style configuration options (marks or attributes).
E Please use the pytest.hookimpl(tryfirst=True) decorator instead
E to configure the hooks.
E See https://docs.pytest.org/en/latest/deprecations.html#configuring-hook-specs-impls-using-markers
E at /usr/local/lib/python3.9/site-packages/pytest_cov/plugin.py:115
/usr/local/lib/python3.9/site-packages/_pytest/warning_types.py:170: PytestDeprecationWarning
=================================================================================================== short test summary info ====================================================================================================
SKIPPED [4] tests/test_asyncio.py:114: condition: sys.platform!="win32"
===================================================================================== 2 failed, 127 passed, 4 skipped, 3 errors in 12.56s ======================================================================================
*** Error code 1
Version: 1.5.0
Python-3.9
FreeBSD 13.2
I have a script that internally is using subproccess.run to call some external executable (e.g dosomething).
So for example
$ python myscript.py do
It internally results in a subprocess call of dosomething /tmp
Test about this base functionality is like
def test_main(fp):
fp.register(['dosomething', '/tmp'])
main(['do'])
Some of the script configurations (sort of hidden) are using env variables. So for example
$ MYSCRIPT_TMP_FOLDER=/mytmp python myscript.py do
It internally results in a subprocess call of dosomething /mytmp
My attempt to test it is like
def test_main(fp):
fp.register(['dosomething', '/mytmp'])
with mock.patch.dict(os.environ, {'MYSCRIPT_TMP_FOLDER': 'mytmp'}):
main(['do'])
Unfortunately the with mock (or something else) seems to interfere with fp. As result the true subprocess and so the dosomething executable is called during the test.
Expected behavior is that fp to keep to be able to intercept submodule call and not to perform it.
I've been really enjoying this, but I have had a small pain point: the name must be fully specified. Here's an example:
cmake_path = Path(cmake.CMAKE_BIN_DIR) / "cmake"
fp.register([os.fspath(cmake_path), "--version"], stdout="3.15.0")
I tried the nuclear option, but it didn't seem to work (and really isn't what I would normally want to do anyway):
fp.register([fp.any(), "--version"], stdout="3.15.0")
A really handy shortcut would be something like this:
fp.register([fp.program("make"), "--version"], stdout="3.15.0")
which would only match the final component of the path. Maybe this could even be automatic, such as "thing"
matching "/any/thing"
or shutil.which("thing")
it it's not an absolute path. I'd think the automatic one should require it be on the path - but I'm also interested in a fuzzy one that doesn't require it be on the path.
This test works as expected - it fails because of an exception:
def callback_function(process):
process.returncode = 1
raise PermissionError("exception raised by subprocess")
def test_raise_exception(fake_process):
fake_process.register(["test"], callback=callback_function)
process = subprocess.Popen(["test"])
process.wait()
raise PermissionError("exception raised by subprocess")
PermissionError: exception raised by subprocess
Replacing process.wait()
with process.communicate()
does not trigger this side effect. Since communicate
is basically wait
+ read
, shouldn't it raise an exception too?
When fp.register
stubs an asyncio subprocess, and a callback is provided, process.stdout.read()
or process.stdout.readline()
will loop infinitely. When no callback is provided, the function works as expected.
Raised originally as a PR with a testcase: #117
This issue is covered by an xfailing testcase: test_asyncio_subprocess_using_callback
.
When I try to load the pytest-subprocess
plugin using the -p
pytest option the load fails because it is in conflict with the Python subprocess library:
$ env - pytest -W error -p subprocess
Traceback (most recent call last):
File "/usr/bin/pytest", line 8, in <module>
sys.exit(console_main())
File "/usr/lib/python3.9/vendor-packages/_pytest/config/__init__.py", line 201, in console_main
code = main()
File "/usr/lib/python3.9/vendor-packages/_pytest/config/__init__.py", line 156, in main
config = _prepareconfig(args, plugins)
File "/usr/lib/python3.9/vendor-packages/_pytest/config/__init__.py", line 341, in _prepareconfig
config = pluginmanager.hook.pytest_cmdline_parse(
File "/usr/lib/python3.9/vendor-packages/pluggy/_hooks.py", line 513, in __call__
return self._hookexec(self.name, self._hookimpls.copy(), kwargs, firstresult)
File "/usr/lib/python3.9/vendor-packages/pluggy/_manager.py", line 120, in _hookexec
return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
File "/usr/lib/python3.9/vendor-packages/pluggy/_callers.py", line 139, in _multicall
raise exception.with_traceback(exception.__traceback__)
File "/usr/lib/python3.9/vendor-packages/pluggy/_callers.py", line 122, in _multicall
teardown.throw(exception) # type: ignore[union-attr]
File "/usr/lib/python3.9/vendor-packages/_pytest/helpconfig.py", line 105, in pytest_cmdline_parse
config = yield
File "/usr/lib/python3.9/vendor-packages/pluggy/_callers.py", line 103, in _multicall
res = hook_impl.function(*args)
File "/usr/lib/python3.9/vendor-packages/_pytest/config/__init__.py", line 1140, in pytest_cmdline_parse
self.parse(args)
File "/usr/lib/python3.9/vendor-packages/_pytest/config/__init__.py", line 1490, in parse
self._preparse(args, addopts=addopts)
File "/usr/lib/python3.9/vendor-packages/_pytest/config/__init__.py", line 1373, in _preparse
self.pluginmanager.consider_preparse(args, exclude_only=False)
File "/usr/lib/python3.9/vendor-packages/_pytest/config/__init__.py", line 787, in consider_preparse
self.consider_pluginarg(parg)
File "/usr/lib/python3.9/vendor-packages/_pytest/config/__init__.py", line 810, in consider_pluginarg
self.import_plugin(arg, consider_entry_points=True)
File "/usr/lib/python3.9/vendor-packages/_pytest/config/__init__.py", line 850, in import_plugin
self.rewrite_hook.mark_rewrite(importspec)
File "/usr/lib/python3.9/vendor-packages/_pytest/assertion/rewrite.py", line 263, in mark_rewrite
self._warn_already_imported(name)
File "/usr/lib/python3.9/vendor-packages/_pytest/assertion/rewrite.py", line 270, in _warn_already_imported
self.config.issue_config_time_warning(
File "/usr/lib/python3.9/vendor-packages/_pytest/config/__init__.py", line 1528, in issue_config_time_warning
warnings.warn(warning, stacklevel=stacklevel)
pytest.PytestAssertRewriteWarning: Module already imported so cannot be rewritten: subprocess
$
Possible solution would be to rename the plugin (in the entry_points.txt
file).
I want to mock specific calls to subprocess but the command inputs are varying, e.g. cmake -S/path/to/generated_data -B/random/build_path
. I would thus like to use regex match these arguments and make sure other commands like cmake --build
are not called.
The new release installs a stray top-level tests
package, i.e.:
/usr/lib/python3.12/site-packages/tests/__init__.py
/usr/lib/python3.12/site-packages/tests/conftest.py
/usr/lib/python3.12/site-packages/tests/example_script.py
...
__________________________ test_multiple_wait[False] ___________________________
fake_process = <pytest_subprocess.core.FakeProcess object at 0x7ffff6a99a60>
fake = False
@pytest.mark.parametrize("fake", [False, True])
def test_multiple_wait(fake_process, fake):
"""
Wait multiple times for 0.2 seconds with process lasting for 0.5.
Third wait shall not raise an exception.
"""
fake_process.allow_unregistered(not fake)
if fake:
fake_process.register_subprocess(
["python", "example_script.py", "wait"], wait=0.5,
)
process = subprocess.Popen(("python", "example_script.py", "wait"),)
with pytest.raises(subprocess.TimeoutExpired):
process.wait(timeout=0.2)
with pytest.raises(subprocess.TimeoutExpired):
process.wait(timeout=0.2)
> process.wait(0.2)
/build/source/tests/test_subprocess.py:420:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/nix/store/1b0k48v6131drfl737qr1v53yk37k4d6-python3-3.9.2/lib/python3.9/subprocess.py:1189: in wait
return self._wait(timeout=timeout)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Popen: returncode: None args: ['python', 'example_script.py', 'wait']>
timeout = 0.2
def _wait(self, timeout):
"""Internal implementation of wait() on POSIX."""
if self.returncode is not None:
return self.returncode
if timeout is not None:
endtime = _time() + timeout
# Enter a busy loop if we have a timeout. This busy loop was
# cribbed from Lib/threading.py in Thread.wait() at r71065.
delay = 0.0005 # 500 us -> initial delay of 1 ms
while True:
if self._waitpid_lock.acquire(False):
try:
if self.returncode is not None:
break # Another thread waited.
(pid, sts) = self._try_wait(os.WNOHANG)
assert pid == self.pid or pid == 0
if pid == self.pid:
self._handle_exitstatus(sts)
break
finally:
self._waitpid_lock.release()
remaining = self._remaining_time(endtime)
if remaining <= 0:
> raise TimeoutExpired(self.args, timeout)
E subprocess.TimeoutExpired: Command '('python', 'example_script.py', 'wait')' timed out after 0.2 seconds
/nix/store/1b0k48v6131drfl737qr1v53yk37k4d6-python3-3.9.2/lib/python3.9/subprocess.py:1911: TimeoutExpired
If the process I am invoking in the code I'm testing has no arguments, then I should be able to register it with fake_process
using either just the same string that is being passed to check_output
, or as a list or tuple, but right now, only the former works and the latter doesn't.
For example, if this is what I am testing:
output = subprocess.check_output("some-command")
Then this:
fake_process.register_subprocess(("some-command",))
and this:
fake_process.register_subprocess("some-command")
should both work, but only the latter works.
Is it possible to specify processes to mock without specifying the entire command?
For example, being able to do
fake_process.register_subprocess("cp")
And have it fake all calls to cp
, regardless of arguments? At the moment I'm having to specify every single possible argument that my application can use in order to get a match.
Am I missing something?
Not sure where the error comes from, but I get a "'method' object is not subscriptable". The code path it was trying to evaluate is:
def _spawn_process() -> subprocess.Popen[bytes]:
return subprocess.Popen(
self.to_popen(),
cwd=cwd,
shell=shell,
env=actual_env.to_popen() if actual_env is not None else None,
start_new_session=True,
stdin=subprocess.DEVNULL,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT if join else subprocess.PIPE,
executable=executable)
# Spawn the child process
try:
process = _spawn_process()
fake_process appears to be incompatible with code that uses anyio with asyncio.
Example test:
def test_anyio(fake_process):
async def impl():
await anyio.sleep(1)
asyncio.get_event_loop().run_until_complete(impl())
Error:
/usr/lib/python3.8/asyncio/base_events.py:616: in run_until_complete
return future.result()
tmp/async_devnull_test.py:28: in impl
await anyio.sleep(1)
/usr/local/lib/python3.8/dist-packages/anyio/_core/_eventloop.py:69: in sleep
return await get_asynclib().sleep(delay)
/usr/local/lib/python3.8/dist-packages/anyio/_core/_eventloop.py:140: in get_asynclib
return import_module(modulename)
/usr/lib/python3.8/importlib/__init__.py:127: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
<frozen importlib._bootstrap>:1014: in _gcd_import
???
<frozen importlib._bootstrap>:991: in _find_and_load
???
<frozen importlib._bootstrap>:975: in _find_and_load_unlocked
???
<frozen importlib._bootstrap>:671: in _load_unlocked
???
/usr/local/lib/python3.8/dist-packages/_pytest/assertion/rewrite.py:170: in exec_module
exec(co, module.__dict__)
/usr/local/lib/python3.8/dist-packages/anyio/_backends/_asyncio.py:897: in <module>
class Process(abc.Process):
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
@dataclass(eq=False)
class Process(abc.Process):
> _process: asyncio.subprocess.Process
E AttributeError: module 'pytest_subprocess.asyncio_subprocess' has no attribute 'Process'
Notably this only occurs if nothing else has caused anyio._backends._asyncio
to be imported yet and can be worked around with a fixture like this (or by otherwise running the equivalent code before the fake_process fixture runs):
@pytest.fixture
def init_anyio():
async def impl():
anyio.get_cancelled_exc_class()
asyncio.get_event_loop().run_until_complete(impl())
FWIW the example test case is not representative of the actual test we ran into it with, but just something that reproduces the problem. In the real test we're testing a FastAPI-based server to see if it invokes processes when it should, and FastAPI is based on anyio.
The sdist package at PyPI is missing tests. Please add missing tests to sdist to make downstream testing easier. Thank you.
If you use from multiprocess import Popen
the library is not able to swap the implementation, I don't know if it's an intended behavior, maybe should be explicitly indicated in the documentation.
Minimal reproducible example: imagine you have the module benchmarking.py
with the following function:
from subprocess import Popen as imported_Popen
def meow(cmdline):
return imported_Popen(cmdline, stdout=subprocess.PIPE)
and then you have a separate file with your test
from benchmarking import meow
def test_echo_null_byte(fp):
fp.register(["echo", "-ne", "\x00"], stdout=bytes.fromhex("00"))
process = meow(["echo", "-ne", "\x00"])
out, _ = process.communicate()
assert process.returncode == 0
assert out == b"\x00"
then the test it's gonna fail because (probably) at the moment the test is run it's too late to swap the implementation.
sphinxcontrib.napoleon
module.--- a/docs/conf.py
+++ b/docs/conf.py
@@ -38,7 +38,7 @@
# ones.
extensions = [
- "sphinxcontrib.napoleon",
+ "sphinx.ext.napoleon",
"sphinx.ext.autodoc",
"sphinx_autodoc_typehints",
]
pytest_subprocess
module+ /usr/bin/sphinx-build -n -T -b man docs build/sphinx/man
Running Sphinx v5.1.1
WARNING:root:Generated changelog file to history.rst
making output directory... done
building [mo]: targets for 0 po files that are out of date
building [man]: all manpages
updating environment: [new config] 4 added, 0 changed, 0 removed
reading sources... [100%] usage
WARNING: autodoc: failed to import class 'core.FakeProcess' from module 'pytest_subprocess'; the following exception was raised:
No module named 'pytest_subprocess'
WARNING: autodoc: failed to import class 'utils.Any' from module 'pytest_subprocess'; the following exception was raised:
No module named 'pytest_subprocess'
looking for now-outdated files... none found
pickling environment... done
checking consistency... done
writing... python-pytest-subprocess.3 { usage api history } done
build succeeded, 2 warnings.
Here is the patch which fixes that
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -8,9 +8,10 @@
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
-# import os
-# import sys
-# sys.path.insert(0, os.path.abspath('.'))
+import os
+import sys
+sys.path.insert(0, os.path.abspath(".."))
+
import datetime
from pathlib import Path
This patch sdoes wjat is descrived in lines above modifird lines.
+ /usr/bin/sphinx-build -n -T -b man docs build/sphinx/man
Running Sphinx v5.1.1
WARNING:root:Generated changelog file to history.rst
making output directory... done
building [mo]: targets for 0 po files that are out of date
building [man]: all manpages
updating environment: [new config] 4 added, 0 changed, 0 removed
reading sources... [100%] usage
WARNING: autodoc: failed to import class 'core.FakeProcess' from module 'pytest_subprocess'; the following exception was raised:
No module named 'pytest_subprocess.core'
looking for now-outdated files... none found
pickling environment... done
checking consistency... done
writing... python-pytest-subprocess.3 { usage api history } /home/tkloczko/rpmbuild/BUILD/pytest-subprocess-1.4.1/pytest_subprocess/utils.py:docstring of pytest_subprocess.utils.Any:4: WARNING: py:data reference target not found: typing.Optional
/home/tkloczko/rpmbuild/BUILD/pytest-subprocess-1.4.1/pytest_subprocess/utils.py:docstring of pytest_subprocess.utils.Any:6: WARNING: py:data reference target not found: typing.Optional
done
build succeeded, 3 warnings.
In case sphinx warnings reference target not found
you can peak on fixes that kind of issues in other projects
latchset/jwcrypto#289
click-contrib/sphinx-click@abc31069
latchset/jwcrypto#289
RDFLib/rdflib-sqlalchemy#95
sissaschool/elementpath@bf869d9e
jaraco/cssutils#21
pywbem/pywbem#2895
sissaschool/xmlschema@42ea98f2
RDFLib/rdflib#2036
frostming/unearth#14
Please let me know if you want above patches as PRs.
Looks like the outputs are assumed to be strings and pytest-subprocess is appending line separators.
def test_binary_output(fake_process):
fake_process.register_subprocess(
["cat", "/tmp/foo"], stdout=bytes.fromhex("000102")
)
proc = subprocess.run(["cat", "/tmp/foo"], capture_output=True)
> assert proc.stdout == b"\x00\x01\x02"
E AssertionError: assert b'\x00\x01\x02\n' == b'\x00\x01\x02'
E Full diff:
E - b'\x00\x01\x02'
E + b'\x00\x01\x02\n'
E ? ++
This should check for text mode: https://github.com/aklajnert/pytest-subprocess/blob/master/pytest_subprocess/core.py#L146
Today I discovered that fake_process.allow_unregistered(True)
in one test case applies to / affects all following test cases.
Here's a minimal reproducible example:
import subprocess
import pytest
import pytest_subprocess
def test_1(fake_process):
with pytest.raises(pytest_subprocess.ProcessNotRegisteredError):
subprocess.run(["ls"])
def test_2(fake_process):
fake_process.allow_unregistered(True)
def test_3(fake_process):
with pytest.raises(pytest_subprocess.ProcessNotRegisteredError):
subprocess.run(["ls"])
Note test_1
and test_3
are exactly the same. Unfortunately when run one after another test_1
passes, while test_3
fails.
Tested on Python 3.8.10, pytest 6.2.4 and pytest_subprocess 1.1.1.
An AssertionError
is raised at fake_popen.py
:188when calling with argument stderr=subprocess.STDOUT
if stdout
is a file (stdout
is expected to be None
:
> assert self.stdout is not None
E AssertionError
pyvenv\Lib\site-packages\pytest_subprocess\fake_popen.py:188: AssertionError
subprocess.Popen
handles this correctly; stderr is simply written to the file at stdout.
Below is an example demonstrating the error. I have GIT_TRACE
set so that git rev-parse
gives a small amount of stdout and stderr:
$ git rev-parse HEAD
06:05:28.502294 exec-cmd.c:244 trace: resolved executable dir: C:/Program Files/Git/mingw64/bin
06:05:28.537291 git.c:463 trace: built-in: git rev-parse HEAD
47c77698dd8e0e35af00da56806fe98fcb9a1056
I define a function that runs the subprocess (so that I can call the same thing, using either the fake or real Popen
:
def run_git_revparse():
import os
os.environ['GIT_TRACE'] = '1'
with NamedTemporaryFile("w+b", delete=False) as f:
path = pathlib.Path(f.name)
# NOTE: I pass f.file because for some reason:
# isinstance(f, io.BufferedWriter) == False
# so pytest_subprocess isinstance checks would fail. I am
# hesitant to change those tests, though you could simply check
# for a few key methods:
# hasattr(f.mode)
# isinstance(f.mode, str)
# hasattr(f.write)
# isinstance(f.write, types.FunctionType)
p = subprocess.Popen(('git', 'rev-parse', 'HEAD'), stdout=f.file, stderr=subprocess.STDOUT)
f.close()
assert p.wait() == 0
assert path.exists()
output = path.read_text()
# print to sys.stdout so I can see with `pytest --capture=no`
print('-'*50)
print(output)
print('-'*50)
output = output.splitlines()
assert len(output) == 3
# account for stderr coming before or after stdout
if 'exec-cmd.c' in output[1]:
output[2], output[0], output[1] = output
# first two lines should be stderr
assert 'exec-cmd.c' in output[0]
assert 'git.c' in output[1]
# last line should be a hex string
assert len(set(output[2]) - set('0123456789abcdef')) == 0
Then define my test function, which first runs the real git
, then the fake "git":
def test_fp(fp):
print() # for --capture=no
fp.allow_unregistered(True)
run_git_revparse()
fp.register(
["git", 'rev-parse', 'HEAD'],
stdout=["47c77698dd8e0e35af00da56806fe98fcb9a1056"],
stderr=[
'05:17:17.982923 exec-cmd.c:244 trace: resolved executable dir: C:/Program Files/Git/mingw64/bin',
'05:17:18.000760 git.c:463 trace: built-in: git rev-parse HEAD'
])
run_git_revparse()
This raises the assertion error. I can correct this:
--- fake_popen.py 2024-02-10 06:09:01.897909800 -0500
+++ fake_popen.py 2024-02-10 06:08:49.610943500 -0500
@@ -181,22 +181,16 @@
stdout = kwargs.get("stdout")
if stdout == subprocess.PIPE:
self.stdout = self._prepare_buffer(self.__stdout)
- elif self.__is_io_writer(stdout):
+ elif isinstance(stdout, (io.BufferedWriter, io.TextIOWrapper)):
self._write_to_buffer(self.__stdout, stdout)
stderr = kwargs.get("stderr")
if stderr == subprocess.STDOUT and self.__stderr:
- if self.__is_io_writer(stdout):
- self._write_to_buffer(self.__stderr, stdout)
- else:
- assert self.stdout is not None
- self.stdout = self._prepare_buffer(self.__stderr, self.stdout)
+ assert self.stdout is not None
+ self.stdout = self._prepare_buffer(self.__stderr, self.stdout)
elif stderr == subprocess.PIPE:
self.stderr = self._prepare_buffer(self.__stderr)
- elif self.__is_io_writer(stderr):
+ elif isinstance(stderr, (io.BufferedWriter, io.TextIOWrapper)):
self._write_to_buffer(self.__stderr, stderr)
-
- def __is_io_writer(self, o):
- return isinstance(o, (io.BufferedWriter, io.TextIOWrapper, io.BufferedRandom))
def _prepare_buffer(
self,
Note:
isinstance
testing.io.BufferedRandom
to the group, as that is what I had, and I would expect it should be included...my test still won't work without it.A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.