From 18721e1d28c641553f42a48afee09322c1a2e67f Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 6 Jun 2026 21:55:09 +0300 Subject: [PATCH] Add `pytest.register_fixture` Fix #12376. --- changelog/12376.feature.rst | 7 +++++ doc/en/deprecations.rst | 2 +- src/_pytest/fixtures.py | 59 +++++++++++++++++++++++++++++++++++++ src/_pytest/python.py | 8 ++--- src/_pytest/unittest.py | 7 +++-- src/pytest/__init__.py | 2 ++ testing/python/fixtures.py | 9 +++--- 7 files changed, 81 insertions(+), 13 deletions(-) create mode 100644 changelog/12376.feature.rst diff --git a/changelog/12376.feature.rst b/changelog/12376.feature.rst new file mode 100644 index 00000000000..49551b5b2a4 --- /dev/null +++ b/changelog/12376.feature.rst @@ -0,0 +1,7 @@ +Added :func:`pytest.register_fixture()` to register fixtures using an imperative interface. + +This is an advanced function intended for use by plugins. + +Normally, fixtures should be registered declaratively using the :func:`@pytest.fixture ` decorator. +Pytest looks for these fixture definitions during the collection phase and registers them automatically. +For some plugin usecases the declarative interface can be cumbersome or unviable, in which case this imperative interface can be used. diff --git a/doc/en/deprecations.rst b/doc/en/deprecations.rst index 7cc58dbd57b..280b0d94425 100644 --- a/doc/en/deprecations.rst +++ b/doc/en/deprecations.rst @@ -37,7 +37,7 @@ node-based matching instead of fragile string prefix matching. # Use instead fixture_manager.parsefactories(holder=plugin_obj, node=directory_node) - fixture_manager._register_fixture(name="fix", func=func, node=directory_node) + pytest.register_fixture(name="fix", func=func, node=directory_node) The equivalent of passing ``nodeid=None`` (global visibility) is ``node=session``. diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 5209c0c1ff9..9359c07e223 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -2322,3 +2322,62 @@ def _showfixtures_main(config: Config, session: Session) -> None: def write_docstring(tw: TerminalWriter, doc: str, indent: str = " ") -> None: for line in doc.split("\n"): tw.line(indent + line) + + +def register_fixture( + *, + name: str, + func: _FixtureFunc[object], + node: nodes.Node, + scope: Scope | ScopeName | Callable[[str, Config], ScopeName] = "function", + params: Sequence[object] | None = None, + ids: tuple[object | None, ...] | Callable[[Any], object | None] | None = None, + autouse: bool = False, +) -> None: + """Register a fixture imperatively. + + This is an advanced function intended for use by plugins. + + Normally, fixtures should be registered declaratively using the + :func:`@pytest.fixture ` decorator. Pytest looks for these + fixture definitions during the collection phase and registers them + automatically. For some plugin usecases the declarative interface can be + cumbersome or unviable, in which case the imperative interface can be used. + + Fixture registration is expected to happen during the collection phase, and + this is the only sanctioned use. However, to allow for more creative uses, + this is not enforced. But do so at your own risk! + + .. versionadded: 9.1 + + :param name: + The fixture's name. + :param func: + The fixture's implementation function. + :param node: + The visibility of the fixture. + + Only items that are descendents of this node in the collection tree will + be able to request this fixture. You can think of this as the place + where you would put the `@pytest.fixture`. + + For global visibility, pass the :class:`session ` node, + which is the root of the collection tree. + :param scope: + The fixture's scope. + :param params: + The fixture's parametrization params. + :param ids: + The fixture's IDs. + :param autouse: + Whether this is an autouse fixture. + """ + node.session._fixturemanager._register_fixture( + name=name, + func=func, + node=node, + scope=scope, + params=params, + ids=ids, + autouse=autouse, + ) diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 6e98a7e987c..45ff185184b 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -591,7 +591,7 @@ def xunit_setup_module_fixture(request) -> Generator[None]: if teardown_module is not None: _call_with_optional_argument(teardown_module, module) - self.session._fixturemanager._register_fixture( + fixtures.register_fixture( # Use a unique name to speed up lookup. name=f"_xunit_setup_module_fixture_{self.obj.__name__}", func=xunit_setup_module_fixture, @@ -627,7 +627,7 @@ def xunit_setup_function_fixture(request) -> Generator[None]: if teardown_function is not None: _call_with_optional_argument(teardown_function, function) - self.session._fixturemanager._register_fixture( + fixtures.register_fixture( # Use a unique name to speed up lookup. name=f"_xunit_setup_function_fixture_{self.obj.__name__}", func=xunit_setup_function_fixture, @@ -807,7 +807,7 @@ def xunit_setup_class_fixture(request) -> Generator[None]: func = getimfunc(teardown_class) _call_with_optional_argument(func, cls) - self.session._fixturemanager._register_fixture( + fixtures.register_fixture( # Use a unique name to speed up lookup. name=f"_xunit_setup_class_fixture_{self.obj.__qualname__}", func=xunit_setup_class_fixture, @@ -841,7 +841,7 @@ def xunit_setup_method_fixture(request) -> Generator[None]: func = getattr(instance, teardown_name) _call_with_optional_argument(func, method) - self.session._fixturemanager._register_fixture( + fixtures.register_fixture( # Use a unique name to speed up lookup. name=f"_xunit_setup_method_fixture_{self.obj.__qualname__}", func=xunit_setup_method_fixture, diff --git a/src/_pytest/unittest.py b/src/_pytest/unittest.py index 84435b3e09f..d5286af1470 100644 --- a/src/_pytest/unittest.py +++ b/src/_pytest/unittest.py @@ -17,6 +17,7 @@ from typing import TYPE_CHECKING from unittest import TestCase +from _pytest import fixtures import _pytest._code from _pytest._code import ExceptionInfo from _pytest.compat import assert_never @@ -170,7 +171,7 @@ def unittest_setup_class_fixture( cleanup() process_teardown_exceptions() - self.session._fixturemanager._register_fixture( + fixtures.register_fixture( # Use a unique name to speed up lookup. name=f"_unittest_setUpClass_fixture_{cls.__qualname__}", func=unittest_setup_class_fixture, @@ -187,7 +188,7 @@ def unittest_skip_fixture(request: FixtureRequest) -> None: reason = getattr(cls, "__unittest_skip_why__", "") raise skip.Exception(reason, _use_item_location=True) - self.session._fixturemanager._register_fixture( + fixtures.register_fixture( name=f"_unittest_skip_fixture_{cls.__qualname__}", func=unittest_skip_fixture, node=self, @@ -216,7 +217,7 @@ def unittest_setup_method_fixture( if teardown is not None: teardown(self, request.function) - self.session._fixturemanager._register_fixture( + fixtures.register_fixture( # Use a unique name to speed up lookup. name=f"_unittest_setup_method_fixture_{cls.__qualname__}", func=unittest_setup_method_fixture, diff --git a/src/pytest/__init__.py b/src/pytest/__init__.py index a74136f77b5..7bce1d55ae3 100644 --- a/src/pytest/__init__.py +++ b/src/pytest/__init__.py @@ -26,6 +26,7 @@ from _pytest.fixtures import FixtureDef from _pytest.fixtures import FixtureLookupError from _pytest.fixtures import FixtureRequest +from _pytest.fixtures import register_fixture from _pytest.fixtures import yield_fixture # type: ignore[deprecated] from _pytest.freeze_support import freeze_includes from _pytest.legacypath import TempdirFactory @@ -177,6 +178,7 @@ "param", "raises", "register_assert_rewrite", + "register_fixture", "set_trace", "skip", "version_tuple", diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index 9ba372b111f..dc9474c6fd0 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -1914,12 +1914,11 @@ def test_register_fixture_ordered_by_visibility(self, pytester: Pytester) -> Non @pytest.hookimpl(wrapper=True) def pytest_collection(session): result = yield - fm = session._fixturemanager item = session.items[0] - fm._register_fixture(name="fix", func=lambda: "session1", node=session) - fm._register_fixture(name="fix", func=lambda fix: f"item1-{fix}", node=item) - fm._register_fixture(name="fix", func=lambda fix: f"item2-{fix}", node=item) - fm._register_fixture(name="fix", func=lambda: "session2", node=session) + pytest.register_fixture(name="fix", func=lambda: "session1", node=session) + pytest.register_fixture(name="fix", func=lambda fix: f"item1-{fix}", node=item) + pytest.register_fixture(name="fix", func=lambda fix: f"item2-{fix}", node=item) + pytest.register_fixture(name="fix", func=lambda: "session2", node=session) return result """ )