Merge pull request #11780 from bluetech/register-fixture

Add an internal "register fixture" API and use it replace object patching for fixture injection
This commit is contained in:
Ran Benita 2024-01-08 21:24:54 +02:00 committed by GitHub
commit b968f63ca5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 160 additions and 99 deletions

View File

@ -1621,6 +1621,69 @@ class FixtureManager:
# Separate parametrized setups.
items[:] = reorder_items(items)
def _register_fixture(
self,
*,
name: str,
func: "_FixtureFunc[object]",
nodeid: Optional[str],
scope: Union[
Scope, _ScopeName, Callable[[str, Config], _ScopeName], None
] = "function",
params: Optional[Sequence[object]] = None,
ids: Optional[
Union[Tuple[Optional[object], ...], Callable[[Any], Optional[object]]]
] = None,
autouse: bool = False,
unittest: bool = False,
) -> None:
"""Register a fixture
:param name:
The fixture's name.
:param func:
The fixture's implementation function.
:param nodeid:
The visibility of the fixture. The fixture will be available to the
node with this nodeid and its children in the collection tree.
None means that the fixture is visible to the entire collection tree,
e.g. a fixture defined for general use in a plugin.
: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.
:param unittest:
Set this if this is a unittest fixture.
"""
fixture_def = FixtureDef(
fixturemanager=self,
baseid=nodeid,
argname=name,
func=func,
scope=scope,
params=params,
unittest=unittest,
ids=ids,
_ispytest=True,
)
faclist = self._arg2fixturedefs.setdefault(name, [])
if fixture_def.has_location:
faclist.append(fixture_def)
else:
# fixturedefs with no location are at the front
# so this inserts the current fixturedef after the
# existing fixturedefs from external plugins but
# before the fixturedefs provided in conftests.
i = len([f for f in faclist if not f.has_location])
faclist.insert(i, fixture_def)
if autouse:
self._nodeid_autousenames.setdefault(nodeid or "", []).append(name)
@overload
def parsefactories(
self,
@ -1672,7 +1735,6 @@ class FixtureManager:
return
self._holderobjseen.add(holderobj)
autousenames = []
for name in dir(holderobj):
# The attribute can be an arbitrary descriptor, so the attribute
# access below can raise. safe_getatt() ignores such exceptions.
@ -1690,36 +1752,19 @@ class FixtureManager:
# to issue a warning if called directly, so here we unwrap it in
# order to not emit the warning when pytest itself calls the
# fixture function.
obj = get_real_method(obj, holderobj)
func = get_real_method(obj, holderobj)
fixture_def = FixtureDef(
fixturemanager=self,
baseid=nodeid,
argname=name,
func=obj,
self._register_fixture(
name=name,
nodeid=nodeid,
func=func,
scope=marker.scope,
params=marker.params,
unittest=unittest,
ids=marker.ids,
_ispytest=True,
autouse=marker.autouse,
)
faclist = self._arg2fixturedefs.setdefault(name, [])
if fixture_def.has_location:
faclist.append(fixture_def)
else:
# fixturedefs with no location are at the front
# so this inserts the current fixturedef after the
# existing fixturedefs from external plugins but
# before the fixturedefs provided in conftests.
i = len([f for f in faclist if not f.has_location])
faclist.insert(i, fixture_def)
if marker.autouse:
autousenames.append(name)
if autousenames:
self._nodeid_autousenames.setdefault(nodeid or "", []).extend(autousenames)
def getfixturedefs(
self, argname: str, nodeid: str
) -> Optional[Sequence[FixtureDef[Any]]]:

View File

@ -582,13 +582,13 @@ class Module(nodes.File, PyCollector):
return importtestmodule(self.path, self.config)
def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]:
self._inject_setup_module_fixture()
self._inject_setup_function_fixture()
self._register_setup_module_fixture()
self._register_setup_function_fixture()
self.session._fixturemanager.parsefactories(self)
return super().collect()
def _inject_setup_module_fixture(self) -> None:
"""Inject a hidden autouse, module scoped fixture into the collected module object
def _register_setup_module_fixture(self) -> None:
"""Register an autouse, module-scoped fixture for the collected module object
that invokes setUpModule/tearDownModule if either or both are available.
Using a fixture to invoke this methods ensures we play nicely and unsurprisingly with
@ -604,23 +604,25 @@ class Module(nodes.File, PyCollector):
if setup_module is None and teardown_module is None:
return
@fixtures.fixture(
autouse=True,
scope="module",
# Use a unique name to speed up lookup.
name=f"_xunit_setup_module_fixture_{self.obj.__name__}",
)
def xunit_setup_module_fixture(request) -> Generator[None, None, None]:
module = request.module
if setup_module is not None:
_call_with_optional_argument(setup_module, request.module)
_call_with_optional_argument(setup_module, module)
yield
if teardown_module is not None:
_call_with_optional_argument(teardown_module, request.module)
_call_with_optional_argument(teardown_module, module)
self.obj.__pytest_setup_module = xunit_setup_module_fixture
self.session._fixturemanager._register_fixture(
# Use a unique name to speed up lookup.
name=f"_xunit_setup_module_fixture_{self.obj.__name__}",
func=xunit_setup_module_fixture,
nodeid=self.nodeid,
scope="module",
autouse=True,
)
def _inject_setup_function_fixture(self) -> None:
"""Inject a hidden autouse, function scoped fixture into the collected module object
def _register_setup_function_fixture(self) -> None:
"""Register an autouse, function-scoped fixture for the collected module object
that invokes setup_function/teardown_function if either or both are available.
Using a fixture to invoke this methods ensures we play nicely and unsurprisingly with
@ -633,25 +635,27 @@ class Module(nodes.File, PyCollector):
if setup_function is None and teardown_function is None:
return
@fixtures.fixture(
autouse=True,
scope="function",
# Use a unique name to speed up lookup.
name=f"_xunit_setup_function_fixture_{self.obj.__name__}",
)
def xunit_setup_function_fixture(request) -> Generator[None, None, None]:
if request.instance is not None:
# in this case we are bound to an instance, so we need to let
# setup_method handle this
yield
return
function = request.function
if setup_function is not None:
_call_with_optional_argument(setup_function, request.function)
_call_with_optional_argument(setup_function, function)
yield
if teardown_function is not None:
_call_with_optional_argument(teardown_function, request.function)
_call_with_optional_argument(teardown_function, function)
self.obj.__pytest_setup_function = xunit_setup_function_fixture
self.session._fixturemanager._register_fixture(
# Use a unique name to speed up lookup.
name=f"_xunit_setup_function_fixture_{self.obj.__name__}",
func=xunit_setup_function_fixture,
nodeid=self.nodeid,
scope="function",
autouse=True,
)
class Package(nodes.Directory):
@ -795,15 +799,15 @@ class Class(PyCollector):
)
return []
self._inject_setup_class_fixture()
self._inject_setup_method_fixture()
self._register_setup_class_fixture()
self._register_setup_method_fixture()
self.session._fixturemanager.parsefactories(self.newinstance(), self.nodeid)
return super().collect()
def _inject_setup_class_fixture(self) -> None:
"""Inject a hidden autouse, class scoped fixture into the collected class object
def _register_setup_class_fixture(self) -> None:
"""Register an autouse, class scoped fixture into the collected class object
that invokes setup_class/teardown_class if either or both are available.
Using a fixture to invoke this methods ensures we play nicely and unsurprisingly with
@ -814,25 +818,27 @@ class Class(PyCollector):
if setup_class is None and teardown_class is None:
return
@fixtures.fixture(
autouse=True,
scope="class",
# Use a unique name to speed up lookup.
name=f"_xunit_setup_class_fixture_{self.obj.__qualname__}",
)
def xunit_setup_class_fixture(cls) -> Generator[None, None, None]:
def xunit_setup_class_fixture(request) -> Generator[None, None, None]:
cls = request.cls
if setup_class is not None:
func = getimfunc(setup_class)
_call_with_optional_argument(func, self.obj)
_call_with_optional_argument(func, cls)
yield
if teardown_class is not None:
func = getimfunc(teardown_class)
_call_with_optional_argument(func, self.obj)
_call_with_optional_argument(func, cls)
self.obj.__pytest_setup_class = xunit_setup_class_fixture
self.session._fixturemanager._register_fixture(
# Use a unique name to speed up lookup.
name=f"_xunit_setup_class_fixture_{self.obj.__qualname__}",
func=xunit_setup_class_fixture,
nodeid=self.nodeid,
scope="class",
autouse=True,
)
def _inject_setup_method_fixture(self) -> None:
"""Inject a hidden autouse, function scoped fixture into the collected class object
def _register_setup_method_fixture(self) -> None:
"""Register an autouse, function scoped fixture into the collected class object
that invokes setup_method/teardown_method if either or both are available.
Using a fixture to invoke these methods ensures we play nicely and unsurprisingly with
@ -845,23 +851,25 @@ class Class(PyCollector):
if setup_method is None and teardown_method is None:
return
@fixtures.fixture(
autouse=True,
scope="function",
# Use a unique name to speed up lookup.
name=f"_xunit_setup_method_fixture_{self.obj.__qualname__}",
)
def xunit_setup_method_fixture(self, request) -> Generator[None, None, None]:
def xunit_setup_method_fixture(request) -> Generator[None, None, None]:
instance = request.instance
method = request.function
if setup_method is not None:
func = getattr(self, setup_name)
func = getattr(instance, setup_name)
_call_with_optional_argument(func, method)
yield
if teardown_method is not None:
func = getattr(self, teardown_name)
func = getattr(instance, teardown_name)
_call_with_optional_argument(func, method)
self.obj.__pytest_setup_method = xunit_setup_method_fixture
self.session._fixturemanager._register_fixture(
# Use a unique name to speed up lookup.
name=f"_xunit_setup_method_fixture_{self.obj.__qualname__}",
func=xunit_setup_method_fixture,
nodeid=self.nodeid,
scope="function",
autouse=True,
)
def hasinit(obj: object) -> bool:

