Merge branch 'main' into Feature-consider-nonparametrized-tests-in-reordering

This commit is contained in:
Sadra Barikbin 2023-08-23 19:57:31 +03:30 committed by GitHub
commit 2336ea53db
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 847 additions and 490 deletions

View File

@ -27,7 +27,7 @@ jobs:
- name: Setup Python
uses: actions/setup-python@v4
with:
python-version: "3.8"
python-version: "3.11"
cache: pip
- name: requests-cache
uses: actions/cache@v3

View File

@ -5,7 +5,7 @@ repos:
- id: black
args: [--safe, --quiet]
- repo: https://github.com/asottile/blacken-docs
rev: 1.15.0
rev: 1.16.0
hooks:
- id: blacken-docs
additional_dependencies: [black==23.7.0]
@ -56,7 +56,7 @@ repos:
hooks:
- id: python-use-type-annotations
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.4.1
rev: v1.5.1
hooks:
- id: mypy
files: ^(src/|testing/)

View File

@ -0,0 +1,5 @@
(This entry is meant to assist plugins which access private pytest internals to instantiate ``FixtureRequest`` objects.)
:class:`~pytest.FixtureRequest` is now an abstract class which can't be instantiated directly.
A new concrete ``TopRequest`` subclass of ``FixtureRequest`` has been added for the ``request`` fixture in test functions,
as counterpart to the existing ``SubRequest`` subclass for the ``request`` fixture in fixture functions.

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

@ -0,0 +1,2 @@
Corrected the spelling of ``Config.ArgsSource.INVOCATION_DIR``.
The previous spelling ``INCOVATION_DIR`` remains as an alias.

View File

@ -0,0 +1 @@
Removes unhelpful error message from assertion rewrite mechanism when exceptions raised in __iter__ methods, and instead treats them as un-iterable.

View File

@ -34,7 +34,7 @@ a function/method call.
**Assert** is where we look at that resulting state and check if it looks how
we'd expect after the dust has settled. It's where we gather evidence to say the
behavior does or does not aligns with what we expect. The ``assert`` in our test
behavior does or does not align with what we expect. The ``assert`` in our test
is where we take that measurement/observation and apply our judgement to it. If
something should be green, we'd say ``assert thing == "green"``.

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

File diff suppressed because it is too large Load Diff

View File

@ -82,6 +82,8 @@ pytest.exit
pytest.main
~~~~~~~~~~~
**Tutorial**: :ref:`pytest.main-usage`
.. autofunction:: pytest.main
pytest.param
@ -235,7 +237,7 @@ pytest.mark.xfail
Marks a test function as *expected to fail*.
.. py:function:: pytest.mark.xfail(condition=None, *, reason=None, raises=None, run=True, strict=False)
.. py:function:: pytest.mark.xfail(condition=None, *, reason=None, raises=None, run=True, strict=xfail_strict)
:type condition: bool or str
:param condition:
@ -247,10 +249,10 @@ Marks a test function as *expected to fail*.
:keyword Type[Exception] raises:
Exception subclass (or tuple of subclasses) expected to be raised by the test function; other exceptions will fail the test.
:keyword bool run:
If the test function should actually be executed. If ``False``, the function will always xfail and will
Whether the test function should actually be executed. If ``False``, the function will always xfail and will
not be executed (useful if a function is segfaulting).
:keyword bool strict:
* If ``False`` (the default) the function will be shown in the terminal output as ``xfailed`` if it fails
* If ``False`` the function will be shown in the terminal output as ``xfailed`` if it fails
and as ``xpass`` if it passes. In both cases this will not cause the test suite to fail as a whole. This
is particularly useful to mark *flaky* tests (tests that fail at random) to be tackled later.
* If ``True``, the function will be shown in the terminal output as ``xfailed`` if it fails, but if it
@ -258,6 +260,8 @@ Marks a test function as *expected to fail*.
that are always failing and there should be a clear indication if they unexpectedly start to pass (for example
a new release of a library fixes a known bug).
Defaults to :confval:`xfail_strict`, which is ``False`` by default.
Custom marks
~~~~~~~~~~~~
@ -1636,11 +1640,11 @@ passed multiple times. The expected format is ``name=value``. For example::
Additionally, ``pytest`` will attempt to intelligently identify and ignore a
virtualenv by the presence of an activation script. Any directory deemed to
be the root of a virtual environment will not be considered during test
collection unless ``collectinvirtualenv`` is given. Note also that
``norecursedirs`` takes precedence over ``collectinvirtualenv``; e.g. if
collection unless ``--collect-in-virtualenv`` is given. Note also that
``norecursedirs`` takes precedence over ``--collect-in-virtualenv``; e.g. if
you intend to run tests in a virtualenv with a base directory that matches
``'.*'`` you *must* override ``norecursedirs`` in addition to using the
``collectinvirtualenv`` flag.
``--collect-in-virtualenv`` flag.
.. confval:: python_classes

