Skip to content
Open
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
7 changes: 7 additions & 0 deletions changelog/12376.feature.rst
Original file line number Diff line number Diff line change
@@ -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 <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.
2 changes: 1 addition & 1 deletion doc/en/deprecations.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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``.

Expand Down
59 changes: 59 additions & 0 deletions src/_pytest/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 <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 <pytest.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,
)
8 changes: 4 additions & 4 deletions src/_pytest/python.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
7 changes: 4 additions & 3 deletions src/_pytest/unittest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions src/pytest/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -177,6 +178,7 @@
"param",
"raises",
"register_assert_rewrite",
"register_fixture",
"set_trace",
"skip",
"version_tuple",
Expand Down
9 changes: 4 additions & 5 deletions testing/python/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
"""
)
Expand Down