Skip to content

Commit da499bb

Browse files
Merge branch 'main' into Improvement-catch-duplicate-values-when-determining-param-indices-in-metafunc-parametrize
2 parents ae5849f + 556e075 commit da499bb

File tree

16 files changed

+219
-162
lines changed

16 files changed

+219
-162
lines changed

.pre-commit-config.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ repos:
2929
language: python
3030
files: \.py$
3131
- repo: https://github.com/PyCQA/flake8
32-
rev: 6.0.0
32+
rev: 6.1.0
3333
hooks:
3434
- id: flake8
3535
language_version: python3
@@ -42,7 +42,7 @@ repos:
4242
- id: reorder-python-imports
4343
args: ['--application-directories=.:src', --py38-plus]
4444
- repo: https://github.com/asottile/pyupgrade
45-
rev: v3.9.0
45+
rev: v3.10.1
4646
hooks:
4747
- id: pyupgrade
4848
args: [--py38-plus]

changelog/11277.bugfix.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fixed a bug that when there are multiple fixtures for an indirect parameter,
2+
the scope of the highest-scope fixture is picked for the parameter set, instead of that of the one with the narrowest scope.

doc/en/example/parametrize.rst

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -657,30 +657,34 @@ Use :func:`pytest.raises` with the
657657
:ref:`pytest.mark.parametrize ref` decorator to write parametrized tests
658658
in which some tests raise exceptions and others do not.
659659

660-
It may be helpful to use ``nullcontext`` as a complement to ``raises``.
660+
``contextlib.nullcontext`` can be used to test cases that are not expected to
661+
raise exceptions but that should result in some value. The value is given as the
662+
``enter_result`` parameter, which will be available as the ``with`` statement’s
663+
target (``e`` in the example below).
661664

662665
For example:
663666

664667
.. code-block:: python
665668
666-
from contextlib import nullcontext as does_not_raise
669+
from contextlib import nullcontext
667670
668671
import pytest
669672
670673
671674
@pytest.mark.parametrize(
672675
"example_input,expectation",
673676
[
674-
(3, does_not_raise()),
675-
(2, does_not_raise()),
676-
(1, does_not_raise()),
677+
(3, nullcontext(2)),
678+
(2, nullcontext(3)),
679+
(1, nullcontext(6)),
677680
(0, pytest.raises(ZeroDivisionError)),
678681
],
679682
)
680683
def test_division(example_input, expectation):
681684
"""Test how much I know division."""
682-
with expectation:
683-
assert (6 / example_input) is not None
685+
with expectation as e:
686+
assert (6 / example_input) == e
684687
685-
In the example above, the first three test cases should run unexceptionally,
686-
while the fourth should raise ``ZeroDivisionError``.
688+
In the example above, the first three test cases should run without any
689+
exceptions, while the fourth should raise a``ZeroDivisionError`` exception,
690+
which is expected by pytest.

doc/en/how-to/assert.rst

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,14 +54,13 @@ operators. (See :ref:`tbreportdemo`). This allows you to use the
5454
idiomatic python constructs without boilerplate code while not losing
5555
introspection information.
5656

57-
However, if you specify a message with the assertion like this:
57+
If a message is specified with the assertion like this:
5858

5959
.. code-block:: python
6060
6161
assert a % 2 == 0, "value was odd, should be even"
6262
63-
then no assertion introspection takes places at all and the message
64-
will be simply shown in the traceback.
63+
it is printed alongside the assertion introspection in the traceback.
6564

6665
See :ref:`assert-details` for more information on assertion introspection.
6766

doc/en/reference/reference.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@ pytest.exit
8282
pytest.main
8383
~~~~~~~~~~~
8484

85+
**Tutorial**: :ref:`pytest.main-usage`
86+
8587
.. autofunction:: pytest.main
8688

8789
pytest.param

src/_pytest/assertion/util.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ def _compare_eq_any(left: Any, right: Any, verbose: int = 0) -> List[str]:
222222
other_side = right if isinstance(left, ApproxBase) else left
223223

