Merge branch 'main' into Improvement-base-fixtureargkeys-on-param-values-not-indices

This commit is contained in:
Sadra Barikbin 2023-08-10 20:13:50 +03:30
commit 853e33c901
16 changed files with 328 additions and 253 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.0.0 rev: 6.1.0
hooks: hooks:
- id: flake8 - id: flake8
language_version: python3 language_version: python3
@ -42,7 +42,7 @@ repos:
- id: reorder-python-imports - id: reorder-python-imports
args: ['--application-directories=.:src', --py38-plus] args: ['--application-directories=.:src', --py38-plus]
- repo: https://github.com/asottile/pyupgrade - repo: https://github.com/asottile/pyupgrade
rev: v3.9.0 rev: v3.10.1
hooks: hooks:
- id: pyupgrade - id: pyupgrade
args: [--py38-plus] args: [--py38-plus]

View File

@ -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.

View File

@ -657,13 +657,16 @@ Use :func:`pytest.raises` with the
:ref:`pytest.mark.parametrize ref` decorator to write parametrized tests :ref:`pytest.mark.parametrize ref` decorator to write parametrized tests
in which some tests raise exceptions and others do not. 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`` statements
target (``e`` in the example below).
For example: For example:
.. code-block:: python .. code-block:: python
from contextlib import nullcontext as does_not_raise from contextlib import nullcontext
import pytest import pytest
@ -671,16 +674,17 @@ For example:
@pytest.mark.parametrize( @pytest.mark.parametrize(
"example_input,expectation", "example_input,expectation",
[ [
(3, does_not_raise()), (3, nullcontext(2)),
(2, does_not_raise()), (2, nullcontext(3)),
(1, does_not_raise()), (1, nullcontext(6)),
(0, pytest.raises(ZeroDivisionError)), (0, pytest.raises(ZeroDivisionError)),
], ],
) )
def test_division(example_input, expectation): def test_division(example_input, expectation):
"""Test how much I know division.""" """Test how much I know division."""
with expectation: with expectation as e:
assert (6 / example_input) is not None assert (6 / example_input) == e
In the example above, the first three test cases should run unexceptionally, In the example above, the first three test cases should run without any
while the fourth should raise ``ZeroDivisionError``. exceptions, while the fourth should raise a``ZeroDivisionError`` exception,
which is expected by pytest.

View File

@ -54,14 +54,13 @@ operators. (See :ref:`tbreportdemo`). This allows you to use the
idiomatic python constructs without boilerplate code while not losing idiomatic python constructs without boilerplate code while not losing
introspection information. 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 .. code-block:: python
assert a % 2 == 0, "value was odd, should be even" assert a % 2 == 0, "value was odd, should be even"
then no assertion introspection takes places at all and the message it is printed alongside the assertion introspection in the traceback.
will be simply shown in the traceback.
See :ref:`assert-details` for more information on assertion introspection. See :ref:`assert-details` for more information on assertion introspection.

View File

@ -82,6 +82,8 @@ pytest.exit
pytest.main pytest.main
~~~~~~~~~~~ ~~~~~~~~~~~
**Tutorial**: :ref:`pytest.main-usage`
.. autofunction:: pytest.main .. autofunction:: pytest.main
pytest.param pytest.param

View File

@ -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 other_side = right if isinstance(left, ApproxBase) else left
explanation = approx_side._repr_compare(other_side) 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) isdatacls(left) or isattrs(left) or isnamedtuple(left)
): ):
# Note: unlike dataclasses/attrs, namedtuples compare only the # Note: unlike dataclasses/attrs, namedtuples compare only the

View File

@ -581,26 +581,25 @@ class PytestPluginManager(PluginManager):
def _try_load_conftest( def _try_load_conftest(
self, anchor: Path, importmode: Union[str, ImportMode], rootpath: Path self, anchor: Path, importmode: Union[str, ImportMode], rootpath: Path
) -> None: ) -> None:
self._getconftestmodules(anchor, importmode, rootpath) self._loadconftestmodules(anchor, importmode, rootpath)
# let's also consider test* subdirs # let's also consider test* subdirs
if anchor.is_dir(): if anchor.is_dir():
for x in anchor.glob("test*"): for x in anchor.glob("test*"):
if x.is_dir(): 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 self, path: Path, importmode: Union[str, ImportMode], rootpath: Path
) -> Sequence[types.ModuleType]: ) -> None:
if self._noconftest: if self._noconftest:
return [] return
directory = self._get_directory(path) directory = self._get_directory(path)
# Optimization: avoid repeated searches in the same directory. # Optimization: avoid repeated searches in the same directory.
# Assumes always called with same importmode and rootpath. # Assumes always called with same importmode and rootpath.
existing_clist = self._dirpath2confmods.get(directory) if directory in self._dirpath2confmods:
if existing_clist is not None: return
return existing_clist
# XXX these days we may rather want to use config.rootpath # XXX these days we may rather want to use config.rootpath
# and allow users to opt into looking into the rootdir parent # and allow users to opt into looking into the rootdir parent
@ -613,16 +612,17 @@ class PytestPluginManager(PluginManager):
mod = self._importconftest(conftestpath, importmode, rootpath) mod = self._importconftest(conftestpath, importmode, rootpath)
clist.append(mod) clist.append(mod)
self._dirpath2confmods[directory] = clist 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( def _rget_with_confmod(
self, self,
name: str, name: str,
path: Path, path: Path,
importmode: Union[str, ImportMode],
rootpath: Path,
) -> Tuple[types.ModuleType, Any]: ) -> Tuple[types.ModuleType, Any]:
modules = self._getconftestmodules(path, importmode, rootpath=rootpath) modules = self._getconftestmodules(path)
for mod in reversed(modules): for mod in reversed(modules):
try: try:
return mod, getattr(mod, name) return mod, getattr(mod, name)
@ -1562,13 +1562,9 @@ class Config:
else: else:
return self._getini_unknown_type(name, type, value) return self._getini_unknown_type(name, type, value)
def _getconftest_pathlist( def _getconftest_pathlist(self, name: str, path: Path) -> Optional[List[Path]]:
self, name: str, path: Path, rootpath: Path
) -> Optional[List[Path]]:
try: try:
mod, relroots = self.pluginmanager._rget_with_confmod( mod, relroots = self.pluginmanager._rget_with_confmod(name, path)
name, path, self.getoption("importmode"), rootpath
)
except KeyError: except KeyError:
return None return None
assert mod.__file__ is not None assert mod.__file__ is not None

View File

@ -64,7 +64,6 @@ from _pytest.pathlib import bestrelpath
from _pytest.scope import _ScopeName from _pytest.scope import _ScopeName
from _pytest.scope import HIGH_SCOPES from _pytest.scope import HIGH_SCOPES
from _pytest.scope import Scope from _pytest.scope import Scope
from _pytest.stash import StashKey
if TYPE_CHECKING: if TYPE_CHECKING:
@ -148,89 +147,6 @@ def get_scope_node(
assert_never(scope) assert_never(scope)
# Used for storing artificial fixturedefs for direct parametrization.
name2pseudofixturedef_key = StashKey[Dict[str, "FixtureDef[Any]"]]()
def add_funcarg_pseudo_fixture_def(
collector: nodes.Collector, metafunc: "Metafunc", fixturemanager: "FixtureManager"
) -> None:
import _pytest.python
# This function will transform all collected calls to functions
# if they use direct funcargs (i.e. direct parametrization)
# because we want later test execution to be able to rely on
# an existing FixtureDef structure for all arguments.
# XXX we can probably avoid this algorithm if we modify CallSpec2
# to directly care for creating the fixturedefs within its methods.
if not metafunc._calls[0].funcargs:
# This function call does not have direct parametrization.
return
# Collect funcargs of all callspecs into a list of values.
arg2params: Dict[str, List[object]] = {}
arg2scope: Dict[str, Scope] = {}
for callspec in metafunc._calls:
for argname, argvalue in callspec.funcargs.items():
assert argname not in callspec.params
callspec.params[argname] = argvalue
arg2params_list = arg2params.setdefault(argname, [])
callspec.indices[argname] = len(arg2params_list)
arg2params_list.append(argvalue)
if argname not in arg2scope:
scope = callspec._arg2scope.get(argname, Scope.Function)
arg2scope[argname] = scope
callspec.funcargs.clear()
# Register artificial FixtureDef's so that later at test execution
# time we can rely on a proper FixtureDef to exist for fixture setup.
arg2fixturedefs = metafunc._arg2fixturedefs
for argname, valuelist in arg2params.items():
# If we have a scope that is higher than function, we need
# to make sure we only ever create an according fixturedef on
# a per-scope basis. We thus store and cache the fixturedef on the
# node related to the scope.
scope = arg2scope[argname]
node = None
if scope is not Scope.Function:
node = get_scope_node(collector, scope)
if node is None:
# If used class scope and there is no class, use module-level
# collector (for now).
if scope is Scope.Class:
assert isinstance(collector, _pytest.python.Module)
node = collector
# If used package scope and there is no package, use session
# (for now).
elif scope is Scope.Package:
node = collector.session
else:
assert False, f"Unhandled missing scope: {scope}"
if node is None:
name2pseudofixturedef = None
else:
default: Dict[str, FixtureDef[Any]] = {}
name2pseudofixturedef = node.stash.setdefault(
name2pseudofixturedef_key, default
)
if name2pseudofixturedef is not None and argname in name2pseudofixturedef:
arg2fixturedefs[argname] = [name2pseudofixturedef[argname]]
else:
fixturedef = FixtureDef(
fixturemanager=fixturemanager,
baseid="",
argname=argname,
func=get_direct_param_fixture_func,
scope=arg2scope[argname],
params=valuelist,
unittest=False,
ids=None,
_ispytest=True,
)
arg2fixturedefs[argname] = [fixturedef]
if name2pseudofixturedef is not None:
name2pseudofixturedef[argname] = fixturedef
def getfixturemarker(obj: object) -> Optional["FixtureFunctionMarker"]: def getfixturemarker(obj: object) -> Optional["FixtureFunctionMarker"]:
"""Return fixturemarker or None if it doesn't exist or raised """Return fixturemarker or None if it doesn't exist or raised
exceptions.""" exceptions."""
@ -271,7 +187,7 @@ def get_parametrized_fixture_keys(
pass pass
else: else:
cs: CallSpec2 = callspec 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 # sort this so that different calls to
# get_parametrized_fixture_keys will be deterministic. # get_parametrized_fixture_keys will be deterministic.
for argname in sorted(cs.indices): for argname in sorted(cs.indices):
@ -282,7 +198,7 @@ def get_parametrized_fixture_keys(
if scope is Scope.Session: if scope is Scope.Session:
scoped_item_path = None scoped_item_path = None
elif scope is Scope.Package: elif scope is Scope.Package:
scoped_item_path = item.path.parent scoped_item_path = item.path
elif scope is Scope.Module: elif scope is Scope.Module:
scoped_item_path = item.path scoped_item_path = item.path
elif scope is Scope.Class: elif scope is Scope.Class:
@ -385,10 +301,6 @@ def reorder_items_atscope(
return items_done return items_done
def get_direct_param_fixture_func(request: "FixtureRequest") -> Any:
return request.param
@dataclasses.dataclass(frozen=True) @dataclasses.dataclass(frozen=True)
class FuncFixtureInfo: class FuncFixtureInfo:
"""Fixture-related information for a fixture-requesting item (e.g. test """Fixture-related information for a fixture-requesting item (e.g. test
@ -512,7 +424,7 @@ class FixtureRequest:
node: Optional[Union[nodes.Item, nodes.Collector]] = self._pyfuncitem node: Optional[Union[nodes.Item, nodes.Collector]] = self._pyfuncitem
elif scope is Scope.Package: elif scope is Scope.Package:
# FIXME: _fixturedef is not defined on FixtureRequest (this class), # 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] node = get_scope_package(self._pyfuncitem, self._fixturedef) # type: ignore[attr-defined]
else: else:
node = get_scope_node(self._pyfuncitem, scope) node = get_scope_node(self._pyfuncitem, scope)

View File

@ -376,7 +376,7 @@ def _in_venv(path: Path) -> bool:
def pytest_ignore_collect(collection_path: Path, config: Config) -> Optional[bool]: def pytest_ignore_collect(collection_path: Path, config: Config) -> Optional[bool]:
ignore_paths = config._getconftest_pathlist( 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 [] ignore_paths = ignore_paths or []
excludeopt = config.getoption("ignore") excludeopt = config.getoption("ignore")
@ -387,7 +387,7 @@ def pytest_ignore_collect(collection_path: Path, config: Config) -> Optional[boo
return True return True
ignore_globs = config._getconftest_pathlist( 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 [] ignore_globs = ignore_globs or []
excludeglobopt = config.getoption("ignore_glob") excludeglobopt = config.getoption("ignore_glob")
@ -551,11 +551,16 @@ class Session(nodes.FSCollector):
pm = self.config.pluginmanager pm = self.config.pluginmanager
# Check if we have the common case of running # Check if we have the common case of running
# hooks with all conftest.py files. # 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, path,
self.config.getoption("importmode"), self.config.getoption("importmode"),
rootpath=self.config.rootpath, rootpath=self.config.rootpath,
) )
my_conftestmodules = pm._getconftestmodules(path)
remove_mods = pm._conftest_plugins.difference(my_conftestmodules) remove_mods = pm._conftest_plugins.difference(my_conftestmodules)
if remove_mods: if remove_mods:
# One or more conftests are not in use at this fspath. # One or more conftests are not in use at this fspath.

View File

@ -40,7 +40,6 @@ from _pytest._code.code import Traceback
from _pytest._io import TerminalWriter from _pytest._io import TerminalWriter
from _pytest._io.saferepr import saferepr from _pytest._io.saferepr import saferepr
from _pytest.compat import ascii_escaped 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_default_arg_names
from _pytest.compat import get_real_func from _pytest.compat import get_real_func
from _pytest.compat import getimfunc from _pytest.compat import getimfunc
@ -59,7 +58,10 @@ from _pytest.config.argparsing import Parser
from _pytest.deprecated import check_ispytest from _pytest.deprecated import check_ispytest
from _pytest.deprecated import INSTANCE_COLLECTOR from _pytest.deprecated import INSTANCE_COLLECTOR
from _pytest.deprecated import NOSE_SUPPORT_METHOD from _pytest.deprecated import NOSE_SUPPORT_METHOD
from _pytest.fixtures import FixtureDef
from _pytest.fixtures import FixtureRequest
from _pytest.fixtures import FuncFixtureInfo from _pytest.fixtures import FuncFixtureInfo
from _pytest.fixtures import get_scope_node
from _pytest.main import Session from _pytest.main import Session
from _pytest.mark import MARK_GEN from _pytest.mark import MARK_GEN
from _pytest.mark import ParameterSet from _pytest.mark import ParameterSet
@ -77,6 +79,7 @@ from _pytest.pathlib import parts
from _pytest.pathlib import visit from _pytest.pathlib import visit
from _pytest.scope import _ScopeName from _pytest.scope import _ScopeName
from _pytest.scope import Scope from _pytest.scope import Scope
from _pytest.stash import StashKey
from _pytest.warning_types import PytestCollectionWarning from _pytest.warning_types import PytestCollectionWarning
from _pytest.warning_types import PytestReturnNotNoneWarning from _pytest.warning_types import PytestReturnNotNoneWarning
from _pytest.warning_types import PytestUnhandledCoroutineWarning from _pytest.warning_types import PytestUnhandledCoroutineWarning
@ -493,13 +496,11 @@ class PyCollector(PyobjMixin, nodes.Collector):
if not metafunc._calls: if not metafunc._calls:
yield Function.from_parent(self, name=name, fixtureinfo=fixtureinfo) yield Function.from_parent(self, name=name, fixtureinfo=fixtureinfo)
else: else:
# Add funcargs() as fixturedefs to fixtureinfo.arg2fixturedefs. # Direct parametrizations taking place in module/class-specific
fm = self.session._fixturemanager # `metafunc.parametrize` calls may have shadowed some fixtures, so make sure
fixtures.add_funcarg_pseudo_fixture_def(self, metafunc, fm) # 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
# Add_funcarg_pseudo_fixture_def may have shadowed some fixtures # into making the closure using `ignore_args` arg to `getfixtureclosure`.
# with direct parametrization, so make sure we update what the
# function really needs.
fixtureinfo.prune_dependency_tree() fixtureinfo.prune_dependency_tree()
for callspec in metafunc._calls: for callspec in metafunc._calls:
@ -1116,11 +1117,8 @@ class CallSpec2:
and stored in item.callspec. and stored in item.callspec.
""" """
# arg name -> arg value which will be passed to the parametrized test # arg name -> arg value which will be passed to a fixture or pseudo-fixture
# function (direct parameterization). # of the same name. (indirect or direct parametrization respectively)
funcargs: Dict[str, object] = dataclasses.field(default_factory=dict)
# arg name -> arg value which will be passed to a fixture of the same name
# (indirect parametrization).
params: Dict[str, object] = dataclasses.field(default_factory=dict) params: Dict[str, object] = dataclasses.field(default_factory=dict)
# arg name -> arg index. # arg name -> arg index.
indices: Dict[str, int] = dataclasses.field(default_factory=dict) indices: Dict[str, int] = dataclasses.field(default_factory=dict)
@ -1134,7 +1132,6 @@ class CallSpec2:
def setmulti( def setmulti(
self, self,
*, *,
valtypes: Mapping[str, "Literal['params', 'funcargs']"],
argnames: Iterable[str], argnames: Iterable[str],
valset: Iterable[object], valset: Iterable[object],
id: str, id: str,
@ -1142,24 +1139,16 @@ class CallSpec2:
scope: Scope, scope: Scope,
param_index: int, param_index: int,
) -> "CallSpec2": ) -> "CallSpec2":
funcargs = self.funcargs.copy()
params = self.params.copy() params = self.params.copy()
indices = self.indices.copy() indices = self.indices.copy()
arg2scope = self._arg2scope.copy() arg2scope = self._arg2scope.copy()
for arg, val in zip(argnames, valset): for arg, val in zip(argnames, valset):
if arg in params or arg in funcargs: if arg in params:
raise ValueError(f"duplicate {arg!r}") raise ValueError(f"duplicate parametrization of {arg!r}")
valtype_for_arg = valtypes[arg]
if valtype_for_arg == "params":
params[arg] = val params[arg] = val
elif valtype_for_arg == "funcargs":
funcargs[arg] = val
else:
assert_never(valtype_for_arg)
indices[arg] = param_index indices[arg] = param_index
arg2scope[arg] = scope arg2scope[arg] = scope
return CallSpec2( return CallSpec2(
funcargs=funcargs,
params=params, params=params,
indices=indices, indices=indices,
_arg2scope=arg2scope, _arg2scope=arg2scope,
@ -1178,6 +1167,14 @@ class CallSpec2:
return "-".join(self._idlist) return "-".join(self._idlist)
def get_direct_param_fixture_func(request: FixtureRequest) -> Any:
return request.param
# Used for storing pseudo fixturedefs for direct parametrization.
name2pseudofixturedef_key = StashKey[Dict[str, FixtureDef[Any]]]()
@final @final
class Metafunc: class Metafunc:
"""Objects passed to the :hook:`pytest_generate_tests` hook. """Objects passed to the :hook:`pytest_generate_tests` hook.
@ -1239,8 +1236,9 @@ class Metafunc:
during the collection phase. If you need to setup expensive resources during the collection phase. If you need to setup expensive resources
see about setting indirect to do it rather than at test setup time. 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 Can be called multiple times per test function (but only on different
previous parametrizations, e.g. argument names), in which case each call parametrizes all previous
parametrizations, e.g.
:: ::
@ -1319,8 +1317,6 @@ class Metafunc:
self._validate_if_using_arg_names(argnames, indirect) self._validate_if_using_arg_names(argnames, indirect)
arg_values_types = self._resolve_arg_value_types(argnames, indirect)
# Use any already (possibly) generated ids with parametrize Marks. # Use any already (possibly) generated ids with parametrize Marks.
if _param_mark and _param_mark._param_ids_from: if _param_mark and _param_mark._param_ids_from:
generated_ids = _param_mark._param_ids_from._param_ids_generated generated_ids = _param_mark._param_ids_from._param_ids_generated
@ -1335,6 +1331,60 @@ class Metafunc:
if _param_mark and _param_mark._param_ids_from and generated_ids is None: if _param_mark and _param_mark._param_ids_from and generated_ids is None:
object.__setattr__(_param_mark._param_ids_from, "_param_ids_generated", ids) object.__setattr__(_param_mark._param_ids_from, "_param_ids_generated", ids)
# Add funcargs as fixturedefs to fixtureinfo.arg2fixturedefs by registering
# artificial "pseudo" FixtureDef's so that later at test execution time we can
# rely on a proper FixtureDef to exist for fixture setup.
arg2fixturedefs = self._arg2fixturedefs
node = None
# If we have a scope that is higher than function, we need
# to make sure we only ever create an according fixturedef on
# 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 = 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
# collector (for now).
if scope_ is Scope.Class:
assert isinstance(collector, _pytest.python.Module)
node = collector
# If used package scope and there is no package, use session
# (for now).
elif scope_ is Scope.Package:
node = collector.session
else:
assert False, f"Unhandled missing scope: {scope}"
if node is None:
name2pseudofixturedef = None
else:
default: Dict[str, FixtureDef[Any]] = {}
name2pseudofixturedef = node.stash.setdefault(
name2pseudofixturedef_key, default
)
arg_directness = self._resolve_args_directness(argnames, indirect)
for argname in argnames:
if arg_directness[argname] == "indirect":
continue
if name2pseudofixturedef is not None and argname in name2pseudofixturedef:
fixturedef = name2pseudofixturedef[argname]
else:
fixturedef = FixtureDef(
fixturemanager=self.definition.session._fixturemanager,
baseid="",
argname=argname,
func=get_direct_param_fixture_func,
scope=scope_,
params=None,
unittest=False,
ids=None,
_ispytest=True,
)
if name2pseudofixturedef is not None:
name2pseudofixturedef[argname] = fixturedef
arg2fixturedefs[argname] = [fixturedef]
# Create the new calls: if we are parametrize() multiple times (by applying the decorator # Create the new calls: if we are parametrize() multiple times (by applying the decorator
# more than once) then we accumulate those calls generating the cartesian product # more than once) then we accumulate those calls generating the cartesian product
# of all calls. # of all calls.
@ -1344,7 +1394,6 @@ class Metafunc:
zip(ids, parametersets) zip(ids, parametersets)
): ):
newcallspec = callspec.setmulti( newcallspec = callspec.setmulti(
valtypes=arg_values_types,
argnames=argnames, argnames=argnames,
valset=param_set.values, valset=param_set.values,
id=param_id, id=param_id,
@ -1421,28 +1470,30 @@ class Metafunc:
return list(itertools.islice(ids, num_ids)) return list(itertools.islice(ids, num_ids))
def _resolve_arg_value_types( def _resolve_args_directness(
self, self,
argnames: Sequence[str], argnames: Sequence[str],
indirect: Union[bool, Sequence[str]], indirect: Union[bool, Sequence[str]],
) -> Dict[str, "Literal['params', 'funcargs']"]: ) -> Dict[str, Literal["indirect", "direct"]]:
"""Resolve if each parametrized argument must be considered a """Resolve if each parametrized argument must be considered an indirect
parameter to a fixture or a "funcarg" to the function, based on the parameter to a fixture of the same name, or a direct parameter to the
``indirect`` parameter of the parametrized() call. parametrized function, based on the ``indirect`` parameter of the
parametrized() call.
:param List[str] argnames: List of argument names passed to ``parametrize()``. :param argnames:
:param indirect: Same as the ``indirect`` parameter of ``parametrize()``. List of argument names passed to ``parametrize()``.
:rtype: Dict[str, str] :param indirect:
A dict mapping each arg name to either: Same as the ``indirect`` parameter of ``parametrize()``.
* "params" if the argname should be the parameter of a fixture of the same name. :returns
* "funcargs" if the argname should be a parameter to the parametrized test function. A dict mapping each arg name to either "indirect" or "direct".
""" """
arg_directness: Dict[str, Literal["indirect", "direct"]]
if isinstance(indirect, bool): if isinstance(indirect, bool):
valtypes: Dict[str, Literal["params", "funcargs"]] = dict.fromkeys( arg_directness = dict.fromkeys(
argnames, "params" if indirect else "funcargs" argnames, "indirect" if indirect else "direct"
) )
elif isinstance(indirect, Sequence): elif isinstance(indirect, Sequence):
valtypes = dict.fromkeys(argnames, "funcargs") arg_directness = dict.fromkeys(argnames, "direct")
for arg in indirect: for arg in indirect:
if arg not in argnames: if arg not in argnames:
fail( fail(
@ -1451,7 +1502,7 @@ class Metafunc:
), ),
pytrace=False, pytrace=False,
) )
valtypes[arg] = "params" arg_directness[arg] = "indirect"
else: else:
fail( fail(
"In {func}: expected Sequence or boolean for indirect, got {type}".format( "In {func}: expected Sequence or boolean for indirect, got {type}".format(
@ -1459,7 +1510,7 @@ class Metafunc:
), ),
pytrace=False, pytrace=False,
) )
return valtypes return arg_directness
def _validate_if_using_arg_names( def _validate_if_using_arg_names(
self, self,
@ -1516,7 +1567,7 @@ def _find_parametrized_scope(
if all_arguments_are_fixtures: if all_arguments_are_fixtures:
fixturedefs = arg2fixturedefs or {} fixturedefs = arg2fixturedefs or {}
used_scopes = [ used_scopes = [
fixturedef[0]._scope fixturedef[-1]._scope
for name, fixturedef in fixturedefs.items() for name, fixturedef in fixturedefs.items()
if name in argnames if name in argnames
] ]
@ -1682,7 +1733,7 @@ class Function(PyobjMixin, nodes.Item):
:param config: :param config:
The pytest Config object. The pytest Config object.
:param callspec: :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. meta information about the parametrization.
:param callobj: :param callobj:
If given, the object which will be called when the Function is invoked, If given, the object which will be called when the Function is invoked,

View File

@ -1573,4 +1573,4 @@ class TestBinaryAndTextMethods:
x.write_text(part, "ascii") x.write_text(part, "ascii")
s = x.read_text("ascii") s = x.read_text("ascii")
assert s == part assert s == part
assert type(s) == type(part) assert type(s) is type(part)

View File

@ -1,5 +1,5 @@
anyio[curio,trio]==3.7.1 anyio[curio,trio]==3.7.1
django==4.2.3 django==4.2.4
pytest-asyncio==0.21.1 pytest-asyncio==0.21.1
pytest-bdd==6.1.1 pytest-bdd==6.1.1
pytest-cov==4.1.0 pytest-cov==4.1.0

View File

@ -2104,9 +2104,7 @@ class TestAutouseManagement:
reprec = pytester.inline_run("-v", "-s", "--confcutdir", pytester.path) reprec = pytester.inline_run("-v", "-s", "--confcutdir", pytester.path)
reprec.assertoutcome(passed=8) reprec.assertoutcome(passed=8)
config = reprec.getcalls("pytest_unconfigure")[0].config config = reprec.getcalls("pytest_unconfigure")[0].config
values = config.pluginmanager._getconftestmodules( values = config.pluginmanager._getconftestmodules(p)[0].values
p, importmode="prepend", rootpath=pytester.path
)[0].values
assert values == ["fin_a1", "fin_a2", "fin_b1", "fin_b2"] * 2 assert values == ["fin_a1", "fin_a2", "fin_b1", "fin_b2"] * 2
def test_scope_ordering(self, pytester: Pytester) -> None: def test_scope_ordering(self, pytester: Pytester) -> None:

View File

@ -23,6 +23,7 @@ from _pytest.compat import getfuncargnames
from _pytest.compat import NOTSET from _pytest.compat import NOTSET
from _pytest.outcomes import fail from _pytest.outcomes import fail
from _pytest.pytester import Pytester from _pytest.pytester import Pytester
from _pytest.python import Function
from _pytest.python import IdMaker from _pytest.python import IdMaker
from _pytest.scope import Scope from _pytest.scope import Scope
@ -33,11 +34,19 @@ class TestMetafunc:
# on the funcarg level, so we don't need a full blown # on the funcarg level, so we don't need a full blown
# initialization. # initialization.
class FuncFixtureInfoMock: class FuncFixtureInfoMock:
name2fixturedefs = None name2fixturedefs: Dict[str, List[fixtures.FixtureDef[object]]] = {}
def __init__(self, names): def __init__(self, names):
self.names_closure = names self.names_closure = names
@dataclasses.dataclass
class FixtureManagerMock:
config: Any
@dataclasses.dataclass
class SessionMock:
_fixturemanager: FixtureManagerMock
@dataclasses.dataclass @dataclasses.dataclass
class DefinitionMock(python.FunctionDefinition): class DefinitionMock(python.FunctionDefinition):
_nodeid: str _nodeid: str
@ -46,6 +55,8 @@ class TestMetafunc:
names = getfuncargnames(func) names = getfuncargnames(func)
fixtureinfo: Any = FuncFixtureInfoMock(names) fixtureinfo: Any = FuncFixtureInfoMock(names)
definition: Any = DefinitionMock._create(obj=func, _nodeid="mock::nodeid") definition: Any = DefinitionMock._create(obj=func, _nodeid="mock::nodeid")
definition._fixtureinfo = fixtureinfo
definition.session = SessionMock(FixtureManagerMock({}))
return python.Metafunc(definition, fixtureinfo, config, _ispytest=True) return python.Metafunc(definition, fixtureinfo, config, _ispytest=True)
def test_no_funcargs(self) -> None: def test_no_funcargs(self) -> None:
@ -98,7 +109,7 @@ class TestMetafunc:
# When the input is an iterator, only len(args) are taken, # When the input is an iterator, only len(args) are taken,
# so the bad Exc isn't reached. # so the bad Exc isn't reached.
metafunc.parametrize("x", [1, 2], ids=gen()) # type: ignore[arg-type] metafunc.parametrize("x", [1, 2], ids=gen()) # type: ignore[arg-type]
assert [(x.funcargs, x.id) for x in metafunc._calls] == [ assert [(x.params, x.id) for x in metafunc._calls] == [
({"x": 1}, "0"), ({"x": 1}, "0"),
({"x": 2}, "2"), ({"x": 2}, "2"),
] ]
@ -151,6 +162,7 @@ class TestMetafunc:
module_fix=[DummyFixtureDef(Scope.Module)], module_fix=[DummyFixtureDef(Scope.Module)],
class_fix=[DummyFixtureDef(Scope.Class)], class_fix=[DummyFixtureDef(Scope.Class)],
func_fix=[DummyFixtureDef(Scope.Function)], func_fix=[DummyFixtureDef(Scope.Function)],
mixed_fix=[DummyFixtureDef(Scope.Module), DummyFixtureDef(Scope.Class)],
), ),
) )
@ -187,6 +199,7 @@ class TestMetafunc:
) )
== Scope.Module == Scope.Module
) )
assert find_scope(["mixed_fix"], indirect=True) == Scope.Class
def test_parametrize_and_id(self) -> None: def test_parametrize_and_id(self) -> None:
def func(x, y): def func(x, y):
@ -712,8 +725,6 @@ class TestMetafunc:
metafunc.parametrize("x", [1], indirect=True) metafunc.parametrize("x", [1], indirect=True)
metafunc.parametrize("y", [2, 3], indirect=True) metafunc.parametrize("y", [2, 3], indirect=True)
assert len(metafunc._calls) == 2 assert len(metafunc._calls) == 2
assert metafunc._calls[0].funcargs == {}
assert metafunc._calls[1].funcargs == {}
assert metafunc._calls[0].params == dict(x=1, y=2) assert metafunc._calls[0].params == dict(x=1, y=2)
assert metafunc._calls[1].params == dict(x=1, y=3) assert metafunc._calls[1].params == dict(x=1, y=3)
@ -725,8 +736,10 @@ class TestMetafunc:
metafunc = self.Metafunc(func) metafunc = self.Metafunc(func)
metafunc.parametrize("x, y", [("a", "b")], indirect=["x"]) metafunc.parametrize("x, y", [("a", "b")], indirect=["x"])
assert metafunc._calls[0].funcargs == dict(y="b") assert metafunc._calls[0].params == dict(x="a", y="b")
assert metafunc._calls[0].params == dict(x="a") # Since `y` is a direct parameter, its pseudo-fixture would
# be registered.
assert list(metafunc._arg2fixturedefs.keys()) == ["y"]
def test_parametrize_indirect_list_all(self) -> None: def test_parametrize_indirect_list_all(self) -> None:
"""#714""" """#714"""
@ -736,8 +749,8 @@ class TestMetafunc:
metafunc = self.Metafunc(func) metafunc = self.Metafunc(func)
metafunc.parametrize("x, y", [("a", "b")], indirect=["x", "y"]) metafunc.parametrize("x, y", [("a", "b")], indirect=["x", "y"])
assert metafunc._calls[0].funcargs == {}
assert metafunc._calls[0].params == dict(x="a", y="b") assert metafunc._calls[0].params == dict(x="a", y="b")
assert list(metafunc._arg2fixturedefs.keys()) == []
def test_parametrize_indirect_list_empty(self) -> None: def test_parametrize_indirect_list_empty(self) -> None:
"""#714""" """#714"""
@ -747,8 +760,8 @@ class TestMetafunc:
metafunc = self.Metafunc(func) metafunc = self.Metafunc(func)
metafunc.parametrize("x, y", [("a", "b")], indirect=[]) metafunc.parametrize("x, y", [("a", "b")], indirect=[])
assert metafunc._calls[0].funcargs == dict(x="a", y="b") assert metafunc._calls[0].params == dict(x="a", y="b")
assert metafunc._calls[0].params == {} assert list(metafunc._arg2fixturedefs.keys()) == ["x", "y"]
def test_parametrize_indirect_wrong_type(self) -> None: def test_parametrize_indirect_wrong_type(self) -> None:
def func(x, y): def func(x, y):
@ -942,9 +955,9 @@ class TestMetafunc:
metafunc = self.Metafunc(lambda x: None) metafunc = self.Metafunc(lambda x: None)
metafunc.parametrize("x", [1, 2]) metafunc.parametrize("x", [1, 2])
assert len(metafunc._calls) == 2 assert len(metafunc._calls) == 2
assert metafunc._calls[0].funcargs == dict(x=1) assert metafunc._calls[0].params == dict(x=1)
assert metafunc._calls[0].id == "1" assert metafunc._calls[0].id == "1"
assert metafunc._calls[1].funcargs == dict(x=2) assert metafunc._calls[1].params == dict(x=2)
assert metafunc._calls[1].id == "2" assert metafunc._calls[1].id == "2"
def test_parametrize_onearg_indirect(self) -> None: def test_parametrize_onearg_indirect(self) -> None:
@ -959,11 +972,45 @@ class TestMetafunc:
metafunc = self.Metafunc(lambda x, y: None) metafunc = self.Metafunc(lambda x, y: None)
metafunc.parametrize(("x", "y"), [(1, 2), (3, 4)]) metafunc.parametrize(("x", "y"), [(1, 2), (3, 4)])
assert len(metafunc._calls) == 2 assert len(metafunc._calls) == 2
assert metafunc._calls[0].funcargs == dict(x=1, y=2) assert metafunc._calls[0].params == dict(x=1, y=2)
assert metafunc._calls[0].id == "1-2" assert metafunc._calls[0].id == "1-2"
assert metafunc._calls[1].funcargs == dict(x=3, y=4) assert metafunc._calls[1].params == dict(x=3, y=4)
assert metafunc._calls[1].id == "3-4" assert metafunc._calls[1].id == "3-4"
def test_high_scoped_parametrize_reordering(self, pytester: Pytester) -> None:
pytester.makepyfile(
"""
import pytest
@pytest.mark.parametrize("arg2", [3, 4])
@pytest.mark.parametrize("arg1", [0, 1, 2], scope='module')
def test1(arg1, arg2):
pass
def test2():
pass
@pytest.mark.parametrize("arg1", [0, 1, 2], scope='module')
def test3(arg1):
pass
"""
)
result = pytester.runpytest("--collect-only")
result.stdout.re_match_lines(
[
r" <Function test1\[0-3\]>",
r" <Function test1\[0-4\]>",
r" <Function test3\[0\]>",
r" <Function test1\[1-3\]>",
r" <Function test1\[1-4\]>",
r" <Function test3\[1\]>",
r" <Function test1\[2-3\]>",
r" <Function test1\[2-4\]>",
r" <Function test3\[2\]>",
r" <Function test2>",
]
)
def test_parametrize_multiple_times(self, pytester: Pytester) -> None: def test_parametrize_multiple_times(self, pytester: Pytester) -> None:
pytester.makepyfile( pytester.makepyfile(
""" """
@ -1503,6 +1550,98 @@ class TestMetafuncFunctional:
result = pytester.runpytest() result = pytester.runpytest()
assert result.ret == 0 assert result.ret == 0
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(scope="module")
def fixture0():
pass
@pytest.fixture(scope="module")
def fixture1(fixture0):
pass
@pytest.mark.parametrize("fixture1", [0], indirect=True)
def test_0(fixture1):
pass
@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("-v")
assert result.ret == 0
result.stdout.fnmatch_lines(
[
"*test_0*",
"*test_1*",
"*test_2*",
"*test_3*",
]
)
class TestMetafuncFunctionalAuto: class TestMetafuncFunctionalAuto:
"""Tests related to automatically find out the correct scope for """Tests related to automatically find out the correct scope for

View File

@ -642,18 +642,11 @@ class TestConfigAPI:
p = tmp_path.joinpath("conftest.py") p = tmp_path.joinpath("conftest.py")
p.write_text(f"mylist = {['.', str(somepath)]}", encoding="utf-8") p.write_text(f"mylist = {['.', str(somepath)]}", encoding="utf-8")
config = pytester.parseconfigure(p) config = pytester.parseconfigure(p)
assert ( assert config._getconftest_pathlist("notexist", path=tmp_path) is None
config._getconftest_pathlist("notexist", path=tmp_path, rootpath=tmp_path) assert config._getconftest_pathlist("mylist", path=tmp_path) == [
is None tmp_path,
) somepath,
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
@pytest.mark.parametrize("maybe_type", ["not passed", "None", '"string"']) @pytest.mark.parametrize("maybe_type", ["not passed", "None", '"string"'])
def test_addini(self, pytester: Pytester, maybe_type: str) -> None: def test_addini(self, pytester: Pytester, maybe_type: str) -> None:

View File

@ -62,28 +62,22 @@ class TestConftestValueAccessGlobal:
def test_basic_init(self, basedir: Path) -> None: def test_basic_init(self, basedir: Path) -> None:
conftest = PytestPluginManager() conftest = PytestPluginManager()
p = basedir / "adir" p = basedir / "adir"
assert ( conftest._loadconftestmodules(p, importmode="prepend", rootpath=basedir)
conftest._rget_with_confmod("a", p, importmode="prepend", rootpath=basedir)[ assert conftest._rget_with_confmod("a", p)[1] == 1
1
]
== 1
)
def test_immediate_initialiation_and_incremental_are_the_same( def test_immediate_initialiation_and_incremental_are_the_same(
self, basedir: Path self, basedir: Path
) -> None: ) -> None:
conftest = PytestPluginManager() conftest = PytestPluginManager()
assert not len(conftest._dirpath2confmods) assert not len(conftest._dirpath2confmods)
conftest._getconftestmodules( conftest._loadconftestmodules(basedir, importmode="prepend", rootpath=basedir)
basedir, importmode="prepend", rootpath=Path(basedir)
)
snap1 = len(conftest._dirpath2confmods) snap1 = len(conftest._dirpath2confmods)
assert snap1 == 1 assert snap1 == 1
conftest._getconftestmodules( conftest._loadconftestmodules(
basedir / "adir", importmode="prepend", rootpath=basedir basedir / "adir", importmode="prepend", rootpath=basedir
) )
assert len(conftest._dirpath2confmods) == snap1 + 1 assert len(conftest._dirpath2confmods) == snap1 + 1
conftest._getconftestmodules( conftest._loadconftestmodules(
basedir / "b", importmode="prepend", rootpath=basedir basedir / "b", importmode="prepend", rootpath=basedir
) )
assert len(conftest._dirpath2confmods) == snap1 + 2 assert len(conftest._dirpath2confmods) == snap1 + 2
@ -91,33 +85,23 @@ class TestConftestValueAccessGlobal:
def test_value_access_not_existing(self, basedir: Path) -> None: def test_value_access_not_existing(self, basedir: Path) -> None:
conftest = ConftestWithSetinitial(basedir) conftest = ConftestWithSetinitial(basedir)
with pytest.raises(KeyError): with pytest.raises(KeyError):
conftest._rget_with_confmod( conftest._rget_with_confmod("a", basedir)
"a", basedir, importmode="prepend", rootpath=Path(basedir)
)
def test_value_access_by_path(self, basedir: Path) -> None: def test_value_access_by_path(self, basedir: Path) -> None:
conftest = ConftestWithSetinitial(basedir) conftest = ConftestWithSetinitial(basedir)
adir = basedir / "adir" adir = basedir / "adir"
assert ( conftest._loadconftestmodules(adir, importmode="prepend", rootpath=basedir)
conftest._rget_with_confmod( assert conftest._rget_with_confmod("a", adir)[1] == 1
"a", adir, importmode="prepend", rootpath=basedir conftest._loadconftestmodules(
)[1] adir / "b", importmode="prepend", rootpath=basedir
== 1
)
assert (
conftest._rget_with_confmod(
"a", adir / "b", importmode="prepend", rootpath=basedir
)[1]
== 1.5
) )
assert conftest._rget_with_confmod("a", adir / "b")[1] == 1.5
def test_value_access_with_confmod(self, basedir: Path) -> None: def test_value_access_with_confmod(self, basedir: Path) -> None:
startdir = basedir / "adir" / "b" startdir = basedir / "adir" / "b"
startdir.joinpath("xx").mkdir() startdir.joinpath("xx").mkdir()
conftest = ConftestWithSetinitial(startdir) conftest = ConftestWithSetinitial(startdir)
mod, value = conftest._rget_with_confmod( mod, value = conftest._rget_with_confmod("a", startdir)
"a", startdir, importmode="prepend", rootpath=Path(basedir)
)
assert value == 1.5 assert value == 1.5
assert mod.__file__ is not None assert mod.__file__ is not None
path = Path(mod.__file__) path = Path(mod.__file__)
@ -143,9 +127,7 @@ def test_doubledash_considered(pytester: Pytester) -> None:
conf.joinpath("conftest.py").touch() conf.joinpath("conftest.py").touch()
conftest = PytestPluginManager() conftest = PytestPluginManager()
conftest_setinitial(conftest, [conf.name, conf.name]) conftest_setinitial(conftest, [conf.name, conf.name])
values = conftest._getconftestmodules( values = conftest._getconftestmodules(conf)
conf, importmode="prepend", rootpath=pytester.path
)
assert len(values) == 1 assert len(values) == 1
@ -192,26 +174,22 @@ def test_conftestcutdir(pytester: Pytester) -> None:
p = pytester.mkdir("x") p = pytester.mkdir("x")
conftest = PytestPluginManager() conftest = PytestPluginManager()
conftest_setinitial(conftest, [pytester.path], confcutdir=p) conftest_setinitial(conftest, [pytester.path], confcutdir=p)
values = conftest._getconftestmodules( conftest._loadconftestmodules(p, importmode="prepend", rootpath=pytester.path)
p, importmode="prepend", rootpath=pytester.path values = conftest._getconftestmodules(p)
)
assert len(values) == 0 assert len(values) == 0
values = conftest._getconftestmodules( conftest._loadconftestmodules(
conf.parent, importmode="prepend", rootpath=pytester.path conf.parent, importmode="prepend", rootpath=pytester.path
) )
values = conftest._getconftestmodules(conf.parent)
assert len(values) == 0 assert len(values) == 0
assert not conftest.has_plugin(str(conf)) assert not conftest.has_plugin(str(conf))
# but we can still import a conftest directly # but we can still import a conftest directly
conftest._importconftest(conf, importmode="prepend", rootpath=pytester.path) conftest._importconftest(conf, importmode="prepend", rootpath=pytester.path)
values = conftest._getconftestmodules( values = conftest._getconftestmodules(conf.parent)
conf.parent, importmode="prepend", rootpath=pytester.path
)
assert values[0].__file__ is not None assert values[0].__file__ is not None
assert values[0].__file__.startswith(str(conf)) assert values[0].__file__.startswith(str(conf))
# and all sub paths get updated properly # and all sub paths get updated properly
values = conftest._getconftestmodules( values = conftest._getconftestmodules(p)
p, importmode="prepend", rootpath=pytester.path
)
assert len(values) == 1 assert len(values) == 1
assert values[0].__file__ is not None assert values[0].__file__ is not None
assert values[0].__file__.startswith(str(conf)) assert values[0].__file__.startswith(str(conf))
@ -221,9 +199,7 @@ def test_conftestcutdir_inplace_considered(pytester: Pytester) -> None:
conf = pytester.makeconftest("") conf = pytester.makeconftest("")
conftest = PytestPluginManager() conftest = PytestPluginManager()
conftest_setinitial(conftest, [conf.parent], confcutdir=conf.parent) conftest_setinitial(conftest, [conf.parent], confcutdir=conf.parent)
values = conftest._getconftestmodules( values = conftest._getconftestmodules(conf.parent)
conf.parent, importmode="prepend", rootpath=pytester.path
)
assert len(values) == 1 assert len(values) == 1
assert values[0].__file__ is not None assert values[0].__file__ is not None
assert values[0].__file__.startswith(str(conf)) assert values[0].__file__.startswith(str(conf))
@ -433,10 +409,8 @@ def test_conftest_import_order(pytester: Pytester, monkeypatch: MonkeyPatch) ->
conftest = PytestPluginManager() conftest = PytestPluginManager()
conftest._confcutdir = pytester.path conftest._confcutdir = pytester.path
monkeypatch.setattr(conftest, "_importconftest", impct) monkeypatch.setattr(conftest, "_importconftest", impct)
mods = cast( conftest._loadconftestmodules(sub, importmode="prepend", rootpath=pytester.path)
List[Path], mods = cast(List[Path], conftest._getconftestmodules(sub))
conftest._getconftestmodules(sub, importmode="prepend", rootpath=pytester.path),
)
expected = [ct1, ct2] expected = [ct1, ct2]
assert mods == expected assert mods == expected