From c3084d39b9657fba904baa5374c534d358aa67e2 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Mon, 16 Jun 2025 10:29:45 +0300 Subject: [PATCH 1/3] pytester: fix subprocess mode ignores all `pytester.plugins` except the first As far as I can see, there is no reason for this, seems like a mistake. --- changelog/13522.bugfix.rst | 1 + src/_pytest/pytester.py | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) create mode 100644 changelog/13522.bugfix.rst diff --git a/changelog/13522.bugfix.rst b/changelog/13522.bugfix.rst new file mode 100644 index 00000000000..2c785485e1a --- /dev/null +++ b/changelog/13522.bugfix.rst @@ -0,0 +1 @@ +Fixed :fixture:`pytester` in subprocess mode ignored all :attr`pytester.plugins ` except the first. diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 59d2b0befe9..360f640474c 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -1494,9 +1494,9 @@ 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 isinstance(plugin, str): + args = ("-p", plugin, *args) args = self._getpytestargs() + args return self.run(*args, timeout=timeout) From 76d62a34c7e26f7deda8820c53bfea449f64e5a7 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Mon, 16 Jun 2025 10:40:46 +0300 Subject: [PATCH 2/3] pytester: avoid a type: ignore --- src/_pytest/pytester.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 360f640474c..587af5ad6c6 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -1229,10 +1229,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) From d32a34533e03c4f8674de880129b0a565f373d08 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Mon, 16 Jun 2025 10:41:04 +0300 Subject: [PATCH 3/3] pytester: error on non-str `pytester.plugins` in subprocess mode instead of silently ignoring In subprocess mode, adding a non-str plugin object to `pytester.plugins` can't work. Previously, such plugins would just be silently ignored. Silently ignoring an explicit setup doesn't seem right. Error instead. --- changelog/13522.bugfix.rst | 4 ++++ src/_pytest/pytester.py | 16 +++++++++++----- testing/test_pytester.py | 22 ++++++++++++++++++++++ 3 files changed, 37 insertions(+), 5 deletions(-) diff --git a/changelog/13522.bugfix.rst b/changelog/13522.bugfix.rst index 2c785485e1a..683304251aa 100644 --- a/changelog/13522.bugfix.rst +++ b/changelog/13522.bugfix.rst @@ -1 +1,5 @@ Fixed :fixture:`pytester` in subprocess mode ignored all :attr`pytester.plugins ` except the first. + +Fixed :fixture:`pytester` in subprocess mode silently ignored non-str :attr:`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 ` explicitly instead. diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 587af5ad6c6..de4e2c8b136 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -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() @@ -1494,8 +1496,12 @@ def runpytest_subprocess( p = make_numbered_dir(root=self.path, prefix="runpytest-", mode=0o700) args = (f"--basetemp={p}", *args) for plugin in self.plugins: - if isinstance(plugin, str): - args = ("-p", plugin, *args) + 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) diff --git a/testing/test_pytester.py b/testing/test_pytester.py index 721e8c19d8b..a5ac8a91b8d 100644 --- a/testing/test_pytester.py +++ b/testing/test_pytester.py @@ -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()