View File

@ -17,7 +17,12 @@ python_classes = ["Test", "Acceptance"]
python_functions = ["test"]
# NOTE: "doc" is not included here, but gets tested explicitly via "doctesting".
testpaths = ["testing"]
norecursedirs = ["testing/example_scripts"]
norecursedirs = [
"testing/example_scripts",
".*",
"build",
"dist",
]
xfail_strict = true
filterwarnings = [
"error",

View File

@ -44,6 +44,7 @@ DEVELOPMENT_STATUS_CLASSIFIERS = (
)
ADDITIONAL_PROJECTS = { # set of additional projects to consider as plugins
"logassert",
"nuts",
}

View File

@ -132,7 +132,7 @@ def isiterable(obj: Any) -> bool:
try:
iter(obj)
return not istext(obj)
except TypeError:
except Exception:
return False

View File

@ -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)
@ -953,7 +953,8 @@ class Config:
#: Command line arguments.
ARGS = enum.auto()
#: Invocation directory.
INCOVATION_DIR = enum.auto()
INVOCATION_DIR = enum.auto()
INCOVATION_DIR = INVOCATION_DIR # backwards compatibility alias
#: 'testpaths' configuration value.
TESTPATHS = enum.auto()
@ -1278,7 +1279,7 @@ class Config:
else:
result = []
if not result:
source = Config.ArgsSource.INCOVATION_DIR
source = Config.ArgsSource.INVOCATION_DIR
result = [str(invocation_dir)]
return result, source
@ -1562,13 +1563,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

View File

@ -32,7 +32,7 @@ from _pytest.compat import safe_getattr
from _pytest.config import Config
from _pytest.config.argparsing import Parser
from _pytest.fixtures import fixture
from _pytest.fixtures import FixtureRequest
from _pytest.fixtures import TopRequest
from _pytest.nodes import Collector
from _pytest.nodes import Item
from _pytest.outcomes import OutcomeException
@ -261,7 +261,7 @@ class DoctestItem(Item):
self.runner = runner
self.dtest = dtest
self.obj = None
self.fixture_request: Optional[FixtureRequest] = None
self.fixture_request: Optional[TopRequest] = None
@classmethod
def from_parent( # type: ignore
@ -571,7 +571,7 @@ class DoctestModule(Module):
)
def _setup_fixtures(doctest_item: DoctestItem) -> FixtureRequest:
def _setup_fixtures(doctest_item: DoctestItem) -> TopRequest:
"""Used by DoctestTextfile and DoctestItem to setup fixture information."""
def func() -> None:
@ -582,7 +582,7 @@ def _setup_fixtures(doctest_item: DoctestItem) -> FixtureRequest:
doctest_item._fixtureinfo = fm.getfixtureinfo( # type: ignore[attr-defined]
node=doctest_item, func=func, cls=None, funcargs=False
)
fixture_request = FixtureRequest(doctest_item, _ispytest=True) # type: ignore[arg-type]
fixture_request = TopRequest(doctest_item, _ispytest=True) # type: ignore[arg-type]
fixture_request._fillfixtures()
return fixture_request

View File

