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:
commit
b968f63ca5
|
@ -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]]]:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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):
|
||||
|
|
Loading…
Reference in New Issue