Merge branch 'main' into Improvement-catch-duplicate-values-when-determining-param-indices-in-metafunc-parametrize
This commit is contained in:
commit
909f695b9f
|
@ -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
|
||||||
|
|
|
@ -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.
|
|
@ -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
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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]
|
||||||
|
|
4
tox.ini
4
tox.ini
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue