Merge branch 'main' into Improvement-catch-duplicate-values-when-determining-param-indices-in-metafunc-parametrize

This commit is contained in:
Sadra Barikbin 2024-01-09 22:53:08 +03:30 committed by GitHub
commit 909f695b9f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 237 additions and 257 deletions

View File

@ -29,7 +29,7 @@ repos:
language: python language: python
files: \.py$ files: \.py$
- repo: https://github.com/PyCQA/flake8 - repo: https://github.com/PyCQA/flake8
rev: 6.1.0 rev: 7.0.0
hooks: hooks:
- id: flake8 - id: flake8
language_version: python3 language_version: python3

View File

@ -0,0 +1,7 @@
Some changes were made to private functions which may affect plugins which access them:
- ``FixtureManager._getautousenames()`` now takes a ``Node`` itself instead of the nodeid.
- ``FixtureManager.getfixturedefs()`` now takes the ``Node`` itself instead of the nodeid.
- The ``_pytest.nodes.iterparentnodeids()`` function is removed without replacement.
Prefer to traverse the node hierarchy itself instead.
If you really need to, copy the function from the previous pytest release.

View File

@ -424,9 +424,9 @@ class FixtureRequest(abc.ABC):
# We arrive here because of a dynamic call to # We arrive here because of a dynamic call to
# getfixturevalue(argname) usage which was naturally # getfixturevalue(argname) usage which was naturally
# not known at parsing/collection time. # not known at parsing/collection time.
assert self._pyfuncitem.parent is not None parent = self._pyfuncitem.parent
parentid = self._pyfuncitem.parent.nodeid assert parent is not None
fixturedefs = self._fixturemanager.getfixturedefs(argname, parentid) fixturedefs = self._fixturemanager.getfixturedefs(argname, parent)
if fixturedefs is not None: if fixturedefs is not None:
self._arg2fixturedefs[argname] = fixturedefs self._arg2fixturedefs[argname] = fixturedefs
# No fixtures defined with this name. # No fixtures defined with this name.
@ -846,9 +846,8 @@ class FixtureLookupError(LookupError):
available = set() available = set()
parent = self.request._pyfuncitem.parent parent = self.request._pyfuncitem.parent
assert parent is not None assert parent is not None
parentid = parent.nodeid
for name, fixturedefs in fm._arg2fixturedefs.items(): for name, fixturedefs in fm._arg2fixturedefs.items():
faclist = list(fm._matchfactories(fixturedefs, parentid)) faclist = list(fm._matchfactories(fixturedefs, parent))
if faclist: if faclist:
available.add(name) available.add(name)
if self.argname in available: if self.argname in available:
@ -989,9 +988,8 @@ class FixtureDef(Generic[FixtureValue]):
# The "base" node ID for the fixture. # The "base" node ID for the fixture.
# #
# This is a node ID prefix. A fixture is only available to a node (e.g. # This is a node ID prefix. A fixture is only available to a node (e.g.
# a `Function` item) if the fixture's baseid is a parent of the node's # a `Function` item) if the fixture's baseid is a nodeid of a parent of
# nodeid (see the `iterparentnodeids` function for what constitutes a # node.
# "parent" and a "prefix" in this context).
# #
# For a fixture found in a Collector's object (e.g. a `Module`s module, # For a fixture found in a Collector's object (e.g. a `Module`s module,
# a `Class`'s class), the baseid is the Collector's nodeid. # a `Class`'s class), the baseid is the Collector's nodeid.
@ -1482,7 +1480,7 @@ class FixtureManager:
else: else:
argnames = () argnames = ()
usefixturesnames = self._getusefixturesnames(node) usefixturesnames = self._getusefixturesnames(node)
autousenames = self._getautousenames(node.nodeid) autousenames = self._getautousenames(node)
initialnames = deduplicate_names(autousenames, usefixturesnames, argnames) initialnames = deduplicate_names(autousenames, usefixturesnames, argnames)
direct_parametrize_args = _get_direct_parametrize_args(node) direct_parametrize_args = _get_direct_parametrize_args(node)
@ -1517,10 +1515,10 @@ class FixtureManager:
self.parsefactories(plugin, nodeid) self.parsefactories(plugin, nodeid)
def _getautousenames(self, nodeid: str) -> Iterator[str]: def _getautousenames(self, node: nodes.Node) -> Iterator[str]:
"""Return the names of autouse fixtures applicable to nodeid.""" """Return the names of autouse fixtures applicable to node."""
for parentnodeid in nodes.iterparentnodeids(nodeid): for parentnode in reversed(list(nodes.iterparentnodes(node))):
basenames = self._nodeid_autousenames.get(parentnodeid) basenames = self._nodeid_autousenames.get(parentnode.nodeid)
if basenames: if basenames:
yield from basenames yield from basenames
@ -1542,7 +1540,6 @@ class FixtureManager:
# to re-discover fixturedefs again for each fixturename # to re-discover fixturedefs again for each fixturename
# (discovering matching fixtures for a given name/node is expensive). # (discovering matching fixtures for a given name/node is expensive).
parentid = parentnode.nodeid
fixturenames_closure = list(initialnames) fixturenames_closure = list(initialnames)
arg2fixturedefs: Dict[str, Sequence[FixtureDef[Any]]] = {} arg2fixturedefs: Dict[str, Sequence[FixtureDef[Any]]] = {}
@ -1554,7 +1551,7 @@ class FixtureManager:
continue continue
if argname in arg2fixturedefs: if argname in arg2fixturedefs:
continue continue
fixturedefs = self.getfixturedefs(argname, parentid) fixturedefs = self.getfixturedefs(argname, parentnode)
if fixturedefs: if fixturedefs:
arg2fixturedefs[argname] = fixturedefs arg2fixturedefs[argname] = fixturedefs
for arg in fixturedefs[-1].argnames: for arg in fixturedefs[-1].argnames:
@ -1621,6 +1618,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,13 +1732,7 @@ class FixtureManager:
return return
self._holderobjseen.add(holderobj) self._holderobjseen.add(holderobj)
autousenames = []
for name in dir(holderobj): for name in dir(holderobj):
# ugly workaround for one of the fspath deprecated property of node
# todo: safely generalize
if isinstance(holderobj, nodes.Node) and name == "fspath":
continue
# 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.
obj = safe_getattr(holderobj, name, None) obj = safe_getattr(holderobj, name, None)
@ -1695,38 +1749,21 @@ 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, node: nodes.Node
) -> Optional[Sequence[FixtureDef[Any]]]: ) -> Optional[Sequence[FixtureDef[Any]]]:
"""Get FixtureDefs for a fixture name which are applicable """Get FixtureDefs for a fixture name which are applicable
to a given node. to a given node.
@ -1737,18 +1774,18 @@ class FixtureManager:
an empty result is returned). an empty result is returned).
:param argname: Name of the fixture to search for. :param argname: Name of the fixture to search for.
:param nodeid: Full node id of the requesting test. :param node: The requesting Node.
""" """
try: try:
fixturedefs = self._arg2fixturedefs[argname] fixturedefs = self._arg2fixturedefs[argname]
except KeyError: except KeyError:
return None return None
return tuple(self._matchfactories(fixturedefs, nodeid)) return tuple(self._matchfactories(fixturedefs, node))
def _matchfactories( def _matchfactories(
self, fixturedefs: Iterable[FixtureDef[Any]], nodeid: str self, fixturedefs: Iterable[FixtureDef[Any]], node: nodes.Node
) -> Iterator[FixtureDef[Any]]: ) -> Iterator[FixtureDef[Any]]:
parentnodeids = set(nodes.iterparentnodeids(nodeid)) parentnodeids = {n.nodeid for n in nodes.iterparentnodes(node)}
for fixturedef in fixturedefs: for fixturedef in fixturedefs:
if fixturedef.baseid in parentnodeids: if fixturedef.baseid in parentnodeids:
yield fixturedef yield fixturedef

View File

@ -49,49 +49,13 @@ SEP = "/"
tracebackcutdir = Path(_pytest.__file__).parent tracebackcutdir = Path(_pytest.__file__).parent
def iterparentnodeids(nodeid: str) -> Iterator[str]: def iterparentnodes(node: "Node") -> Iterator["Node"]:
"""Return the parent node IDs of a given node ID, inclusive. """Return the parent nodes, including the node itself, from the node
upwards."""
For the node ID parent: Optional[Node] = node
while parent is not None:
"testing/code/test_excinfo.py::TestFormattedExcinfo::test_repr_source" yield parent
parent = parent.parent
the result would be
""
"testing"
"testing/code"
"testing/code/test_excinfo.py"
"testing/code/test_excinfo.py::TestFormattedExcinfo"
"testing/code/test_excinfo.py::TestFormattedExcinfo::test_repr_source"
Note that / components are only considered until the first ::.
"""
pos = 0
first_colons: Optional[int] = nodeid.find("::")
if first_colons == -1:
first_colons = None
# The root Session node - always present.
yield ""
# Eagerly consume SEP parts until first colons.
while True:
at = nodeid.find(SEP, pos, first_colons)
if at == -1:
break
if at > 0:
yield nodeid[:at]
pos = at + len(SEP)
# Eagerly consume :: parts.
while True:
at = nodeid.find("::", pos)
if at == -1:
break
if at > 0:
yield nodeid[:at]
pos = at + len("::")
# The node ID itself.
if nodeid:
yield nodeid
_NodeType = TypeVar("_NodeType", bound="Node") _NodeType = TypeVar("_NodeType", bound="Node")
@ -488,7 +452,7 @@ def get_fslocation_from_item(node: "Node") -> Tuple[Union[str, Path], Optional[i
* "location": a pair (path, lineno) * "location": a pair (path, lineno)
* "obj": a Python object that the node wraps. * "obj": a Python object that the node wraps.
* "fspath": just a path * "path": just a path
:rtype: A tuple of (str|Path, int) with filename and 0-based line number. :rtype: A tuple of (str|Path, int) with filename and 0-based line number.
""" """
@ -499,7 +463,7 @@ def get_fslocation_from_item(node: "Node") -> Tuple[Union[str, Path], Optional[i
obj = getattr(node, "obj", None) obj = getattr(node, "obj", None)
if obj is not None: if obj is not None:
return getfslineno(obj) return getfslineno(obj)
return getattr(node, "fspath", "unknown location"), -1 return getattr(node, "path", "unknown location"), -1
class Collector(Node, abc.ABC): class Collector(Node, abc.ABC):

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

@ -29,7 +29,6 @@ from _pytest.python import Class
from _pytest.python import Function from _pytest.python import Function
from _pytest.python import Module from _pytest.python import Module
from _pytest.runner import CallInfo from _pytest.runner import CallInfo
from _pytest.scope import Scope
if TYPE_CHECKING: if TYPE_CHECKING:
import unittest import unittest
@ -71,8 +70,9 @@ class UnitTestCase(Class):
skipped = _is_skipped(cls) skipped = _is_skipped(cls)
if not skipped: if not skipped:
self._inject_setup_teardown_fixtures(cls) self._register_unittest_setup_method_fixture(cls)
self._inject_setup_class_fixture() self._register_unittest_setup_class_fixture(cls)
self._register_setup_class_fixture()
self.session._fixturemanager.parsefactories(self, unittest=True) self.session._fixturemanager.parsefactories(self, unittest=True)
loader = TestLoader() loader = TestLoader()
@ -93,91 +93,75 @@ 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_setup_teardown_fixtures(self, cls: type) -> None: def _register_unittest_setup_class_fixture(self, cls: type) -> None:
"""Injects a hidden auto-use fixture to invoke setUpClass/setup_method and corresponding """Register an auto-use fixture to invoke setUpClass and
teardown functions (#517).""" tearDownClass (#517)."""
class_fixture = _make_xunit_fixture( setup = getattr(cls, "setUpClass", None)
cls, teardown = getattr(cls, "tearDownClass", None)
"setUpClass", if setup is None and teardown is None:
"tearDownClass", return None
"doClassCleanups", cleanup = getattr(cls, "doClassCleanups", lambda: None)
scope=Scope.Class,
pass_self=False,
)
if class_fixture:
cls.__pytest_class_setup = class_fixture # type: ignore[attr-defined]
method_fixture = _make_xunit_fixture( def unittest_setup_class_fixture(
cls, request: FixtureRequest,
"setup_method", ) -> Generator[None, None, None]:
"teardown_method", cls = request.cls
None, if _is_skipped(cls):
scope=Scope.Function, reason = cls.__unittest_skip_why__
pass_self=True, raise pytest.skip.Exception(reason, _use_item_location=True)
) if setup is not None:
if method_fixture: try:
cls.__pytest_method_setup = method_fixture # type: ignore[attr-defined]
def _make_xunit_fixture(
obj: type,
setup_name: str,
teardown_name: str,
cleanup_name: Optional[str],
scope: Scope,
pass_self: bool,
):
setup = getattr(obj, setup_name, None)
teardown = getattr(obj, teardown_name, None)
if setup is None and teardown is None:
return None
if cleanup_name:
cleanup = getattr(obj, cleanup_name, lambda *args: None)
else:
def cleanup(*args):
pass
@pytest.fixture(
scope=scope.value,
autouse=True,
# Use a unique name to speed up lookup.
name=f"_unittest_{setup_name}_fixture_{obj.__qualname__}",
)
def fixture(self, request: FixtureRequest) -> Generator[None, None, None]:
if _is_skipped(self):
reason = self.__unittest_skip_why__
raise pytest.skip.Exception(reason, _use_item_location=True)
if setup is not None:
try:
if pass_self:
setup(self, request.function)
else:
setup() setup()
# unittest does not call the cleanup function for every BaseException, so we # unittest does not call the cleanup function for every BaseException, so we
# follow this here. # follow this here.
except Exception: except Exception:
if pass_self:
cleanup(self)
else:
cleanup() cleanup()
raise
raise yield
yield try:
try: if teardown is not None:
if teardown is not None:
if pass_self:
teardown(self, request.function)
else:
teardown() teardown()
finally: finally:
if pass_self:
cleanup(self)
else:
cleanup() cleanup()
return fixture 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 _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
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)
if setup is not None:
setup(self, request.function)
yield
if teardown is not None:
teardown(self, request.function)
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):

View File

@ -1,7 +1,9 @@
anyio[curio,trio]==4.2.0 anyio[curio,trio]==4.2.0
django==5.0 django==5.0
pytest-asyncio==0.23.2 pytest-asyncio==0.23.3
pytest-bdd==7.0.1 # Temporarily not installed until pytest-bdd is fixed:
# https://github.com/pytest-dev/pytest/pull/11785
# pytest-bdd==7.0.1
pytest-cov==4.1.0 pytest-cov==4.1.0
pytest-django==4.7.0 pytest-django==4.7.0
pytest-flakes==4.0.5 pytest-flakes==4.0.5

View File

@ -1574,7 +1574,7 @@ class TestFixtureManagerParseFactories:
""" """
def test_hello(item, fm): def test_hello(item, fm):
for name in ("fm", "hello", "item"): for name in ("fm", "hello", "item"):
faclist = fm.getfixturedefs(name, item.nodeid) faclist = fm.getfixturedefs(name, item)
assert len(faclist) == 1 assert len(faclist) == 1
fac = faclist[0] fac = faclist[0]
assert fac.func.__name__ == name assert fac.func.__name__ == name
@ -1598,7 +1598,7 @@ class TestFixtureManagerParseFactories:
def hello(self, request): def hello(self, request):
return "class" return "class"
def test_hello(self, item, fm): def test_hello(self, item, fm):
faclist = fm.getfixturedefs("hello", item.nodeid) faclist = fm.getfixturedefs("hello", item)
print(faclist) print(faclist)
assert len(faclist) == 3 assert len(faclist) == 3
@ -1804,7 +1804,7 @@ class TestAutouseDiscovery:
""" """
from _pytest.pytester import get_public_names from _pytest.pytester import get_public_names
def test_check_setup(item, fm): def test_check_setup(item, fm):
autousenames = list(fm._getautousenames(item.nodeid)) autousenames = list(fm._getautousenames(item))
assert len(get_public_names(autousenames)) == 2 assert len(get_public_names(autousenames)) == 2
assert "perfunction2" in autousenames assert "perfunction2" in autousenames
assert "perfunction" in autousenames assert "perfunction" in autousenames

View File

@ -2,7 +2,6 @@ import re
import warnings import warnings
from pathlib import Path from pathlib import Path
from typing import cast from typing import cast
from typing import List
from typing import Type from typing import Type
import pytest import pytest
@ -12,29 +11,6 @@ from _pytest.pytester import Pytester
from _pytest.warning_types import PytestWarning from _pytest.warning_types import PytestWarning
@pytest.mark.parametrize(
("nodeid", "expected"),
(
("", [""]),
("a", ["", "a"]),
("aa/b", ["", "aa", "aa/b"]),
("a/b/c", ["", "a", "a/b", "a/b/c"]),
("a/bbb/c::D", ["", "a", "a/bbb", "a/bbb/c", "a/bbb/c::D"]),
("a/b/c::D::eee", ["", "a", "a/b", "a/b/c", "a/b/c::D", "a/b/c::D::eee"]),
("::xx", ["", "::xx"]),
# / only considered until first ::
("a/b/c::D/d::e", ["", "a", "a/b", "a/b/c", "a/b/c::D/d", "a/b/c::D/d::e"]),
# : alone is not a separator.
("a/b::D:e:f::g", ["", "a", "a/b", "a/b::D:e:f", "a/b::D:e:f::g"]),
# / not considered if a part of a test name
("a/b::c/d::e[/test]", ["", "a", "a/b", "a/b::c/d", "a/b::c/d::e[/test]"]),
),
)
def test_iterparentnodeids(nodeid: str, expected: List[str]) -> None:
result = list(nodes.iterparentnodeids(nodeid))
assert result == expected
def test_node_from_parent_disallowed_arguments() -> None: def test_node_from_parent_disallowed_arguments() -> None:
with pytest.raises(TypeError, match="session is"): with pytest.raises(TypeError, match="session is"):
nodes.Node.from_parent(None, session=None) # type: ignore[arg-type] nodes.Node.from_parent(None, session=None) # type: ignore[arg-type]

View File

@ -134,9 +134,11 @@ changedir = testing/plugins_integration
deps = -rtesting/plugins_integration/requirements.txt deps = -rtesting/plugins_integration/requirements.txt
setenv = setenv =
PYTHONPATH=. PYTHONPATH=.
# Command temporarily removed until pytest-bdd is fixed:
# https://github.com/pytest-dev/pytest/pull/11785
# pytest bdd_wallet.py
commands = commands =
pip check pip check
pytest bdd_wallet.py
pytest --cov=. simple_integration.py pytest --cov=. simple_integration.py
pytest --ds=django_settings simple_integration.py pytest --ds=django_settings simple_integration.py
pytest --html=simple.html simple_integration.py pytest --html=simple.html simple_integration.py