View File

@ -70,9 +70,9 @@ class UnitTestCase(Class):
skipped = _is_skipped(cls)
if not skipped:
self._inject_unittest_setup_method_fixture(cls)
self._inject_unittest_setup_class_fixture(cls)
self._inject_setup_class_fixture()
self._register_unittest_setup_method_fixture(cls)
self._register_unittest_setup_class_fixture(cls)
self._register_setup_class_fixture()
self.session._fixturemanager.parsefactories(self, unittest=True)
loader = TestLoader()
@ -93,8 +93,8 @@ class UnitTestCase(Class):
if ut is None or runtest != ut.TestCase.runTest: # type: ignore
yield TestCaseFunction.from_parent(self, name="runTest")
def _inject_unittest_setup_class_fixture(self, cls: type) -> None:
"""Injects a hidden auto-use fixture to invoke setUpClass and
def _register_unittest_setup_class_fixture(self, cls: type) -> None:
"""Register an auto-use fixture to invoke setUpClass and
tearDownClass (#517)."""
setup = getattr(cls, "setUpClass", None)
teardown = getattr(cls, "tearDownClass", None)
@ -102,15 +102,12 @@ class UnitTestCase(Class):
return None
cleanup = getattr(cls, "doClassCleanups", lambda: None)
@pytest.fixture(
scope="class",
autouse=True,
# Use a unique name to speed up lookup.
name=f"_unittest_setUpClass_fixture_{cls.__qualname__}",
)
def fixture(self) -> Generator[None, None, None]:
if _is_skipped(self):
reason = self.__unittest_skip_why__
def unittest_setup_class_fixture(
request: FixtureRequest,
) -> Generator[None, None, None]:
cls = request.cls
if _is_skipped(cls):
reason = cls.__unittest_skip_why__
raise pytest.skip.Exception(reason, _use_item_location=True)
if setup is not None:
try:
@ -127,23 +124,27 @@ class UnitTestCase(Class):
finally:
cleanup()
cls.__pytest_class_setup = fixture # type: ignore[attr-defined]
self.session._fixturemanager._register_fixture(
# Use a unique name to speed up lookup.
name=f"_unittest_setUpClass_fixture_{cls.__qualname__}",
func=unittest_setup_class_fixture,
nodeid=self.nodeid,
scope="class",
autouse=True,
)
def _inject_unittest_setup_method_fixture(self, cls: type) -> None:
"""Injects a hidden auto-use fixture to invoke setup_method and
def _register_unittest_setup_method_fixture(self, cls: type) -> None:
"""Register an auto-use fixture to invoke setup_method and
teardown_method (#517)."""
setup = getattr(cls, "setup_method", None)
teardown = getattr(cls, "teardown_method", None)
if setup is None and teardown is None:
return None
@pytest.fixture(
scope="function",
autouse=True,
# Use a unique name to speed up lookup.
name=f"_unittest_setup_method_fixture_{cls.__qualname__}",
)
def fixture(self, request: FixtureRequest) -> Generator[None, None, None]:
def unittest_setup_method_fixture(
request: FixtureRequest,
) -> Generator[None, None, None]:
self = request.instance
if _is_skipped(self):
reason = self.__unittest_skip_why__
raise pytest.skip.Exception(reason, _use_item_location=True)
@ -153,7 +154,14 @@ class UnitTestCase(Class):
if teardown is not None:
teardown(self, request.function)
cls.__pytest_method_setup = fixture # type: ignore[attr-defined]
self.session._fixturemanager._register_fixture(
# Use a unique name to speed up lookup.
name=f"_unittest_setup_method_fixture_{cls.__qualname__}",
func=unittest_setup_method_fixture,
nodeid=self.nodeid,
scope="function",
autouse=True,
)
class TestCaseFunction(Function):