Skip to content

Commit

Permalink
python,unittest: replace obj fixture patching with `FixtureManager._r…
Browse files Browse the repository at this point in the history
…egister_fixture`

Instead of modifying user objects like modules and classes that we
really shouldn't be touching, use the new `_register_fixture` internal
API to do it directly.
  • Loading branch information
bluetech committed Jan 8, 2024
1 parent 3234c79 commit c8792bd
Show file tree
Hide file tree
Showing 2 changed files with 91 additions and 75 deletions.
108 changes: 58 additions & 50 deletions src/_pytest/python.py
Original file line number Diff line number Diff line change
Expand Up @@ -582,13 +582,13 @@ def _getobj(self):
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
Expand All @@ -604,23 +604,25 @@ def _inject_setup_module_fixture(self) -> None:
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
Expand All @@ -633,25 +635,27 @@ def _inject_setup_function_fixture(self) -> None:
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):
Expand Down Expand Up @@ -795,15 +799,15 @@ def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]:
)
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
Expand All @@ -814,25 +818,27 @@ def _inject_setup_class_fixture(self) -> None:
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
Expand All @@ -845,23 +851,25 @@ def _inject_setup_method_fixture(self) -> None:
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:
Expand Down
58 changes: 33 additions & 25 deletions src/_pytest/unittest.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,9 @@ def collect(self) -> Iterable[Union[Item, Collector]]:

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()
Expand All @@ -93,24 +93,21 @@ def collect(self) -> Iterable[Union[Item, Collector]]:
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)
if setup is None and teardown is None:
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__

Check warning on line 110 in src/_pytest/unittest.py

View check run for this annotation

Codecov / codecov/patch

src/_pytest/unittest.py#L110

Added line #L110 was not covered by tests
raise pytest.skip.Exception(reason, _use_item_location=True)
if setup is not None:
try:
Expand All @@ -127,23 +124,27 @@ def fixture(self) -> Generator[None, None, None]:
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)
Expand All @@ -153,7 +154,14 @@ def fixture(self, request: FixtureRequest) -> Generator[None, None, None]:
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):
Expand Down

0 comments on commit c8792bd

Please sign in to comment.