Skip to content

Commit

Permalink
Merge branch 'main' into Improvement-base-fixtureargkeys-on-param-val…
Browse files Browse the repository at this point in the history
…ues-not-indices
  • Loading branch information
sadra-barikbin committed Aug 10, 2023
2 parents cab7a0e + 556e075 commit 853e33c
Show file tree
Hide file tree
Showing 16 changed files with 328 additions and 253 deletions.
4 changes: 2 additions & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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]
Expand Down
2 changes: 2 additions & 0 deletions changelog/11277.bugfix.rst
Original file line number Diff line number Diff line change
@@ -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.
22 changes: 13 additions & 9 deletions doc/en/example/parametrize.rst
Original file line number Diff line number Diff line change
Expand Up @@ -657,30 +657,34 @@ 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
@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.
5 changes: 2 additions & 3 deletions doc/en/how-to/assert.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
2 changes: 2 additions & 0 deletions doc/en/reference/reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ pytest.exit
pytest.main
~~~~~~~~~~~

**Tutorial**: :ref:`pytest.main-usage`

.. autofunction:: pytest.main

pytest.param
Expand Down
2 changes: 1 addition & 1 deletion src/_pytest/assertion/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
32 changes: 14 additions & 18 deletions src/_pytest/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -581,26 +581,25 @@ def _is_in_confcutdir(self, path: Path) -> bool:
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
Expand All @@ -613,16 +612,17 @@ def _getconftestmodules(
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)
Expand Down Expand Up @@ -1562,13 +1562,9 @@ def _getini(self, name: str):
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
Expand Down
94 changes: 3 additions & 91 deletions src/_pytest/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@
from _pytest.scope import _ScopeName
from _pytest.scope import HIGH_SCOPES
from _pytest.scope import Scope
from _pytest.stash import StashKey


if TYPE_CHECKING:
Expand Down Expand Up @@ -148,89 +147,6 @@ def get_scope_node(
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"]:
"""Return fixturemarker or None if it doesn't exist or raised
exceptions."""
Expand Down Expand Up @@ -271,7 +187,7 @@ def get_parametrized_fixture_keys(
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 in sorted(cs.indices):
Expand All @@ -282,7 +198,7 @@ def get_parametrized_fixture_keys(
if scope is Scope.Session:
scoped_item_path = None
elif scope is Scope.Package:
scoped_item_path = item.path.parent
scoped_item_path = item.path
elif scope is Scope.Module:
scoped_item_path = item.path
elif scope is Scope.Class:
Expand Down Expand Up @@ -385,10 +301,6 @@ def reorder_items_atscope(
return items_done


def get_direct_param_fixture_func(request: "FixtureRequest") -> Any:
return request.param


@dataclasses.dataclass(frozen=True)
class FuncFixtureInfo:
"""Fixture-related information for a fixture-requesting item (e.g. test
Expand Down Expand Up @@ -512,7 +424,7 @@ def node(self):
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)
Expand Down
11 changes: 8 additions & 3 deletions src/_pytest/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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")
Expand Down Expand Up @@ -551,11 +551,16 @@ def gethookproxy(self, fspath: "os.PathLike[str]"):
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.
Expand Down
Loading

0 comments on commit 853e33c

Please sign in to comment.