@ -1,3 +1,4 @@
import abc
import dataclasses
import functools
import inspect
@ -63,7 +64,6 @@ from _pytest.pathlib import bestrelpath
from _pytest.scope import _ScopeName
from _pytest.scope import HIGH_SCOPES
from _pytest.scope import Scope
from _pytest.stash import StashKey
if TYPE_CHECKING:
@ -146,89 +146,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."""
@ -367,10 +284,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
@ -430,26 +343,32 @@ class FuncFixtureInfo:
self.names_closure[:] = sorted(closure, key=self.names_closure.index)
class FixtureRequest:
"""A request for a fixture from a test or fixture function.
class FixtureRequest(abc.ABC):
"""The type of the ``request`` fixture.
A request object gives access to the requesting test context and has
an optional ``param`` attribute in case the fixture is parametrized
indirectly.
A request object gives access to the requesting test context and has a
``param`` attribute in case the fixture is parametrized.
"""
def __init__(self, pyfuncitem: "Function", *, _ispytest: bool = False) -> None:
def __init__(
self,
pyfuncitem: "Function",
fixturename: Optional[str],
arg2fixturedefs: Dict[str, Sequence["FixtureDef[Any]"]],
arg2index: Dict[str, int],
fixture_defs: Dict[str, "FixtureDef[Any]"],
*,
_ispytest: bool = False,
) -> None:
check_ispytest(_ispytest)
#: Fixture for which this request is being performed.
self.fixturename: Optional[str] = None
self._pyfuncitem = pyfuncitem
self._fixturemanager = pyfuncitem.session._fixturemanager
self._scope = Scope.Function
self.fixturename: Final = fixturename
self._pyfuncitem: Final = pyfuncitem
# The FixtureDefs for each fixture name requested by this item.
# Starts from the statically-known fixturedefs resolved during
# collection. Dynamically requested fixtures (using
# `request.getfixturevalue("foo")`) are added dynamically.
self._arg2fixturedefs = pyfuncitem._fixtureinfo.name2fixturedefs.copy()
self._arg2fixturedefs: Final = arg2fixturedefs
# A fixture may override another fixture with the same name, e.g. a fixture
# in a module can override a fixture in a conftest, a fixture in a class can
# override a fixture in the module, and so on.
@ -459,10 +378,10 @@ class FixtureRequest:
# The fixturedefs list in _arg2fixturedefs for a given name is ordered from
# furthest to closest, so we use negative indexing -1, -2, ... to go from
# last to first.
self._arg2index: Dict[str, int] = {}
self._arg2index: Final = arg2index
# The evaluated argnames so far, mapping to the FixtureDef they resolved
# to.
self._fixture_defs: Dict[str, FixtureDef[Any]] = {}
self._fixture_defs: Final = fixture_defs
# Notes on the type of `param`:
# -`request.param` is only defined in parametrized fixtures, and will raise
# AttributeError otherwise. Python typing has no notion of "undefined", so
@ -473,6 +392,15 @@ class FixtureRequest:
# for now just using Any.
self.param: Any
@property
def _fixturemanager(self) -> "FixtureManager":
return self._pyfuncitem.session._fixturemanager
@property
@abc.abstractmethod
def _scope(self) -> Scope:
raise NotImplementedError()
@property
def scope(self) -> _ScopeName:
"""Scope string, one of "function", "class", "module", "package", "session"."""
@ -486,25 +414,10 @@ class FixtureRequest:
return result
@property
@abc.abstractmethod
def node(self):
"""Underlying collection node (depends on current request scope)."""
scope = self._scope
if scope is Scope.Function:
# This might also be a non-function Item despite its attribute name.
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).
node = get_scope_package(self._pyfuncitem, self._fixturedef) # type: ignore[attr-defined]
else:
node = get_scope_node(self._pyfuncitem, scope)
if node is None and scope is Scope.Class:
# Fallback to function item itself.
node = self._pyfuncitem
assert node, 'Could not obtain a node for scope "{}" for function {!r}'.format(
scope, self._pyfuncitem
)
return node
raise NotImplementedError()
def _getnextfixturedef(self, argname: str) -> "FixtureDef[Any]":
fixturedefs = self._arg2fixturedefs.get(argname, None)
@ -590,11 +503,11 @@ class FixtureRequest:
"""Pytest session object."""
return self._pyfuncitem.session # type: ignore[no-any-return]
@abc.abstractmethod
def addfinalizer(self, finalizer: Callable[[], object]) -> None:
"""Add finalizer/teardown function to be called without arguments after
the last test within the requesting test context finished execution."""
# XXX usually this method is shadowed by fixturedef specific ones.
self.node.addfinalizer(finalizer)
raise NotImplementedError()
def applymarker(self, marker: Union[str, MarkDecorator]) -> None:
"""Apply a marker to a single test function invocation.
@ -615,13 +528,6 @@ class FixtureRequest:
"""
raise self._fixturemanager.FixtureLookupError(None, self, msg)
def _fillfixtures(self) -> None:
item = self._pyfuncitem
fixturenames = getattr(item, "fixturenames", self.fixturenames)
for argname in fixturenames:
if argname not in item.funcargs:
item.funcargs[argname] = self.getfixturevalue(argname)
def getfixturevalue(self, argname: str) -> Any:
"""Dynamically run a named fixture function.
@ -755,6 +661,98 @@ class FixtureRequest:
finalizer = functools.partial(fixturedef.finish, request=subrequest)
subrequest.node.addfinalizer(finalizer)
@final
class TopRequest(FixtureRequest):
"""The type of the ``request`` fixture in a test function."""
def __init__(self, pyfuncitem: "Function", *, _ispytest: bool = False) -> None:
super().__init__(
fixturename=None,
pyfuncitem=pyfuncitem,
arg2fixturedefs=pyfuncitem._fixtureinfo.name2fixturedefs.copy(),
arg2index={},
fixture_defs={},
_ispytest=_ispytest,
)
@property
def _scope(self) -> Scope:
return Scope.Function
@property
def node(self):
return self._pyfuncitem
def __repr__(self) -> str:
return "<FixtureRequest for %r>" % (self.node)
def _fillfixtures(self) -> None:
item = self._pyfuncitem
fixturenames = getattr(item, "fixturenames", self.fixturenames)
for argname in fixturenames:
if argname not in item.funcargs:
item.funcargs[argname] = self.getfixturevalue(argname)
def addfinalizer(self, finalizer: Callable[[], object]) -> None:
self.node.addfinalizer(finalizer)
@final
class SubRequest(FixtureRequest):
"""The type of the ``request`` fixture in a fixture function requested
(transitively) by a test function."""
def __init__(
self,
request: FixtureRequest,
scope: Scope,
param: Any,
param_index: int,
fixturedef: "FixtureDef[object]",
*,
_ispytest: bool = False,
) -> None:
super().__init__(
pyfuncitem=request._pyfuncitem,
fixturename=fixturedef.argname,
fixture_defs=request._fixture_defs,
arg2fixturedefs=request._arg2fixturedefs,
arg2index=request._arg2index,
_ispytest=_ispytest,
)
self._parent_request: Final[FixtureRequest] = request
self._scope_field: Final = scope
self._fixturedef: Final = fixturedef
if param is not NOTSET:
self.param = param
self.param_index: Final = param_index
def __repr__(self) -> str:
return f"<SubRequest {self.fixturename!r} for {self._pyfuncitem!r}>"
@property
def _scope(self) -> Scope:
return self._scope_field
@property
def node(self):
scope = self._scope
if scope is Scope.Function:
# This might also be a non-function Item despite its attribute name.
node: Optional[Union[nodes.Item, nodes.Collector]] = self._pyfuncitem
elif scope is Scope.Package:
node = get_scope_package(self._pyfuncitem, self._fixturedef)
else:
node = get_scope_node(self._pyfuncitem, scope)
if node is None and scope is Scope.Class:
# Fallback to function item itself.
node = self._pyfuncitem
assert node, 'Could not obtain a node for scope "{}" for function {!r}'.format(
scope, self._pyfuncitem
)
return node
def _check_scope(
self,
argname: str,
@ -789,44 +787,7 @@ class FixtureRequest:
)
return lines
def __repr__(self) -> str:
return "<FixtureRequest for %r>" % (self.node)
@final
class SubRequest(FixtureRequest):
"""A sub request for handling getting a fixture from a test function/fixture."""
def __init__(
self,
request: "FixtureRequest",
scope: Scope,
param: Any,
param_index: int,
fixturedef: "FixtureDef[object]",
*,
_ispytest: bool = False,
) -> None:
check_ispytest(_ispytest)
self._parent_request = request
self.fixturename = fixturedef.argname
if param is not NOTSET:
self.param = param
self.param_index = param_index
self._scope = scope
self._fixturedef = fixturedef
self._pyfuncitem = request._pyfuncitem
self._fixture_defs = request._fixture_defs
self._arg2fixturedefs = request._arg2fixturedefs
self._arg2index = request._arg2index
self._fixturemanager = request._fixturemanager
def __repr__(self) -> str:
return f"<SubRequest {self.fixturename!r} for {self._pyfuncitem!r}>"
def addfinalizer(self, finalizer: Callable[[], object]) -> None:
"""Add finalizer/teardown function to be called without arguments after
the last test within the requesting test context finished execution."""
self._fixturedef.addfinalizer(finalizer)
def _schedule_finalizers(

View File

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

View File

@ -461,7 +461,9 @@ if TYPE_CHECKING:
*conditions: Union[str, bool],
reason: str = ...,
run: bool = ...,
raises: Union[Type[BaseException], Tuple[Type[BaseException], ...]] = ...,
raises: Union[
None, Type[BaseException], Tuple[Type[BaseException], ...]
] = ...,
strict: bool = ...,
) -> MarkDecorator:
...

View File

@ -233,6 +233,9 @@ def xfail(reason: str = "") -> NoReturn:
This function should be called only during testing (setup, call or teardown).
No other code is executed after using ``xfail()`` (it is implemented
internally by raising an exception).
:param reason:
The message to show the user as reason for the xfail.

View File

@ -829,7 +829,7 @@ class Pytester:
return self._makefile(ext, args, kwargs)
def makeconftest(self, source: str) -> Path:
"""Write a contest.py file.
"""Write a conftest.py file.
:param source: The contents.
:returns: The conftest.py file.

View File

@ -40,7 +40,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
@ -59,7 +58,10 @@ from _pytest.config.argparsing import Parser
from _pytest.deprecated import check_ispytest
from _pytest.deprecated import INSTANCE_COLLECTOR
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 get_scope_node
from _pytest.main import Session
from _pytest.mark import MARK_GEN
from _pytest.mark import ParameterSet
@ -77,6 +79,7 @@ from _pytest.pathlib import parts
from _pytest.pathlib import visit
from _pytest.scope import _ScopeName
from _pytest.scope import Scope
from _pytest.stash import StashKey
from _pytest.warning_types import PytestCollectionWarning
from _pytest.warning_types import PytestReturnNotNoneWarning
from _pytest.warning_types import PytestUnhandledCoroutineWarning
@ -493,13 +496,11 @@ class PyCollector(PyobjMixin, nodes.Collector):
if not metafunc._calls:
yield Function.from_parent(self, name=name, fixtureinfo=fixtureinfo)
else:
# Add funcargs() as fixturedefs to fixtureinfo.arg2fixturedefs.
fm = self.session._fixturemanager
fixtures.add_funcarg_pseudo_fixture_def(self, metafunc, fm)
# Add_funcarg_pseudo_fixture_def may have shadowed some fixtures
# with direct parametrization, 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:
@ -1116,11 +1117,8 @@ class CallSpec2:
and stored in item.callspec.
"""
# arg name -> arg value which will be passed to the parametrized test
# function (direct parameterization).
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).
# arg name -> arg value which will be passed to a fixture or pseudo-fixture
# of the same name. (indirect or direct parametrization respectively)
params: Dict[str, object] = dataclasses.field(default_factory=dict)
# arg name -> arg index.
indices: Dict[str, int] = dataclasses.field(default_factory=dict)
@ -1134,7 +1132,6 @@ class CallSpec2:
def setmulti(
self,
*,
valtypes: Mapping[str, "Literal['params', 'funcargs']"],
argnames: Iterable[str],
valset: Iterable[object],
id: str,
@ -1142,24 +1139,16 @@ class CallSpec2:
scope: Scope,
param_index: int,
) -> "CallSpec2":
funcargs = self.funcargs.copy()
params = self.params.copy()
indices = self.indices.copy()
arg2scope = self._arg2scope.copy()
for arg, val in zip(argnames, valset):
if arg in params or arg in funcargs:
raise ValueError(f"duplicate {arg!r}")
valtype_for_arg = valtypes[arg]
if valtype_for_arg == "params":
if arg in params:
raise ValueError(f"duplicate parametrization of {arg!r}")
params[arg] = val
elif valtype_for_arg == "funcargs":
funcargs[arg] = val
else:
assert_never(valtype_for_arg)
indices[arg] = param_index
arg2scope[arg] = scope
return CallSpec2(
funcargs=funcargs,
params=params,
indices=indices,
_arg2scope=arg2scope,
@ -1178,6 +1167,14 @@ class CallSpec2:
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
class Metafunc:
"""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
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.
::
@ -1319,8 +1317,6 @@ class Metafunc:
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.
if _param_mark and _param_mark._param_ids_from:
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:
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
# more than once) then we accumulate those calls generating the cartesian product
# of all calls.
@ -1344,7 +1394,6 @@ class Metafunc:
zip(ids, parametersets)
):
newcallspec = callspec.setmulti(
valtypes=arg_values_types,
argnames=argnames,
valset=param_set.values,
id=param_id,
@ -1421,28 +1470,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(
@ -1451,7 +1502,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(
@ -1459,7 +1510,7 @@ class Metafunc:
),
pytrace=False,
)
return valtypes
return arg_directness
def _validate_if_using_arg_names(
self,
@ -1516,7 +1567,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
]
@ -1682,7 +1733,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,
@ -1761,7 +1812,7 @@ class Function(PyobjMixin, nodes.Item):
def _initrequest(self) -> None:
self.funcargs: Dict[str, object] = {}
self._request = fixtures.FixtureRequest(self, _ispytest=True)
self._request = fixtures.TopRequest(self, _ispytest=True)
@property
def function(self):

View File

@ -1,5 +1,7 @@
# PYTHON_ARGCOMPLETE_OK
"""pytest: unit and functional testing with Python."""
from typing import TYPE_CHECKING
from _pytest import __version__
from _pytest import version_tuple
from _pytest._code import ExceptionInfo
@ -165,6 +167,7 @@ __all__ = [
"yield_fixture",
]
if not TYPE_CHECKING:
def __getattr__(name: str) -> object:
if name == "Instance":

View File

@ -272,7 +272,7 @@ def test_importing_instance_is_deprecated(pytester: Pytester) -> None:
pytest.PytestDeprecationWarning,
match=re.escape("The pytest.Instance collector type is deprecated"),
):
pytest.Instance
pytest.Instance # type:ignore[attr-defined]
with pytest.warns(
pytest.PytestDeprecationWarning,

View File

@ -22,13 +22,13 @@ def checked_order():
assert order == [
("issue_519.py", "fix1", "arg1v1"),
("test_one[arg1v1-arg2v1]", "fix2", "arg2v1"),
("test_two[arg1v1-arg2v1]", "fix2", "arg2v1"),
("test_one[arg1v1-arg2v2]", "fix2", "arg2v2"),
("test_two[arg1v1-arg2v1]", "fix2", "arg2v1"),
("test_two[arg1v1-arg2v2]", "fix2", "arg2v2"),
("issue_519.py", "fix1", "arg1v2"),
("test_one[arg1v2-arg2v1]", "fix2", "arg2v1"),
("test_two[arg1v2-arg2v1]", "fix2", "arg2v1"),
("test_one[arg1v2-arg2v2]", "fix2", "arg2v2"),
("test_two[arg1v2-arg2v1]", "fix2", "arg2v1"),
("test_two[arg1v2-arg2v2]", "fix2", "arg2v2"),
]

View File

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

View File

@ -4,10 +4,9 @@ import textwrap
from pathlib import Path
import pytest
from _pytest import fixtures
from _pytest.compat import getfuncargnames
from _pytest.config import ExitCode
from _pytest.fixtures import FixtureRequest
from _pytest.fixtures import TopRequest
from _pytest.monkeypatch import MonkeyPatch
from _pytest.pytester import get_public_names
from _pytest.pytester import Pytester
@ -659,7 +658,7 @@ class TestRequestBasic:
"""
)
assert isinstance(item, Function)
req = fixtures.FixtureRequest(item, _ispytest=True)
req = TopRequest(item, _ispytest=True)
assert req.function == item.obj
assert req.keywords == item.keywords
assert hasattr(req.module, "test_func")
@ -701,9 +700,7 @@ class TestRequestBasic:
(item1,) = pytester.genitems([modcol])
assert isinstance(item1, Function)
assert item1.name == "test_method"
arg2fixturedefs = fixtures.FixtureRequest(
item1, _ispytest=True
)._arg2fixturedefs
arg2fixturedefs = TopRequest(item1, _ispytest=True)._arg2fixturedefs
assert len(arg2fixturedefs) == 1
assert arg2fixturedefs["something"][0].argname == "something"
@ -969,7 +966,7 @@ class TestRequestBasic:
modcol = pytester.getmodulecol("def test_somefunc(): pass")
(item,) = pytester.genitems([modcol])
assert isinstance(item, Function)
req = fixtures.FixtureRequest(item, _ispytest=True)
req = TopRequest(item, _ispytest=True)
assert req.path == modcol.path
def test_request_fixturenames(self, pytester: Pytester) -> None:
@ -1128,7 +1125,7 @@ class TestRequestMarking:
"""
)
assert isinstance(item1, Function)
req1 = fixtures.FixtureRequest(item1, _ispytest=True)
req1 = TopRequest(item1, _ispytest=True)
assert "xfail" not in item1.keywords
req1.applymarker(pytest.mark.xfail)
assert "xfail" in item1.keywords
@ -2103,9 +2100,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:
@ -4041,7 +4036,7 @@ class TestScopeOrdering:
)
items, _ = pytester.inline_genitems()
assert isinstance(items[0], Function)
request = FixtureRequest(items[0], _ispytest=True)
request = TopRequest(items[0], _ispytest=True)
assert request.fixturenames == "m1 f1".split()
def test_func_closure_with_native_fixtures(
@ -4090,7 +4085,7 @@ class TestScopeOrdering:
)
items, _ = pytester.inline_genitems()
assert isinstance(items[0], Function)
request = FixtureRequest(items[0], _ispytest=True)
request = TopRequest(items[0], _ispytest=True)
# order of fixtures based on their scope and position in the parameter list
assert (
request.fixturenames
@ -4118,7 +4113,7 @@ class TestScopeOrdering:
)
items, _ = pytester.inline_genitems()
assert isinstance(items[0], Function)
request = FixtureRequest(items[0], _ispytest=True)
request = TopRequest(items[0], _ispytest=True)
assert request.fixturenames == "m1 f1".split()
def test_func_closure_scopes_reordered(self, pytester: Pytester) -> None:
@ -4152,7 +4147,7 @@ class TestScopeOrdering:
)
items, _ = pytester.inline_genitems()
assert isinstance(items[0], Function)
request = FixtureRequest(items[0], _ispytest=True)
request = TopRequest(items[0], _ispytest=True)
assert request.fixturenames == "s1 m1 c1 f2 f1".split()
def test_func_closure_same_scope_closer_root_first(
@ -4195,7 +4190,7 @@ class TestScopeOrdering:
)
items, _ = pytester.inline_genitems()
assert isinstance(items[0], Function)
request = FixtureRequest(items[0], _ispytest=True)
request = TopRequest(items[0], _ispytest=True)
assert request.fixturenames == "p_sub m_conf m_sub m_test f1".split()
def test_func_closure_all_scopes_complex(self, pytester: Pytester) -> None:
@ -4240,7 +4235,7 @@ class TestScopeOrdering:
)
items, _ = pytester.inline_genitems()
assert isinstance(items[0], Function)
request = FixtureRequest(items[0], _ispytest=True)
request = TopRequest(items[0], _ispytest=True)
assert request.fixturenames == "s1 p1 m1 m2 c1 f2 f1".split()
def test_multiple_packages(self, pytester: Pytester) -> None:

View File

@ -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
@ -33,11 +34,19 @@ class TestMetafunc:
# on the funcarg level, so we don't need a full blown
# initialization.
class FuncFixtureInfoMock:
name2fixturedefs = None
name2fixturedefs: Dict[str, List[fixtures.FixtureDef[object]]] = {}
def __init__(self, names):
self.names_closure = names
@dataclasses.dataclass
class FixtureManagerMock:
config: Any
@dataclasses.dataclass
class SessionMock:
_fixturemanager: FixtureManagerMock
@dataclasses.dataclass
class DefinitionMock(python.FunctionDefinition):
_nodeid: str
@ -46,6 +55,8 @@ class TestMetafunc:
names = getfuncargnames(func)
fixtureinfo: Any = FuncFixtureInfoMock(names)
definition: Any = DefinitionMock._create(obj=func, _nodeid="mock::nodeid")
definition._fixtureinfo = fixtureinfo
definition.session = SessionMock(FixtureManagerMock({}))
return python.Metafunc(definition, fixtureinfo, config, _ispytest=True)
def test_no_funcargs(self) -> None:
@ -98,7 +109,7 @@ class TestMetafunc:
# When the input is an iterator, only len(args) are taken,
# so the bad Exc isn't reached.
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": 2}, "2"),
]
@ -151,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)],
),
)
@ -187,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):
@ -712,8 +725,6 @@ class TestMetafunc:
metafunc.parametrize("x", [1], indirect=True)
metafunc.parametrize("y", [2, 3], indirect=True)
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[1].params == dict(x=1, y=3)
@ -725,8 +736,10 @@ class TestMetafunc:
metafunc = self.Metafunc(func)
metafunc.parametrize("x, y", [("a", "b")], indirect=["x"])
assert metafunc._calls[0].funcargs == dict(y="b")
assert metafunc._calls[0].params == dict(x="a")
assert metafunc._calls[0].params == dict(x="a", y="b")
# 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:
"""#714"""
@ -736,8 +749,8 @@ class TestMetafunc:
metafunc = self.Metafunc(func)
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 list(metafunc._arg2fixturedefs.keys()) == []
def test_parametrize_indirect_list_empty(self) -> None:
"""#714"""
@ -747,8 +760,8 @@ class TestMetafunc:
metafunc = self.Metafunc(func)
metafunc.parametrize("x, y", [("a", "b")], indirect=[])
assert metafunc._calls[0].funcargs == dict(x="a", y="b")
assert metafunc._calls[0].params == {}
assert metafunc._calls[0].params == dict(x="a", y="b")
assert list(metafunc._arg2fixturedefs.keys()) == ["x", "y"]
def test_parametrize_indirect_wrong_type(self) -> None:
def func(x, y):
@ -942,9 +955,9 @@ class TestMetafunc:
metafunc = self.Metafunc(lambda x: None)
metafunc.parametrize("x", [1, 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[1].funcargs == dict(x=2)
assert metafunc._calls[1].params == dict(x=2)
assert metafunc._calls[1].id == "2"
def test_parametrize_onearg_indirect(self) -> None:
@ -959,11 +972,45 @@ class TestMetafunc:
metafunc = self.Metafunc(lambda x, y: None)
metafunc.parametrize(("x", "y"), [(1, 2), (3, 4)])
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[1].funcargs == dict(x=3, y=4)
assert metafunc._calls[1].params == dict(x=3, y=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:
pytester.makepyfile(
"""
@ -1503,6 +1550,98 @@ class TestMetafuncFunctional:
result = pytester.runpytest()
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:
"""Tests related to automatically find out the correct scope for

View File

@ -685,6 +685,25 @@ class TestAssertionRewrite:
assert msg is not None
assert "<MY42 object> < 0" in msg
def test_assert_handling_raise_in__iter__(self, pytester: Pytester) -> None:
pytester.makepyfile(
"""\
class A:
def __iter__(self):
raise ValueError()
def __eq__(self, o: object) -> bool:
return self is o
def __repr__(self):
return "<A object>"
assert A() == A()
"""
)
result = pytester.runpytest()
result.stdout.fnmatch_lines(["*E*assert <A object> == <A object>"])
def test_formatchar(self) -> None:
def f() -> None:
assert "%test" == "test" # type: ignore[comparison-overlap]

View File

@ -507,6 +507,24 @@ class TestParseIni:
result = pytester.runpytest("--foo=1")
result.stdout.fnmatch_lines("* no tests ran in *")
def test_args_source_args(self, pytester: Pytester):
config = pytester.parseconfig("--", "test_filename.py")
assert config.args_source == Config.ArgsSource.ARGS
def test_args_source_invocation_dir(self, pytester: Pytester):
config = pytester.parseconfig()
assert config.args_source == Config.ArgsSource.INVOCATION_DIR
def test_args_source_testpaths(self, pytester: Pytester):
pytester.makeini(
"""
[pytest]
testpaths=*
"""
)
config = pytester.parseconfig()
assert config.args_source == Config.ArgsSource.TESTPATHS
class TestConfigCmdlineParsing:
def test_parsing_again_fails(self, pytester: Pytester) -> None:
@ -642,18 +660,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:

View File

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

View File

@ -2,6 +2,7 @@ from pathlib import Path
import pytest
from _pytest.compat import LEGACY_PATH
from _pytest.fixtures import TopRequest
from _pytest.legacypath import TempdirFactory
from _pytest.legacypath import Testdir
@ -91,7 +92,7 @@ def test_fixturerequest_getmodulepath(pytester: pytest.Pytester) -> None:
modcol = pytester.getmodulecol("def test_somefunc(): pass")
(item,) = pytester.genitems([modcol])
assert isinstance(item, pytest.Function)
req = pytest.FixtureRequest(item, _ispytest=True)
req = TopRequest(item, _ispytest=True)
assert req.path == modcol.path
assert req.fspath == modcol.fspath # type: ignore[attr-defined]

View File

@ -12,6 +12,7 @@ envlist =
pypy3
py38-{pexpect,xdist,unittestextras,numpy,pluggymain,pylib}
doctesting
doctesting-coverage
plugins
py38-freeze
docs