224224
explanation = approx_side._repr_compare(other_side)
225-
elif type(left) == type(right) and (
225+
elif type(left) is type(right) and (
226226
isdatacls(left) or isattrs(left) or isnamedtuple(left)
227227
):
228228
# Note: unlike dataclasses/attrs, namedtuples compare only the

src/_pytest/config/__init__.py

Lines changed: 14 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -581,26 +581,25 @@ def _is_in_confcutdir(self, path: Path) -> bool:
581581
def _try_load_conftest(
582582
self, anchor: Path, importmode: Union[str, ImportMode], rootpath: Path
583583
) -> None:
584-
self._getconftestmodules(anchor, importmode, rootpath)
584+
self._loadconftestmodules(anchor, importmode, rootpath)
585585
# let's also consider test* subdirs
586586
if anchor.is_dir():
587587
for x in anchor.glob("test*"):
588588
if x.is_dir():
589-
self._getconftestmodules(x, importmode, rootpath)
589+
self._loadconftestmodules(x, importmode, rootpath)
590590

591-
def _getconftestmodules(
591+
def _loadconftestmodules(
592592
self, path: Path, importmode: Union[str, ImportMode], rootpath: Path
593-
) -> Sequence[types.ModuleType]:
593+
) -> None:
594594
if self._noconftest:
595-
return []
595+
return
596596

597597
directory = self._get_directory(path)
598598

599599
# Optimization: avoid repeated searches in the same directory.
600600
# Assumes always called with same importmode and rootpath.
601-
existing_clist = self._dirpath2confmods.get(directory)
602-
if existing_clist is not None:
603-
return existing_clist
601+
if directory in self._dirpath2confmods:
602+
return
604603

605604
# XXX these days we may rather want to use config.rootpath
606605
# and allow users to opt into looking into the rootdir parent
@@ -613,16 +612,17 @@ def _getconftestmodules(
613612
mod = self._importconftest(conftestpath, importmode, rootpath)
614613
clist.append(mod)
615614
self._dirpath2confmods[directory] = clist
616-
return clist
615+
616+
def _getconftestmodules(self, path: Path) -> Sequence[types.ModuleType]:
617+
directory = self._get_directory(path)
618+
return self._dirpath2confmods.get(directory, ())
617619

618620
def _rget_with_confmod(
619621
self,
620622
name: str,
621623
path: Path,
622-
importmode: Union[str, ImportMode],
623-
rootpath: Path,
624624
) -> Tuple[types.ModuleType, Any]:
625-
modules = self._getconftestmodules(path, importmode, rootpath=rootpath)
625+
modules = self._getconftestmodules(path)
626626
for mod in reversed(modules):
627627
try:
628628
return mod, getattr(mod, name)
@@ -1562,13 +1562,9 @@ def _getini(self, name: str):
15621562
else:
15631563
return self._getini_unknown_type(name, type, value)
15641564

1565-
def _getconftest_pathlist(
1566-
self, name: str, path: Path, rootpath: Path
1567-
) -> Optional[List[Path]]:
1565+
def _getconftest_pathlist(self, name: str, path: Path) -> Optional[List[Path]]:
15681566
try:
1569-
mod, relroots = self.pluginmanager._rget_with_confmod(
1570-
name, path, self.getoption("importmode"), rootpath
1571-
)
1567+
mod, relroots = self.pluginmanager._rget_with_confmod(name, path)
15721568
except KeyError:
15731569
return None
15741570
assert mod.__file__ is not None

src/_pytest/fixtures.py

Lines changed: 30 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -155,11 +155,17 @@ def getfixturemarker(obj: object) -> Optional["FixtureFunctionMarker"]:
155155
)
156156

157157

158-
# Parametrized fixture key, helper alias for code below.
159-
_Key = Tuple[object, ...]
158+
@dataclasses.dataclass(frozen=True)
159+
class FixtureArgKey:
160+
argname: str
161+
param_index: int
162+
scoped_item_path: Optional[Path]
163+
item_cls: Optional[type]
160164

161165

162-
def get_parametrized_fixture_keys(item: nodes.Item, scope: Scope) -> Iterator[_Key]:
166+
def get_parametrized_fixture_keys(
167+
item: nodes.Item, scope: Scope
168+
) -> Iterator[FixtureArgKey]:
163169
"""Return list of keys for all parametrized arguments which match
164170
the specified scope."""
165171
assert scope is not Scope.Function
@@ -169,24 +175,28 @@ def get_parametrized_fixture_keys(item: nodes.Item, scope: Scope) -> Iterator[_K
169175
pass
170176
else:
171177
cs: CallSpec2 = callspec
172-
# cs.indices.items() is random order of argnames. Need to
178+
# cs.indices is random order of argnames. Need to
173179
# sort this so that different calls to
174180
# get_parametrized_fixture_keys will be deterministic.
175-
for argname, param_index in sorted(cs.indices.items()):
181+
for argname in sorted(cs.indices):
176182
if cs._arg2scope[argname] != scope:
177183
continue
184+
185+
item_cls = None
178186
if scope is Scope.Session:
179-
key: _Key = (argname, param_index)
187+
scoped_item_path = None
180188
elif scope is Scope.Package:
181-
key = (argname, param_index, item.path)
189+
scoped_item_path = item.path
182190
elif scope is Scope.Module:
183-
key = (argname, param_index, item.path)
191+
scoped_item_path = item.path
184192
elif scope is Scope.Class:
193+
scoped_item_path = item.path
185194
item_cls = item.cls # type: ignore[attr-defined]
186-
key = (argname, param_index, item.path, item_cls)
187195
else:
188196
assert_never(scope)
189-
yield key
197+
198+
param_index = cs.indices[argname]
199+
yield FixtureArgKey(argname, param_index, scoped_item_path, item_cls)
190200

191201

192202
# 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
196206

197207

198208
def reorder_items(items: Sequence[nodes.Item]) -> List[nodes.Item]:
199-
argkeys_cache: Dict[Scope, Dict[nodes.Item, Dict[_Key, None]]] = {}
200-
items_by_argkey: Dict[Scope, Dict[_Key, Deque[nodes.Item]]] = {}
209+
argkeys_cache: Dict[Scope, Dict[nodes.Item, Dict[FixtureArgKey, None]]] = {}
210+
items_by_argkey: Dict[Scope, Dict[FixtureArgKey, Deque[nodes.Item]]] = {}
201211
for scope in HIGH_SCOPES:
202-
d: Dict[nodes.Item, Dict[_Key, None]] = {}
212+
d: Dict[nodes.Item, Dict[FixtureArgKey, None]] = {}
203213
argkeys_cache[scope] = d
204-
item_d: Dict[_Key, Deque[nodes.Item]] = defaultdict(deque)
214+
item_d: Dict[FixtureArgKey, Deque[nodes.Item]] = defaultdict(deque)
205215
items_by_argkey[scope] = item_d
206216
for item in items:
207217
keys = dict.fromkeys(get_parametrized_fixture_keys(item, scope), None)
@@ -217,8 +227,8 @@ def reorder_items(items: Sequence[nodes.Item]) -> List[nodes.Item]:
217227

218228
def fix_cache_order(
219229
item: nodes.Item,
220-
argkeys_cache: Dict[Scope, Dict[nodes.Item, Dict[_Key, None]]],
221-
items_by_argkey: Dict[Scope, Dict[_Key, "Deque[nodes.Item]"]],
230+
argkeys_cache: Dict[Scope, Dict[nodes.Item, Dict[FixtureArgKey, None]]],
231+
items_by_argkey: Dict[Scope, Dict[FixtureArgKey, "Deque[nodes.Item]"]],
222232
) -> None:
223233
for scope in HIGH_SCOPES:
224234
for key in argkeys_cache[scope].get(item, []):
@@ -227,13 +237,13 @@ def fix_cache_order(
227237

228238
def reorder_items_atscope(
229239
items: Dict[nodes.Item, None],
230-
argkeys_cache: Dict[Scope, Dict[nodes.Item, Dict[_Key, None]]],
231-
items_by_argkey: Dict[Scope, Dict[_Key, "Deque[nodes.Item]"]],
240+
argkeys_cache: Dict[Scope, Dict[nodes.Item, Dict[FixtureArgKey, None]]],
241+
items_by_argkey: Dict[Scope, Dict[FixtureArgKey, "Deque[nodes.Item]"]],
232242
scope: Scope,
233243
) -> Dict[nodes.Item, None]:
234244
if scope is Scope.Function or len(items) < 3:
235245
return items
236-
ignore: Set[Optional[_Key]] = set()
246+
ignore: Set[Optional[FixtureArgKey]] = set()
237247
items_deque = deque(items)
238248
items_done: Dict[nodes.Item, None] = {}
239249
scoped_items_by_argkey = items_by_argkey[scope]
@@ -394,7 +404,7 @@ def node(self):
394404
node: Optional[Union[nodes.Item, nodes.Collector]] = self._pyfuncitem
395405
elif scope is Scope.Package:
396406
# FIXME: _fixturedef is not defined on FixtureRequest (this class),
397-
# but on FixtureRequest (a subclass).
407+
# but on SubRequest (a subclass).
398408
node = get_scope_package(self._pyfuncitem, self._fixturedef) # type: ignore[attr-defined]
399409
else:
400410
node = get_scope_node(self._pyfuncitem, scope)

src/_pytest/main.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -376,7 +376,7 @@ def _in_venv(path: Path) -> bool:
376376

377377
def pytest_ignore_collect(collection_path: Path, config: Config) -> Optional[bool]:
378378
ignore_paths = config._getconftest_pathlist(
379-
"collect_ignore", path=collection_path.parent, rootpath=config.rootpath
379+
"collect_ignore", path=collection_path.parent
380380
)
381381
ignore_paths = ignore_paths or []
382382
excludeopt = config.getoption("ignore")
@@ -387,7 +387,7 @@ def pytest_ignore_collect(collection_path: Path, config: Config) -> Optional[boo
387387
return True
388388

389389
ignore_globs = config._getconftest_pathlist(
390-
"collect_ignore_glob", path=collection_path.parent, rootpath=config.rootpath
390+
"collect_ignore_glob", path=collection_path.parent
391391
)
392392
ignore_globs = ignore_globs or []
393393
excludeglobopt = config.getoption("ignore_glob")
@@ -551,11 +551,16 @@ def gethookproxy(self, fspath: "os.PathLike[str]"):
551551
pm = self.config.pluginmanager
552552
# Check if we have the common case of running
553553
# hooks with all conftest.py files.
554-
my_conftestmodules = pm._getconftestmodules(
554+
#
555+
# TODO: pytest relies on this call to load non-initial conftests. This
556+
# is incidental. It will be better to load conftests at a more
557+
# well-defined place.
558+
pm._loadconftestmodules(
555559
path,
556560
self.config.getoption("importmode"),
557561
rootpath=self.config.rootpath,
558562
)
563+
my_conftestmodules = pm._getconftestmodules(path)
559564
remove_mods = pm._conftest_plugins.difference(my_conftestmodules)
560565
if remove_mods:
561566
# One or more conftests are not in use at this fspath.

0 commit comments

Comments
 (0)