Merge branch 'main' into Improvement-catch-duplicate-values-when-determining-param-indices-in-metafunc-parametrize
This commit is contained in:
commit
da499bbd23
|
@ -29,7 +29,7 @@ repos:
|
|||
language: python
|
||||
files: \.py$
|
||||
- repo: https://github.com/PyCQA/flake8
|
||||
rev: 6.0.0
|
||||
rev: 6.1.0
|
||||
hooks:
|
||||
- id: flake8
|
||||
language_version: python3
|
||||
|
@ -42,7 +42,7 @@ repos:
|
|||
- id: reorder-python-imports
|
||||
args: ['--application-directories=.:src', --py38-plus]
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v3.9.0
|
||||
rev: v3.10.1
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args: [--py38-plus]
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
Fixed a bug that when there are multiple fixtures for an indirect parameter,
|
||||
the scope of the highest-scope fixture is picked for the parameter set, instead of that of the one with the narrowest scope.
|
|
@ -657,13 +657,16 @@ Use :func:`pytest.raises` with the
|
|||
:ref:`pytest.mark.parametrize ref` decorator to write parametrized tests
|
||||
in which some tests raise exceptions and others do not.
|
||||
|
||||
It may be helpful to use ``nullcontext`` as a complement to ``raises``.
|
||||
``contextlib.nullcontext`` can be used to test cases that are not expected to
|
||||
raise exceptions but that should result in some value. The value is given as the
|
||||
``enter_result`` parameter, which will be available as the ``with`` statement’s
|
||||
target (``e`` in the example below).
|
||||
|
||||
For example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from contextlib import nullcontext as does_not_raise
|
||||
from contextlib import nullcontext
|
||||
|
||||
import pytest
|
||||
|
||||
|
@ -671,16 +674,17 @@ For example:
|
|||
@pytest.mark.parametrize(
|
||||
"example_input,expectation",
|
||||
[
|
||||
(3, does_not_raise()),
|
||||
(2, does_not_raise()),
|
||||
(1, does_not_raise()),
|
||||
(3, nullcontext(2)),
|
||||
(2, nullcontext(3)),
|
||||
(1, nullcontext(6)),
|
||||
(0, pytest.raises(ZeroDivisionError)),
|
||||
],
|
||||
)
|
||||
def test_division(example_input, expectation):
|
||||
"""Test how much I know division."""
|
||||
with expectation:
|
||||
assert (6 / example_input) is not None
|
||||
with expectation as e:
|
||||
assert (6 / example_input) == e
|
||||
|
||||
In the example above, the first three test cases should run unexceptionally,
|
||||
while the fourth should raise ``ZeroDivisionError``.
|
||||
In the example above, the first three test cases should run without any
|
||||
exceptions, while the fourth should raise a``ZeroDivisionError`` exception,
|
||||
which is expected by pytest.
|
||||
|
|
|
@ -54,14 +54,13 @@ operators. (See :ref:`tbreportdemo`). This allows you to use the
|
|||
idiomatic python constructs without boilerplate code while not losing
|
||||
introspection information.
|
||||
|
||||
However, if you specify a message with the assertion like this:
|
||||
If a message is specified with the assertion like this:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
assert a % 2 == 0, "value was odd, should be even"
|
||||
|
||||
then no assertion introspection takes places at all and the message
|
||||
will be simply shown in the traceback.
|
||||
it is printed alongside the assertion introspection in the traceback.
|
||||
|
||||
See :ref:`assert-details` for more information on assertion introspection.
|
||||
|
||||
|
|
|
@ -82,6 +82,8 @@ pytest.exit
|
|||
pytest.main
|
||||
~~~~~~~~~~~
|
||||
|
||||
**Tutorial**: :ref:`pytest.main-usage`
|
||||
|
||||
.. autofunction:: pytest.main
|
||||
|
||||
pytest.param
|
||||
|
|
|
@ -222,7 +222,7 @@ def _compare_eq_any(left: Any, right: Any, verbose: int = 0) -> List[str]:
|
|||
other_side = right if isinstance(left, ApproxBase) else left
|
||||
|
||||
explanation = approx_side._repr_compare(other_side)
|
||||
elif type(left) == type(right) and (
|
||||
elif type(left) is type(right) and (
|
||||
isdatacls(left) or isattrs(left) or isnamedtuple(left)
|
||||
):
|
||||
# Note: unlike dataclasses/attrs, namedtuples compare only the
|
||||
|
|
|
@ -581,26 +581,25 @@ class PytestPluginManager(PluginManager):
|
|||
def _try_load_conftest(
|
||||
self, anchor: Path, importmode: Union[str, ImportMode], rootpath: Path
|
||||
) -> None:
|
||||
self._getconftestmodules(anchor, importmode, rootpath)
|
||||
self._loadconftestmodules(anchor, importmode, rootpath)
|
||||
# let's also consider test* subdirs
|
||||
if anchor.is_dir():
|
||||
for x in anchor.glob("test*"):
|
||||
if x.is_dir():
|
||||
self._getconftestmodules(x, importmode, rootpath)
|
||||
self._loadconftestmodules(x, importmode, rootpath)
|
||||
|
||||
def _getconftestmodules(
|
||||
def _loadconftestmodules(
|
||||
self, path: Path, importmode: Union[str, ImportMode], rootpath: Path
|
||||
) -> Sequence[types.ModuleType]:
|
||||
) -> None:
|
||||
if self._noconftest:
|
||||
return []
|
||||
return
|
||||
|
||||
directory = self._get_directory(path)
|
||||
|
||||
# Optimization: avoid repeated searches in the same directory.
|
||||
# Assumes always called with same importmode and rootpath.
|
||||
existing_clist = self._dirpath2confmods.get(directory)
|
||||
if existing_clist is not None:
|
||||
return existing_clist
|
||||
if directory in self._dirpath2confmods:
|
||||
return
|
||||
|
||||
# XXX these days we may rather want to use config.rootpath
|
||||
# and allow users to opt into looking into the rootdir parent
|
||||
|
@ -613,16 +612,17 @@ class PytestPluginManager(PluginManager):
|
|||
mod = self._importconftest(conftestpath, importmode, rootpath)
|
||||
clist.append(mod)
|
||||
self._dirpath2confmods[directory] = clist
|
||||
return clist
|
||||
|
||||
def _getconftestmodules(self, path: Path) -> Sequence[types.ModuleType]:
|
||||
directory = self._get_directory(path)
|
||||
return self._dirpath2confmods.get(directory, ())
|
||||
|
||||
def _rget_with_confmod(
|
||||
self,
|
||||
name: str,
|
||||
path: Path,
|
||||
importmode: Union[str, ImportMode],
|
||||
rootpath: Path,
|
||||
) -> Tuple[types.ModuleType, Any]:
|
||||
modules = self._getconftestmodules(path, importmode, rootpath=rootpath)
|
||||
modules = self._getconftestmodules(path)
|
||||
for mod in reversed(modules):
|
||||
try:
|
||||
return mod, getattr(mod, name)
|
||||
|
@ -1562,13 +1562,9 @@ class Config:
|
|||
else:
|
||||
return self._getini_unknown_type(name, type, value)
|
||||
|
||||
def _getconftest_pathlist(
|
||||
self, name: str, path: Path, rootpath: Path
|
||||
) -> Optional[List[Path]]:
|
||||
def _getconftest_pathlist(self, name: str, path: Path) -> Optional[List[Path]]:
|
||||
try:
|
||||
mod, relroots = self.pluginmanager._rget_with_confmod(
|
||||
name, path, self.getoption("importmode"), rootpath
|
||||
)
|
||||
mod, relroots = self.pluginmanager._rget_with_confmod(name, path)
|
||||
except KeyError:
|
||||
return None
|
||||
assert mod.__file__ is not None
|
||||
|
|
|
@ -155,11 +155,17 @@ def getfixturemarker(obj: object) -> Optional["FixtureFunctionMarker"]:
|
|||
)
|
||||
|
||||
|
||||
# Parametrized fixture key, helper alias for code below.
|
||||
_Key = Tuple[object, ...]
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class FixtureArgKey:
|
||||
argname: str
|
||||
param_index: int
|
||||
scoped_item_path: Optional[Path]
|
||||
item_cls: Optional[type]
|
||||
|
||||
|
||||
def get_parametrized_fixture_keys(item: nodes.Item, scope: Scope) -> Iterator[_Key]:
|
||||
def get_parametrized_fixture_keys(
|
||||
item: nodes.Item, scope: Scope
|
||||
) -> Iterator[FixtureArgKey]:
|
||||
"""Return list of keys for all parametrized arguments which match
|
||||
the specified scope."""
|
||||
assert scope is not Scope.Function
|
||||
|
@ -169,24 +175,28 @@ def get_parametrized_fixture_keys(item: nodes.Item, scope: Scope) -> Iterator[_K
|
|||
pass
|
||||
else:
|
||||
cs: CallSpec2 = callspec
|
||||
# cs.indices.items() is random order of argnames. Need to
|
||||
# cs.indices is random order of argnames. Need to
|
||||
# sort this so that different calls to
|
||||
# get_parametrized_fixture_keys will be deterministic.
|
||||
for argname, param_index in sorted(cs.indices.items()):
|
||||
for argname in sorted(cs.indices):
|
||||
if cs._arg2scope[argname] != scope:
|
||||
continue
|
||||
|
||||
item_cls = None
|
||||
if scope is Scope.Session:
|
||||
key: _Key = (argname, param_index)
|
||||
scoped_item_path = None
|
||||
elif scope is Scope.Package:
|
||||
key = (argname, param_index, item.path)
|
||||
scoped_item_path = item.path
|
||||
elif scope is Scope.Module:
|
||||
key = (argname, param_index, item.path)
|
||||
scoped_item_path = item.path
|
||||
elif scope is Scope.Class:
|
||||
scoped_item_path = item.path
|
||||
item_cls = item.cls # type: ignore[attr-defined]
|
||||
key = (argname, param_index, item.path, item_cls)
|
||||
else:
|
||||
assert_never(scope)
|
||||
yield key
|
||||
|
||||
param_index = cs.indices[argname]
|
||||
yield FixtureArgKey(argname, param_index, scoped_item_path, item_cls)
|
||||
|
||||
|
||||
# Algorithm for sorting on a per-parametrized resource setup basis.
|
||||
|
@ -196,12 +206,12 @@ def get_parametrized_fixture_keys(item: nodes.Item, scope: Scope) -> Iterator[_K
|
|||
|
||||
|
||||
def reorder_items(items: Sequence[nodes.Item]) -> List[nodes.Item]:
|
||||
argkeys_cache: Dict[Scope, Dict[nodes.Item, Dict[_Key, None]]] = {}
|
||||
items_by_argkey: Dict[Scope, Dict[_Key, Deque[nodes.Item]]] = {}
|
||||
argkeys_cache: Dict[Scope, Dict[nodes.Item, Dict[FixtureArgKey, None]]] = {}
|
||||
items_by_argkey: Dict[Scope, Dict[FixtureArgKey, Deque[nodes.Item]]] = {}
|
||||
for scope in HIGH_SCOPES:
|
||||
d: Dict[nodes.Item, Dict[_Key, None]] = {}
|
||||
d: Dict[nodes.Item, Dict[FixtureArgKey, None]] = {}
|
||||
argkeys_cache[scope] = d
|
||||
item_d: Dict[_Key, Deque[nodes.Item]] = defaultdict(deque)
|
||||
item_d: Dict[FixtureArgKey, Deque[nodes.Item]] = defaultdict(deque)
|
||||
items_by_argkey[scope] = item_d
|
||||
for item in items:
|
||||
keys = dict.fromkeys(get_parametrized_fixture_keys(item, scope), None)
|
||||
|
@ -217,8 +227,8 @@ def reorder_items(items: Sequence[nodes.Item]) -> List[nodes.Item]:
|
|||
|
||||
def fix_cache_order(
|
||||
item: nodes.Item,
|
||||
argkeys_cache: Dict[Scope, Dict[nodes.Item, Dict[_Key, None]]],
|
||||
items_by_argkey: Dict[Scope, Dict[_Key, "Deque[nodes.Item]"]],
|
||||
argkeys_cache: Dict[Scope, Dict[nodes.Item, Dict[FixtureArgKey, None]]],
|
||||
items_by_argkey: Dict[Scope, Dict[FixtureArgKey, "Deque[nodes.Item]"]],
|
||||
) -> None:
|
||||
for scope in HIGH_SCOPES:
|
||||
for key in argkeys_cache[scope].get(item, []):
|
||||
|
@ -227,13 +237,13 @@ def fix_cache_order(
|
|||
|
||||
def reorder_items_atscope(
|
||||
items: Dict[nodes.Item, None],
|
||||
argkeys_cache: Dict[Scope, Dict[nodes.Item, Dict[_Key, None]]],
|
||||
items_by_argkey: Dict[Scope, Dict[_Key, "Deque[nodes.Item]"]],
|
||||
argkeys_cache: Dict[Scope, Dict[nodes.Item, Dict[FixtureArgKey, None]]],
|
||||
items_by_argkey: Dict[Scope, Dict[FixtureArgKey, "Deque[nodes.Item]"]],
|
||||
scope: Scope,
|
||||
) -> Dict[nodes.Item, None]:
|
||||
if scope is Scope.Function or len(items) < 3:
|
||||
return items
|
||||
ignore: Set[Optional[_Key]] = set()
|
||||
ignore: Set[Optional[FixtureArgKey]] = set()
|
||||
items_deque = deque(items)
|
||||
items_done: Dict[nodes.Item, None] = {}
|
||||
scoped_items_by_argkey = items_by_argkey[scope]
|
||||
|
@ -394,7 +404,7 @@ class FixtureRequest:
|
|||
node: Optional[Union[nodes.Item, nodes.Collector]] = self._pyfuncitem
|
||||
elif scope is Scope.Package:
|
||||
# FIXME: _fixturedef is not defined on FixtureRequest (this class),
|
||||
# but on FixtureRequest (a subclass).
|
||||
# but on SubRequest (a subclass).
|
||||
node = get_scope_package(self._pyfuncitem, self._fixturedef) # type: ignore[attr-defined]
|
||||
else:
|
||||
node = get_scope_node(self._pyfuncitem, scope)
|
||||
|
|
|
@ -376,7 +376,7 @@ def _in_venv(path: Path) -> bool:
|
|||
|
||||
def pytest_ignore_collect(collection_path: Path, config: Config) -> Optional[bool]:
|
||||
ignore_paths = config._getconftest_pathlist(
|
||||
"collect_ignore", path=collection_path.parent, rootpath=config.rootpath
|
||||
"collect_ignore", path=collection_path.parent
|
||||
)
|
||||
ignore_paths = ignore_paths or []
|
||||
excludeopt = config.getoption("ignore")
|
||||
|
@ -387,7 +387,7 @@ def pytest_ignore_collect(collection_path: Path, config: Config) -> Optional[boo
|
|||
return True
|
||||
|
||||
ignore_globs = config._getconftest_pathlist(
|
||||
"collect_ignore_glob", path=collection_path.parent, rootpath=config.rootpath
|
||||
"collect_ignore_glob", path=collection_path.parent
|
||||
)
|
||||
ignore_globs = ignore_globs or []
|
||||
excludeglobopt = config.getoption("ignore_glob")
|
||||
|
@ -551,11 +551,16 @@ class Session(nodes.FSCollector):
|
|||
pm = self.config.pluginmanager
|
||||
# Check if we have the common case of running
|
||||
# hooks with all conftest.py files.
|
||||
my_conftestmodules = pm._getconftestmodules(
|
||||
#
|
||||
# TODO: pytest relies on this call to load non-initial conftests. This
|
||||
# is incidental. It will be better to load conftests at a more
|
||||
# well-defined place.
|
||||
pm._loadconftestmodules(
|
||||
path,
|
||||
self.config.getoption("importmode"),
|
||||
rootpath=self.config.rootpath,
|
||||
)
|
||||
my_conftestmodules = pm._getconftestmodules(path)
|
||||
remove_mods = pm._conftest_plugins.difference(my_conftestmodules)
|
||||
if remove_mods:
|
||||
# One or more conftests are not in use at this fspath.
|
||||
|
|
|
@ -41,7 +41,6 @@ from _pytest._code.code import Traceback
|
|||
from _pytest._io import TerminalWriter
|
||||
from _pytest._io.saferepr import saferepr
|
||||
from _pytest.compat import ascii_escaped
|
||||
from _pytest.compat import assert_never
|
||||
from _pytest.compat import get_default_arg_names
|
||||
from _pytest.compat import get_real_func
|
||||
from _pytest.compat import getimfunc
|
||||
|
@ -498,8 +497,11 @@ class PyCollector(PyobjMixin, nodes.Collector):
|
|||
if not metafunc._calls:
|
||||
yield Function.from_parent(self, name=name, fixtureinfo=fixtureinfo)
|
||||
else:
|
||||
# Dynamic direct parametrization may have shadowed some fixtures,
|
||||
# so make sure we update what the function really needs.
|
||||
# Direct parametrizations taking place in module/class-specific
|
||||
# `metafunc.parametrize` calls may have shadowed some fixtures, so make sure
|
||||
# we update what the function really needs a.k.a its fixture closure. Note that
|
||||
# direct parametrizations using `@pytest.mark.parametrize` have already been considered
|
||||
# into making the closure using `ignore_args` arg to `getfixtureclosure`.
|
||||
fixtureinfo.prune_dependency_tree()
|
||||
|
||||
for callspec in metafunc._calls:
|
||||
|
@ -1283,8 +1285,9 @@ class Metafunc:
|
|||
during the collection phase. If you need to setup expensive resources
|
||||
see about setting indirect to do it rather than at test setup time.
|
||||
|
||||
Can be called multiple times, in which case each call parametrizes all
|
||||
previous parametrizations, e.g.
|
||||
Can be called multiple times per test function (but only on different
|
||||
argument names), in which case each call parametrizes all previous
|
||||
parametrizations, e.g.
|
||||
|
||||
::
|
||||
|
||||
|
@ -1389,7 +1392,8 @@ class Metafunc:
|
|||
# a per-scope basis. We thus store and cache the fixturedef on the
|
||||
# node related to the scope.
|
||||
if scope_ is not Scope.Function:
|
||||
collector = cast(nodes.Node, self.definition.parent)
|
||||
collector = self.definition.parent
|
||||
assert collector is not None
|
||||
node = get_scope_node(collector, scope_)
|
||||
if node is None:
|
||||
# If used class scope and there is no class, use module-level
|
||||
|
@ -1402,7 +1406,7 @@ class Metafunc:
|
|||
elif scope_ is Scope.Package:
|
||||
node = collector.session
|
||||
else:
|
||||
assert_never(scope_) # type: ignore[arg-type]
|
||||
assert False, f"Unhandled missing scope: {scope}"
|
||||
if node is None:
|
||||
name2pseudofixturedef = None
|
||||
else:
|
||||
|
@ -1410,9 +1414,9 @@ class Metafunc:
|
|||
name2pseudofixturedef = node.stash.setdefault(
|
||||
name2pseudofixturedef_key, default
|
||||
)
|
||||
arg_values_types = self._resolve_arg_value_types(argnames, indirect)
|
||||
arg_directness = self._resolve_args_directness(argnames, indirect)
|
||||
for argname in argnames:
|
||||
if arg_values_types[argname] == "params":
|
||||
if arg_directness[argname] == "indirect":
|
||||
continue
|
||||
if name2pseudofixturedef is not None and argname in name2pseudofixturedef:
|
||||
fixturedef = name2pseudofixturedef[argname]
|
||||
|
@ -1517,28 +1521,30 @@ class Metafunc:
|
|||
|
||||
return list(itertools.islice(ids, num_ids))
|
||||
|
||||
def _resolve_arg_value_types(
|
||||
def _resolve_args_directness(
|
||||
self,
|
||||
argnames: Sequence[str],
|
||||
indirect: Union[bool, Sequence[str]],
|
||||
) -> Dict[str, "Literal['params', 'funcargs']"]:
|
||||
"""Resolve if each parametrized argument must be considered a
|
||||
parameter to a fixture or a "funcarg" to the function, based on the
|
||||
``indirect`` parameter of the parametrized() call.
|
||||
) -> Dict[str, Literal["indirect", "direct"]]:
|
||||
"""Resolve if each parametrized argument must be considered an indirect
|
||||
parameter to a fixture of the same name, or a direct parameter to the
|
||||
parametrized function, based on the ``indirect`` parameter of the
|
||||
parametrized() call.
|
||||
|
||||
:param List[str] argnames: List of argument names passed to ``parametrize()``.
|
||||
:param indirect: Same as the ``indirect`` parameter of ``parametrize()``.
|
||||
:rtype: Dict[str, str]
|
||||
A dict mapping each arg name to either:
|
||||
* "params" if the argname should be the parameter of a fixture of the same name.
|
||||
* "funcargs" if the argname should be a parameter to the parametrized test function.
|
||||
:param argnames:
|
||||
List of argument names passed to ``parametrize()``.
|
||||
:param indirect:
|
||||
Same as the ``indirect`` parameter of ``parametrize()``.
|
||||
:returns
|
||||
A dict mapping each arg name to either "indirect" or "direct".
|
||||
"""
|
||||
arg_directness: Dict[str, Literal["indirect", "direct"]]
|
||||
if isinstance(indirect, bool):
|
||||
valtypes: Dict[str, Literal["params", "funcargs"]] = dict.fromkeys(
|
||||
argnames, "params" if indirect else "funcargs"
|
||||
arg_directness = dict.fromkeys(
|
||||
argnames, "indirect" if indirect else "direct"
|
||||
)
|
||||
elif isinstance(indirect, Sequence):
|
||||
valtypes = dict.fromkeys(argnames, "funcargs")
|
||||
arg_directness = dict.fromkeys(argnames, "direct")
|
||||
for arg in indirect:
|
||||
if arg not in argnames:
|
||||
fail(
|
||||
|
@ -1547,7 +1553,7 @@ class Metafunc:
|
|||
),
|
||||
pytrace=False,
|
||||
)
|
||||
valtypes[arg] = "params"
|
||||
arg_directness[arg] = "indirect"
|
||||
else:
|
||||
fail(
|
||||
"In {func}: expected Sequence or boolean for indirect, got {type}".format(
|
||||
|
@ -1555,7 +1561,7 @@ class Metafunc:
|
|||
),
|
||||
pytrace=False,
|
||||
)
|
||||
return valtypes
|
||||
return arg_directness
|
||||
|
||||
def _validate_if_using_arg_names(
|
||||
self,
|
||||
|
@ -1612,7 +1618,7 @@ def _find_parametrized_scope(
|
|||
if all_arguments_are_fixtures:
|
||||
fixturedefs = arg2fixturedefs or {}
|
||||
used_scopes = [
|
||||
fixturedef[0]._scope
|
||||
fixturedef[-1]._scope
|
||||
for name, fixturedef in fixturedefs.items()
|
||||
if name in argnames
|
||||
]
|
||||
|
@ -1778,7 +1784,7 @@ class Function(PyobjMixin, nodes.Item):
|
|||
:param config:
|
||||
The pytest Config object.
|
||||
:param callspec:
|
||||
If given, this is function has been parametrized and the callspec contains
|
||||
If given, this function has been parametrized and the callspec contains
|
||||
meta information about the parametrization.
|
||||
:param callobj:
|
||||
If given, the object which will be called when the Function is invoked,
|
||||
|
|
|
@ -1573,4 +1573,4 @@ class TestBinaryAndTextMethods:
|
|||
x.write_text(part, "ascii")
|
||||
s = x.read_text("ascii")
|
||||
assert s == part
|
||||
assert type(s) == type(part)
|
||||
assert type(s) is type(part)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
anyio[curio,trio]==3.7.1
|
||||
django==4.2.3
|
||||
django==4.2.4
|
||||
pytest-asyncio==0.21.1
|
||||
pytest-bdd==6.1.1
|
||||
pytest-cov==4.1.0
|
||||
|
|
|
@ -2103,9 +2103,7 @@ class TestAutouseManagement:
|
|||
reprec = pytester.inline_run("-v", "-s", "--confcutdir", pytester.path)
|
||||
reprec.assertoutcome(passed=8)
|
||||
config = reprec.getcalls("pytest_unconfigure")[0].config
|
||||
values = config.pluginmanager._getconftestmodules(
|
||||
p, importmode="prepend", rootpath=pytester.path
|
||||
)[0].values
|
||||
values = config.pluginmanager._getconftestmodules(p)[0].values
|
||||
assert values == ["fin_a1", "fin_a2", "fin_b1", "fin_b2"] * 2
|
||||
|
||||
def test_scope_ordering(self, pytester: Pytester) -> None:
|
||||
|
|
|
@ -23,6 +23,7 @@ from _pytest.compat import getfuncargnames
|
|||
from _pytest.compat import NOTSET
|
||||
from _pytest.outcomes import fail
|
||||
from _pytest.pytester import Pytester
|
||||
from _pytest.python import Function
|
||||
from _pytest.python import IdMaker
|
||||
from _pytest.scope import Scope
|
||||
|
||||
|
@ -161,6 +162,7 @@ class TestMetafunc:
|
|||
module_fix=[DummyFixtureDef(Scope.Module)],
|
||||
class_fix=[DummyFixtureDef(Scope.Class)],
|
||||
func_fix=[DummyFixtureDef(Scope.Function)],
|
||||
mixed_fix=[DummyFixtureDef(Scope.Module), DummyFixtureDef(Scope.Class)],
|
||||
),
|
||||
)
|
||||
|
||||
|
@ -197,6 +199,7 @@ class TestMetafunc:
|
|||
)
|
||||
== Scope.Module
|
||||
)
|
||||
assert find_scope(["mixed_fix"], indirect=True) == Scope.Class
|
||||
|
||||
def test_parametrize_and_id(self) -> None:
|
||||
def func(x, y):
|
||||
|
@ -1588,29 +1591,94 @@ class TestMetafuncFunctional:
|
|||
def test_parametrize_module_level_test_with_class_scope(
|
||||
self, pytester: Pytester
|
||||
) -> None:
|
||||
"""
|
||||
Test that a class-scoped parametrization without a corresponding `Class`
|
||||
gets module scope, i.e. we only create a single FixtureDef for it per module.
|
||||
"""
|
||||
module = pytester.makepyfile(
|
||||
"""
|
||||
import pytest
|
||||
|
||||
@pytest.mark.parametrize("x", [0, 1], scope="class")
|
||||
def test_1(x):
|
||||
pass
|
||||
|
||||
@pytest.mark.parametrize("x", [1, 2], scope="module")
|
||||
def test_2(x):
|
||||
pass
|
||||
"""
|
||||
)
|
||||
test_1_0, _, test_2_0, _ = pytester.genitems((pytester.getmodulecol(module),))
|
||||
|
||||
assert isinstance(test_1_0, Function)
|
||||
assert test_1_0.name == "test_1[0]"
|
||||
test_1_fixture_x = test_1_0._fixtureinfo.name2fixturedefs["x"][-1]
|
||||
|
||||
assert isinstance(test_2_0, Function)
|
||||
assert test_2_0.name == "test_2[1]"
|
||||
test_2_fixture_x = test_2_0._fixtureinfo.name2fixturedefs["x"][-1]
|
||||
|
||||
assert test_1_fixture_x is test_2_fixture_x
|
||||
|
||||
def test_reordering_with_scopeless_and_just_indirect_parametrization(
|
||||
self, pytester: Pytester
|
||||
) -> None:
|
||||
pytester.makeconftest(
|
||||
"""
|
||||
import pytest
|
||||
|
||||
@pytest.fixture(scope="package")
|
||||
def fixture1():
|
||||
pass
|
||||
"""
|
||||
)
|
||||
pytester.makepyfile(
|
||||
"""
|
||||
import pytest
|
||||
|
||||
@pytest.fixture
|
||||
def item(request):
|
||||
return request._pyfuncitem
|
||||
@pytest.fixture(scope="module")
|
||||
def fixture0():
|
||||
pass
|
||||
|
||||
fixturedef = None
|
||||
@pytest.fixture(scope="module")
|
||||
def fixture1(fixture0):
|
||||
pass
|
||||
|
||||
@pytest.mark.parametrize("x", [0, 1], scope="class")
|
||||
def test_1(item, x):
|
||||
global fixturedef
|
||||
fixturedef = item._fixtureinfo.name2fixturedefs['x'][-1]
|
||||
@pytest.mark.parametrize("fixture1", [0], indirect=True)
|
||||
def test_0(fixture1):
|
||||
pass
|
||||
|
||||
@pytest.mark.parametrize("x", [1, 2], scope="module")
|
||||
def test_2(item, x):
|
||||
global fixturedef
|
||||
assert fixturedef == item._fixtureinfo.name2fixturedefs['x'][-1]
|
||||
@pytest.fixture(scope="module")
|
||||
def fixture():
|
||||
pass
|
||||
|
||||
@pytest.mark.parametrize("fixture", [0], indirect=True)
|
||||
def test_1(fixture):
|
||||
pass
|
||||
|
||||
def test_2():
|
||||
pass
|
||||
|
||||
class Test:
|
||||
@pytest.fixture(scope="class")
|
||||
def fixture(self, fixture):
|
||||
pass
|
||||
|
||||
@pytest.mark.parametrize("fixture", [0], indirect=True)
|
||||
def test_3(self, fixture):
|
||||
pass
|
||||
"""
|
||||
)
|
||||
result = pytester.runpytest()
|
||||
result = pytester.runpytest("-v")
|
||||
assert result.ret == 0
|
||||
result.stdout.fnmatch_lines(
|
||||
[
|
||||
"*test_0*",
|
||||
"*test_1*",
|
||||
"*test_2*",
|
||||
"*test_3*",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class TestMetafuncFunctionalAuto:
|
||||
|
|
|
@ -642,18 +642,11 @@ class TestConfigAPI:
|
|||
p = tmp_path.joinpath("conftest.py")
|
||||
p.write_text(f"mylist = {['.', str(somepath)]}", encoding="utf-8")
|
||||
config = pytester.parseconfigure(p)
|
||||
assert (
|
||||
config._getconftest_pathlist("notexist", path=tmp_path, rootpath=tmp_path)
|
||||
is None
|
||||
)
|
||||
pl = (
|
||||
config._getconftest_pathlist("mylist", path=tmp_path, rootpath=tmp_path)
|
||||
or []
|
||||
)
|
||||
print(pl)
|
||||
assert len(pl) == 2
|
||||
assert pl[0] == tmp_path
|
||||
assert pl[1] == somepath
|
||||
assert config._getconftest_pathlist("notexist", path=tmp_path) is None
|
||||
assert config._getconftest_pathlist("mylist", path=tmp_path) == [
|
||||
tmp_path,
|
||||
somepath,
|
||||
]
|
||||
|
||||
@pytest.mark.parametrize("maybe_type", ["not passed", "None", '"string"'])
|
||||
def test_addini(self, pytester: Pytester, maybe_type: str) -> None:
|
||||
|
|
|
@ -62,28 +62,22 @@ class TestConftestValueAccessGlobal:
|
|||
def test_basic_init(self, basedir: Path) -> None:
|
||||
conftest = PytestPluginManager()
|
||||
p = basedir / "adir"
|
||||
assert (
|
||||
conftest._rget_with_confmod("a", p, importmode="prepend", rootpath=basedir)[
|
||||
1
|
||||
]
|
||||
== 1
|
||||
)
|
||||
conftest._loadconftestmodules(p, importmode="prepend", rootpath=basedir)
|
||||
assert conftest._rget_with_confmod("a", p)[1] == 1
|
||||
|
||||
def test_immediate_initialiation_and_incremental_are_the_same(
|
||||
self, basedir: Path
|
||||
) -> None:
|
||||
conftest = PytestPluginManager()
|
||||
assert not len(conftest._dirpath2confmods)
|
||||
conftest._getconftestmodules(
|
||||
basedir, importmode="prepend", rootpath=Path(basedir)
|
||||
)
|
||||
conftest._loadconftestmodules(basedir, importmode="prepend", rootpath=basedir)
|
||||
snap1 = len(conftest._dirpath2confmods)
|
||||
assert snap1 == 1
|
||||
conftest._getconftestmodules(
|
||||
conftest._loadconftestmodules(
|
||||
basedir / "adir", importmode="prepend", rootpath=basedir
|
||||
)
|
||||
assert len(conftest._dirpath2confmods) == snap1 + 1
|
||||
conftest._getconftestmodules(
|
||||
conftest._loadconftestmodules(
|
||||
basedir / "b", importmode="prepend", rootpath=basedir
|
||||
)
|
||||
assert len(conftest._dirpath2confmods) == snap1 + 2
|
||||
|
@ -91,33 +85,23 @@ class TestConftestValueAccessGlobal:
|
|||
def test_value_access_not_existing(self, basedir: Path) -> None:
|
||||
conftest = ConftestWithSetinitial(basedir)
|
||||
with pytest.raises(KeyError):
|
||||
conftest._rget_with_confmod(
|
||||
"a", basedir, importmode="prepend", rootpath=Path(basedir)
|
||||
)
|
||||
conftest._rget_with_confmod("a", basedir)
|
||||
|
||||
def test_value_access_by_path(self, basedir: Path) -> None:
|
||||
conftest = ConftestWithSetinitial(basedir)
|
||||
adir = basedir / "adir"
|
||||
assert (
|
||||
conftest._rget_with_confmod(
|
||||
"a", adir, importmode="prepend", rootpath=basedir
|
||||
)[1]
|
||||
== 1
|
||||
)
|
||||
assert (
|
||||
conftest._rget_with_confmod(
|
||||
"a", adir / "b", importmode="prepend", rootpath=basedir
|
||||
)[1]
|
||||
== 1.5
|
||||
conftest._loadconftestmodules(adir, importmode="prepend", rootpath=basedir)
|
||||
assert conftest._rget_with_confmod("a", adir)[1] == 1
|
||||
conftest._loadconftestmodules(
|
||||
adir / "b", importmode="prepend", rootpath=basedir
|
||||
)
|
||||
assert conftest._rget_with_confmod("a", adir / "b")[1] == 1.5
|
||||
|
||||
def test_value_access_with_confmod(self, basedir: Path) -> None:
|
||||
startdir = basedir / "adir" / "b"
|
||||
startdir.joinpath("xx").mkdir()
|
||||
conftest = ConftestWithSetinitial(startdir)
|
||||
mod, value = conftest._rget_with_confmod(
|
||||
"a", startdir, importmode="prepend", rootpath=Path(basedir)
|
||||
)
|
||||
mod, value = conftest._rget_with_confmod("a", startdir)
|
||||
assert value == 1.5
|
||||
assert mod.__file__ is not None
|
||||
path = Path(mod.__file__)
|
||||
|
@ -143,9 +127,7 @@ def test_doubledash_considered(pytester: Pytester) -> None:
|
|||
conf.joinpath("conftest.py").touch()
|
||||
conftest = PytestPluginManager()
|
||||
conftest_setinitial(conftest, [conf.name, conf.name])
|
||||
values = conftest._getconftestmodules(
|
||||
conf, importmode="prepend", rootpath=pytester.path
|
||||
)
|
||||
values = conftest._getconftestmodules(conf)
|
||||
assert len(values) == 1
|
||||
|
||||
|
||||
|
@ -192,26 +174,22 @@ def test_conftestcutdir(pytester: Pytester) -> None:
|
|||
p = pytester.mkdir("x")
|
||||
conftest = PytestPluginManager()
|
||||
conftest_setinitial(conftest, [pytester.path], confcutdir=p)
|
||||
values = conftest._getconftestmodules(
|
||||
p, importmode="prepend", rootpath=pytester.path
|
||||
)
|
||||
conftest._loadconftestmodules(p, importmode="prepend", rootpath=pytester.path)
|
||||
values = conftest._getconftestmodules(p)
|
||||
assert len(values) == 0
|
||||
values = conftest._getconftestmodules(
|
||||
conftest._loadconftestmodules(
|
||||
conf.parent, importmode="prepend", rootpath=pytester.path
|
||||
)
|
||||
values = conftest._getconftestmodules(conf.parent)
|
||||
assert len(values) == 0
|
||||
assert not conftest.has_plugin(str(conf))
|
||||
# but we can still import a conftest directly
|
||||
conftest._importconftest(conf, importmode="prepend", rootpath=pytester.path)
|
||||
values = conftest._getconftestmodules(
|
||||
conf.parent, importmode="prepend", rootpath=pytester.path
|
||||
)
|
||||
values = conftest._getconftestmodules(conf.parent)
|
||||
assert values[0].__file__ is not None
|
||||
assert values[0].__file__.startswith(str(conf))
|
||||
# and all sub paths get updated properly
|
||||
values = conftest._getconftestmodules(
|
||||
p, importmode="prepend", rootpath=pytester.path
|
||||
)
|
||||
values = conftest._getconftestmodules(p)
|
||||
assert len(values) == 1
|
||||
assert values[0].__file__ is not None
|
||||
assert values[0].__file__.startswith(str(conf))
|
||||
|
@ -221,9 +199,7 @@ def test_conftestcutdir_inplace_considered(pytester: Pytester) -> None:
|
|||
conf = pytester.makeconftest("")
|
||||
conftest = PytestPluginManager()
|
||||
conftest_setinitial(conftest, [conf.parent], confcutdir=conf.parent)
|
||||
values = conftest._getconftestmodules(
|
||||
conf.parent, importmode="prepend", rootpath=pytester.path
|
||||
)
|
||||
values = conftest._getconftestmodules(conf.parent)
|
||||
assert len(values) == 1
|
||||
assert values[0].__file__ is not None
|
||||
assert values[0].__file__.startswith(str(conf))
|
||||
|
@ -433,10 +409,8 @@ def test_conftest_import_order(pytester: Pytester, monkeypatch: MonkeyPatch) ->
|
|||
conftest = PytestPluginManager()
|
||||
conftest._confcutdir = pytester.path
|
||||
monkeypatch.setattr(conftest, "_importconftest", impct)
|
||||
mods = cast(
|
||||
List[Path],
|
||||
conftest._getconftestmodules(sub, importmode="prepend", rootpath=pytester.path),
|
||||
)
|
||||
conftest._loadconftestmodules(sub, importmode="prepend", rootpath=pytester.path)
|
||||
mods = cast(List[Path], conftest._getconftestmodules(sub))
|
||||
expected = [ct1, ct2]
|
||||
assert mods == expected
|
||||
|
||||
|
|
Loading…
Reference in New Issue