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. # Separate parametrized setups.
items[:] = reorder_items(items) 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 @overload
def parsefactories( def parsefactories(
self, self,
@ -1672,7 +1735,6 @@ class FixtureManager:
return return
self._holderobjseen.add(holderobj) self._holderobjseen.add(holderobj)
autousenames = []
for name in dir(holderobj): for name in dir(holderobj):
# The attribute can be an arbitrary descriptor, so the attribute # The attribute can be an arbitrary descriptor, so the attribute
# access below can raise. safe_getatt() ignores such exceptions. # 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 # to issue a warning if called directly, so here we unwrap it in
# order to not emit the warning when pytest itself calls the # order to not emit the warning when pytest itself calls the
# fixture function. # fixture function.
obj = get_real_method(obj, holderobj) func = get_real_method(obj, holderobj)
fixture_def = FixtureDef( self._register_fixture(
fixturemanager=self, name=name,
baseid=nodeid, nodeid=nodeid,
argname=name, func=func,
func=obj,
scope=marker.scope, scope=marker.scope,
params=marker.params, params=marker.params,
unittest=unittest, unittest=unittest,
ids=marker.ids, 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( def getfixturedefs(
self, argname: str, nodeid: str self, argname: str, nodeid: str
) -> Optional[Sequence[FixtureDef[Any]]]: ) -> Optional[Sequence[FixtureDef[Any]]]:

View File

@ -582,13 +582,13 @@ class Module(nodes.File, PyCollector):
return importtestmodule(self.path, self.config) return importtestmodule(self.path, self.config)
def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]: def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]:
self._inject_setup_module_fixture() self._register_setup_module_fixture()
self._inject_setup_function_fixture() self._register_setup_function_fixture()
self.session._fixturemanager.parsefactories(self) self.session._fixturemanager.parsefactories(self)
return super().collect() return super().collect()
def _inject_setup_module_fixture(self) -> None: def _register_setup_module_fixture(self) -> None:
"""Inject a hidden autouse, module scoped fixture into the collected module object """Register an autouse, module-scoped fixture for the collected module object
that invokes setUpModule/tearDownModule if either or both are available. that invokes setUpModule/tearDownModule if either or both are available.
Using a fixture to invoke this methods ensures we play nicely and unsurprisingly with 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: if setup_module is None and teardown_module is None:
return 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]: def xunit_setup_module_fixture(request) -> Generator[None, None, None]:
module = request.module
if setup_module is not None: if setup_module is not None:
_call_with_optional_argument(setup_module, request.module) _call_with_optional_argument(setup_module, module)
yield yield
if teardown_module is not None: 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: def _register_setup_function_fixture(self) -> None:
"""Inject a hidden autouse, function scoped fixture into the collected module object """Register an autouse, function-scoped fixture for the collected module object
that invokes setup_function/teardown_function if either or both are available. 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 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: if setup_function is None and teardown_function is None:
return 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]: def xunit_setup_function_fixture(request) -> Generator[None, None, None]:
if request.instance is not None: if request.instance is not None:
# in this case we are bound to an instance, so we need to let # in this case we are bound to an instance, so we need to let
# setup_method handle this # setup_method handle this
yield yield
return return
function = request.function
if setup_function is not None: if setup_function is not None:
_call_with_optional_argument(setup_function, request.function) _call_with_optional_argument(setup_function, function)
yield yield
if teardown_function is not None: 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): class Package(nodes.Directory):
@ -795,15 +799,15 @@ class Class(PyCollector):
) )
return [] return []
self._inject_setup_class_fixture() self._register_setup_class_fixture()
self._inject_setup_method_fixture() self._register_setup_method_fixture()
self.session._fixturemanager.parsefactories(self.newinstance(), self.nodeid) self.session._fixturemanager.parsefactories(self.newinstance(), self.nodeid)
return super().collect() return super().collect()
def _inject_setup_class_fixture(self) -> None: def _register_setup_class_fixture(self) -> None:
"""Inject a hidden autouse, class scoped fixture into the collected class object """Register an autouse, class scoped fixture into the collected class object
that invokes setup_class/teardown_class if either or both are available. 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 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: if setup_class is None and teardown_class is None:
return return
@fixtures.fixture( def xunit_setup_class_fixture(request) -> Generator[None, None, None]:
autouse=True, cls = request.cls
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]:
if setup_class is not None: if setup_class is not None:
func = getimfunc(setup_class) func = getimfunc(setup_class)
_call_with_optional_argument(func, self.obj) _call_with_optional_argument(func, cls)
yield yield
if teardown_class is not None: if teardown_class is not None:
func = getimfunc(teardown_class) 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: def _register_setup_method_fixture(self) -> None:
"""Inject a hidden autouse, function scoped fixture into the collected class object """Register an autouse, function scoped fixture into the collected class object
that invokes setup_method/teardown_method if either or both are available. 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 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: if setup_method is None and teardown_method is None:
return return
@fixtures.fixture( def xunit_setup_method_fixture(request) -> Generator[None, None, None]:
autouse=True, instance = request.instance
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]:
method = request.function method = request.function
if setup_method is not None: if setup_method is not None:
func = getattr(self, setup_name) func = getattr(instance, setup_name)
_call_with_optional_argument(func, method) _call_with_optional_argument(func, method)
yield yield
if teardown_method is not None: if teardown_method is not None:
func = getattr(self, teardown_name) func = getattr(instance, teardown_name)
_call_with_optional_argument(func, method) _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: def hasinit(obj: object) -> bool:

View File

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