Skip to content

pytester: a couple of fixes for pytester.plugins in subprocess mode #13522

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jun 18, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions changelog/13522.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Fixed :fixture:`pytester` in subprocess mode ignored all :attr`pytester.plugins <pytest.Pytester.plugins>` except the first.

Fixed :fixture:`pytester` in subprocess mode silently ignored non-str :attr:`pytester.plugins <pytest.Pytester.plugins>`.
Now it errors instead.
If you are affected by this, specify the plugin by name, or switch the affected tests to use :func:`pytester.runpytest_inprocess <pytest.Pytester.runpytest_inprocess>` explicitly instead.
23 changes: 14 additions & 9 deletions src/_pytest/pytester.py
Original file line number Diff line number Diff line change
Expand Up @@ -682,9 +682,11 @@ def __init__(
self._name = name
self._path: Path = tmp_path_factory.mktemp(name, numbered=True)
#: A list of plugins to use with :py:meth:`parseconfig` and
#: :py:meth:`runpytest`. Initially this is an empty list but plugins can
#: be added to the list. The type of items to add to the list depends on
#: the method using them so refer to them for details.
#: :py:meth:`runpytest`. Initially this is an empty list but plugins can
#: be added to the list.
#:
#: When running in subprocess mode, specify plugins by name (str) - adding
#: plugin objects directly is not supported.
self.plugins: list[str | _PluggyPlugin] = []
self._sys_path_snapshot = SysPathsSnapshot()
self._sys_modules_snapshot = self.__take_sys_modules_snapshot()
Expand Down Expand Up @@ -1229,10 +1231,9 @@ def parseconfig(self, *args: str | os.PathLike[str]) -> Config:
"""
import _pytest.config

new_args = self._ensure_basetemp(args)
new_args = [str(x) for x in new_args]
new_args = [str(x) for x in self._ensure_basetemp(args)]

config = _pytest.config._prepareconfig(new_args, self.plugins) # type: ignore[arg-type]
config = _pytest.config._prepareconfig(new_args, self.plugins)
# we don't know what the test will do with this half-setup config
# object and thus we make sure it gets unconfigured properly in any
# case (otherwise capturing could still be active, for example)
Expand Down Expand Up @@ -1494,9 +1495,13 @@ def runpytest_subprocess(
__tracebackhide__ = True
p = make_numbered_dir(root=self.path, prefix="runpytest-", mode=0o700)
args = (f"--basetemp={p}", *args)
plugins = [x for x in self.plugins if isinstance(x, str)]
if plugins:
args = ("-p", plugins[0], *args)
for plugin in self.plugins:
if not isinstance(plugin, str):
raise ValueError(
f"Specifying plugins as objects is not supported in pytester subprocess mode; "
f"specify by name instead: {plugin}"
)
args = ("-p", plugin, *args)
args = self._getpytestargs() + args
return self.run(*args, timeout=timeout)

Expand Down
22 changes: 22 additions & 0 deletions testing/test_pytester.py
Original file line number Diff line number Diff line change
Expand Up @@ -834,3 +834,25 @@ def test_two():
result.assert_outcomes(passed=1, deselected=1)
# If deselected is not passed, it is not checked at all.
result.assert_outcomes(passed=1)


def test_pytester_subprocess_with_string_plugins(pytester: Pytester) -> None:
"""Test that pytester.runpytest_subprocess is OK with named (string)
`.plugins`."""
pytester.plugins = ["pytester"]

result = pytester.runpytest_subprocess()
assert result.ret == ExitCode.NO_TESTS_COLLECTED


def test_pytester_subprocess_with_non_string_plugins(pytester: Pytester) -> None:
"""Test that pytester.runpytest_subprocess fails with a proper error given
non-string `.plugins`."""

class MyPlugin:
pass

pytester.plugins = [MyPlugin()]

with pytest.raises(ValueError, match="plugins as objects is not supported"):
pytester.runpytest_subprocess()