diff options
author | arcadia-devtools <arcadia-devtools@yandex-team.ru> | 2022-02-14 00:49:36 +0300 |
---|---|---|
committer | arcadia-devtools <arcadia-devtools@yandex-team.ru> | 2022-02-14 00:49:36 +0300 |
commit | 82cfd1b7cab2d843cdf5467d9737f72597a493bd (patch) | |
tree | 1dfdcfe81a1a6b193ceacc2a828c521b657a339b /contrib/python/pytest/py3/_pytest/python.py | |
parent | 3df7211d3e3691f8e33b0a1fb1764fe810d59302 (diff) | |
download | ydb-82cfd1b7cab2d843cdf5467d9737f72597a493bd.tar.gz |
intermediate changes
ref:68b1302de4b5da30b6bdf02193f7a2604d8b5cf8
Diffstat (limited to 'contrib/python/pytest/py3/_pytest/python.py')
-rw-r--r-- | contrib/python/pytest/py3/_pytest/python.py | 569 |
1 files changed, 322 insertions, 247 deletions
diff --git a/contrib/python/pytest/py3/_pytest/python.py b/contrib/python/pytest/py3/_pytest/python.py index f1a47d7d33..eed95b65cc 100644 --- a/contrib/python/pytest/py3/_pytest/python.py +++ b/contrib/python/pytest/py3/_pytest/python.py @@ -10,6 +10,7 @@ import warnings from collections import Counter from collections import defaultdict from functools import partial +from pathlib import Path from typing import Any from typing import Callable from typing import Dict @@ -19,14 +20,14 @@ from typing import Iterator from typing import List from typing import Mapping from typing import Optional +from typing import Pattern from typing import Sequence from typing import Set from typing import Tuple -from typing import Type from typing import TYPE_CHECKING from typing import Union -import py +import attr import _pytest from _pytest import fixtures @@ -38,6 +39,7 @@ from _pytest._code.code import TerminalRepr 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 final from _pytest.compat import get_default_arg_names from _pytest.compat import get_real_func @@ -45,8 +47,8 @@ from _pytest.compat import getimfunc from _pytest.compat import getlocation from _pytest.compat import is_async_function from _pytest.compat import is_generator +from _pytest.compat import LEGACY_PATH from _pytest.compat import NOTSET -from _pytest.compat import REGEX_TYPE from _pytest.compat import safe_getattr from _pytest.compat import safe_isclass from _pytest.compat import STRING_TYPES @@ -54,7 +56,9 @@ from _pytest.config import Config from _pytest.config import ExitCode from _pytest.config import hookimpl from _pytest.config.argparsing import Parser +from _pytest.deprecated import check_ispytest from _pytest.deprecated import FSCOLLECTOR_GETHOOKPROXY_ISINITPATH +from _pytest.deprecated import INSTANCE_COLLECTOR from _pytest.fixtures import FuncFixtureInfo from _pytest.main import Session from _pytest.mark import MARK_GEN @@ -65,16 +69,22 @@ from _pytest.mark.structures import MarkDecorator from _pytest.mark.structures import normalize_mark_list from _pytest.outcomes import fail from _pytest.outcomes import skip +from _pytest.pathlib import bestrelpath +from _pytest.pathlib import fnmatch_ex from _pytest.pathlib import import_path from _pytest.pathlib import ImportPathMismatchError from _pytest.pathlib import parts from _pytest.pathlib import visit +from _pytest.scope import Scope from _pytest.warning_types import PytestCollectionWarning from _pytest.warning_types import PytestUnhandledCoroutineWarning if TYPE_CHECKING: from typing_extensions import Literal - from _pytest.fixtures import _Scope + from _pytest.scope import _ScopeName + + +_PYTEST_DIR = Path(_pytest.__file__).parent def pytest_addoption(parser: Parser) -> None: @@ -135,8 +145,7 @@ def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]: def pytest_generate_tests(metafunc: "Metafunc") -> None: for marker in metafunc.definition.iter_markers(name="parametrize"): - # TODO: Fix this type-ignore (overlapping kwargs). - metafunc.parametrize(*marker.args, **marker.kwargs, _param_mark=marker) # type: ignore[misc] + metafunc.parametrize(*marker.args, **marker.kwargs, _param_mark=marker) def pytest_configure(config: Config) -> None: @@ -148,14 +157,14 @@ def pytest_configure(config: Config) -> None: "or a list of tuples of values if argnames specifies multiple names. " "Example: @parametrize('arg1', [1,2]) would lead to two calls of the " "decorated test function, one with arg1=1 and another with arg1=2." - "see https://docs.pytest.org/en/stable/parametrize.html for more info " + "see https://docs.pytest.org/en/stable/how-to/parametrize.html for more info " "and examples.", ) config.addinivalue_line( "markers", "usefixtures(fixturename1, fixturename2, ...): mark tests as needing " "all of the specified fixtures. see " - "https://docs.pytest.org/en/stable/fixture.html#usefixtures ", + "https://docs.pytest.org/en/stable/explanation/fixtures.html#usefixtures ", ) @@ -170,7 +179,7 @@ def async_warn_and_skip(nodeid: str) -> None: msg += " - pytest-trio\n" msg += " - pytest-twisted" warnings.warn(PytestUnhandledCoroutineWarning(msg.format(nodeid))) - skip(msg="async def function and no async plugin installed (see warnings)") + skip(reason="async def function and no async plugin installed (see warnings)") @hookimpl(trylast=True) @@ -186,32 +195,31 @@ def pytest_pyfunc_call(pyfuncitem: "Function") -> Optional[object]: return True -def pytest_collect_file( - path: py.path.local, parent: nodes.Collector -) -> Optional["Module"]: - ext = path.ext - if ext == ".py": - if not parent.session.isinitpath(path): +def pytest_collect_file(file_path: Path, parent: nodes.Collector) -> Optional["Module"]: + if file_path.suffix == ".py": + if not parent.session.isinitpath(file_path): if not path_matches_patterns( - path, parent.config.getini("python_files") + ["__init__.py"] + file_path, parent.config.getini("python_files") + ["__init__.py"] ): return None - ihook = parent.session.gethookproxy(path) - module: Module = ihook.pytest_pycollect_makemodule(path=path, parent=parent) + ihook = parent.session.gethookproxy(file_path) + module: Module = ihook.pytest_pycollect_makemodule( + module_path=file_path, parent=parent + ) return module return None -def path_matches_patterns(path: py.path.local, patterns: Iterable[str]) -> bool: +def path_matches_patterns(path: Path, patterns: Iterable[str]) -> bool: """Return whether path matches any of the patterns in the list of globs given.""" - return any(path.fnmatch(pattern) for pattern in patterns) + return any(fnmatch_ex(pattern, path) for pattern in patterns) -def pytest_pycollect_makemodule(path: py.path.local, parent) -> "Module": - if path.basename == "__init__.py": - pkg: Package = Package.from_parent(parent, fspath=path) +def pytest_pycollect_makemodule(module_path: Path, parent) -> "Module": + if module_path.name == "__init__.py": + pkg: Package = Package.from_parent(parent, path=module_path) return pkg - mod: Module = Module.from_parent(parent, fspath=path) + mod: Module = Module.from_parent(parent, path=module_path) return mod @@ -250,20 +258,13 @@ def pytest_pycollect_makeitem(collector: "PyCollector", name: str, obj: object): return res -class PyobjMixin: - _ALLOW_MARKERS = True - - # Function and attributes that the mixin needs (for type-checking only). - if TYPE_CHECKING: - name: str = "" - parent: Optional[nodes.Node] = None - own_markers: List[Mark] = [] +class PyobjMixin(nodes.Node): + """this mix-in inherits from Node to carry over the typing information - def getparent(self, cls: Type[nodes._NodeType]) -> Optional[nodes._NodeType]: - ... + as its intended to always mix in before a node + its position in the mro is unaffected""" - def listchain(self) -> List[nodes.Node]: - ... + _ALLOW_MARKERS = True @property def module(self): @@ -279,9 +280,13 @@ class PyobjMixin: @property def instance(self): - """Python instance object this node was collected from (can be None).""" - node = self.getparent(Instance) - return node.obj if node is not None else None + """Python instance object the function is bound to. + + Returns None if not a test method, e.g. for a standalone test function, + a staticmethod, a class or a module. + """ + node = self.getparent(Function) + return getattr(node.obj, "__self__", None) if node is not None else None @property def obj(self): @@ -290,7 +295,7 @@ class PyobjMixin: if obj is None: self._obj = obj = self._getobj() # XXX evil hack - # used to avoid Instance collector marker duplication + # used to avoid Function marker duplication if self._ALLOW_MARKERS: self.own_markers.extend(get_unpacked_marks(self.obj)) return obj @@ -312,8 +317,6 @@ class PyobjMixin: chain.reverse() parts = [] for node in chain: - if isinstance(node, Instance): - continue name = node.name if isinstance(node, Module): name = os.path.splitext(name)[0] @@ -325,7 +328,7 @@ class PyobjMixin: parts.reverse() return ".".join(parts) - def reportinfo(self) -> Tuple[Union[py.path.local, str], int, str]: + def reportinfo(self) -> Tuple[Union["os.PathLike[str]", str], Optional[int], str]: # XXX caching? obj = self.obj compat_co_firstlineno = getattr(obj, "compat_co_firstlineno", None) @@ -334,13 +337,13 @@ class PyobjMixin: file_path = sys.modules[obj.__module__].__file__ if file_path.endswith(".pyc"): file_path = file_path[:-1] - fspath: Union[py.path.local, str] = file_path + path: Union["os.PathLike[str]", str] = file_path lineno = compat_co_firstlineno else: - fspath, lineno = getfslineno(obj) + path, lineno = getfslineno(obj) modpath = self.getmodpath() assert isinstance(lineno, int) - return fspath, lineno, modpath + return path, lineno, modpath # As an optimization, these builtin attribute names are pre-ignored when @@ -384,10 +387,7 @@ class PyCollector(PyobjMixin, nodes.Collector): if isinstance(obj, staticmethod): # staticmethods need to be unwrapped. obj = safe_getattr(obj, "__func__", False) - return ( - safe_getattr(obj, "__call__", False) - and fixtures.getfixturemarker(obj) is None - ) + return callable(obj) and fixtures.getfixturemarker(obj) is None else: return False @@ -413,15 +413,19 @@ class PyCollector(PyobjMixin, nodes.Collector): if not getattr(self.obj, "__test__", True): return [] - # NB. we avoid random getattrs and peek in the __dict__ instead - # (XXX originally introduced from a PyPy need, still true?) + # Avoid random getattrs and peek in the __dict__ instead. dicts = [getattr(self.obj, "__dict__", {})] - for basecls in self.obj.__class__.__mro__: - dicts.append(basecls.__dict__) + if isinstance(self.obj, type): + for basecls in self.obj.__mro__: + dicts.append(basecls.__dict__) + + # In each class, nodes should be definition ordered. Since Python 3.6, + # __dict__ is definition ordered. seen: Set[str] = set() - values: List[Union[nodes.Item, nodes.Collector]] = [] + dict_values: List[List[Union[nodes.Item, nodes.Collector]]] = [] ihook = self.ihook for dic in dicts: + values: List[Union[nodes.Item, nodes.Collector]] = [] # Note: seems like the dict can change during iteration - # be careful not to remove the list() without consideration. for name, obj in list(dic.items()): @@ -439,13 +443,14 @@ class PyCollector(PyobjMixin, nodes.Collector): values.extend(res) else: values.append(res) + dict_values.append(values) - def sort_key(item): - fspath, lineno, _ = item.reportinfo() - return (str(fspath), lineno) - - values.sort(key=sort_key) - return values + # Between classes in the class hierarchy, reverse-MRO order -- nodes + # inherited from base classes should come before subclasses. + result = [] + for values in reversed(dict_values): + result.extend(values) + return result def _genfunctions(self, name: str, funcobj) -> Iterator["Function"]: modulecol = self.getparent(Module) @@ -453,26 +458,32 @@ class PyCollector(PyobjMixin, nodes.Collector): module = modulecol.obj clscol = self.getparent(Class) cls = clscol and clscol.obj or None - fm = self.session._fixturemanager definition = FunctionDefinition.from_parent(self, name=name, callobj=funcobj) fixtureinfo = definition._fixtureinfo + # pytest_generate_tests impls call metafunc.parametrize() which fills + # metafunc._calls, the outcome of the hook. metafunc = Metafunc( - definition, fixtureinfo, self.config, cls=cls, module=module + definition=definition, + fixtureinfo=fixtureinfo, + config=self.config, + cls=cls, + module=module, + _ispytest=True, ) methods = [] if hasattr(module, "pytest_generate_tests"): methods.append(module.pytest_generate_tests) if cls is not None and hasattr(cls, "pytest_generate_tests"): methods.append(cls().pytest_generate_tests) - self.ihook.pytest_generate_tests.call_extra(methods, dict(metafunc=metafunc)) 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 @@ -486,7 +497,6 @@ class PyCollector(PyobjMixin, nodes.Collector): self, name=subname, callspec=callspec, - callobj=funcobj, fixtureinfo=fixtureinfo, keywords={callspec.id: True}, originalname=name, @@ -512,12 +522,23 @@ class Module(nodes.File, PyCollector): Using a fixture to invoke this methods ensures we play nicely and unsurprisingly with other fixtures (#517). """ + has_nose = self.config.pluginmanager.has_plugin("nose") setup_module = _get_first_non_fixture_func( self.obj, ("setUpModule", "setup_module") ) + if setup_module is None and has_nose: + # The name "setup" is too common - only treat as fixture if callable. + setup_module = _get_first_non_fixture_func(self.obj, ("setup",)) + if not callable(setup_module): + setup_module = None teardown_module = _get_first_non_fixture_func( self.obj, ("tearDownModule", "teardown_module") ) + if teardown_module is None and has_nose: + teardown_module = _get_first_non_fixture_func(self.obj, ("teardown",)) + # Same as "setup" above - only treat as fixture if callable. + if not callable(teardown_module): + teardown_module = None if setup_module is None and teardown_module is None: return @@ -526,7 +547,7 @@ class Module(nodes.File, PyCollector): autouse=True, scope="module", # Use a unique name to speed up lookup. - name=f"xunit_setup_module_fixture_{self.obj.__name__}", + name=f"_xunit_setup_module_fixture_{self.obj.__name__}", ) def xunit_setup_module_fixture(request) -> Generator[None, None, None]: if setup_module is not None: @@ -555,7 +576,7 @@ class Module(nodes.File, PyCollector): autouse=True, scope="function", # Use a unique name to speed up lookup. - name=f"xunit_setup_function_fixture_{self.obj.__name__}", + name=f"_xunit_setup_function_fixture_{self.obj.__name__}", ) def xunit_setup_function_fixture(request) -> Generator[None, None, None]: if request.instance is not None: @@ -575,7 +596,7 @@ class Module(nodes.File, PyCollector): # We assume we are only called once per module. importmode = self.config.getoption("--import-mode") try: - mod = import_path(self.fspath, mode=importmode) + mod = import_path(self.path, mode=importmode, root=self.config.rootpath) except SyntaxError as e: raise self.CollectError( ExceptionInfo.from_current().getrepr(style="short") @@ -601,19 +622,19 @@ class Module(nodes.File, PyCollector): ) formatted_tb = str(exc_repr) raise self.CollectError( - "ImportError while importing test module '{fspath}'.\n" + "ImportError while importing test module '{path}'.\n" "Hint: make sure your test modules/packages have valid Python names.\n" "Traceback:\n" - "{traceback}".format(fspath=self.fspath, traceback=formatted_tb) + "{traceback}".format(path=self.path, traceback=formatted_tb) ) from e except skip.Exception as e: if e.allow_module_level: raise raise self.CollectError( - "Using pytest.skip outside of a test is not allowed. " - "To decorate a test function, use the @pytest.mark.skip " - "or @pytest.mark.skipif decorators instead, and to skip a " - "module use `pytestmark = pytest.mark.{skip,skipif}." + "Using pytest.skip outside of a test will skip the entire module. " + "If that's your intention, pass `allow_module_level=True`. " + "If you want to skip a specific test or an entire class, " + "use the @pytest.mark.skip or @pytest.mark.skipif decorators." ) from e self.config.pluginmanager.consider_module(mod) return mod @@ -622,20 +643,27 @@ class Module(nodes.File, PyCollector): class Package(Module): def __init__( self, - fspath: py.path.local, + fspath: Optional[LEGACY_PATH], parent: nodes.Collector, # NOTE: following args are unused: config=None, session=None, nodeid=None, + path=Optional[Path], ) -> None: # NOTE: Could be just the following, but kept as-is for compat. # nodes.FSCollector.__init__(self, fspath, parent=parent) session = parent.session nodes.FSCollector.__init__( - self, fspath, parent=parent, config=config, session=session, nodeid=nodeid + self, + fspath=fspath, + path=path, + parent=parent, + config=config, + session=session, + nodeid=nodeid, ) - self.name = os.path.basename(str(fspath.dirname)) + self.name = self.path.parent.name def setup(self) -> None: # Not using fixtures to call setup_module here because autouse fixtures @@ -653,69 +681,69 @@ class Package(Module): func = partial(_call_with_optional_argument, teardown_module, self.obj) self.addfinalizer(func) - def gethookproxy(self, fspath: py.path.local): + def gethookproxy(self, fspath: "os.PathLike[str]"): warnings.warn(FSCOLLECTOR_GETHOOKPROXY_ISINITPATH, stacklevel=2) return self.session.gethookproxy(fspath) - def isinitpath(self, path: py.path.local) -> bool: + def isinitpath(self, path: Union[str, "os.PathLike[str]"]) -> bool: warnings.warn(FSCOLLECTOR_GETHOOKPROXY_ISINITPATH, stacklevel=2) return self.session.isinitpath(path) def _recurse(self, direntry: "os.DirEntry[str]") -> bool: if direntry.name == "__pycache__": return False - path = py.path.local(direntry.path) - ihook = self.session.gethookproxy(path.dirpath()) - if ihook.pytest_ignore_collect(path=path, config=self.config): + fspath = Path(direntry.path) + ihook = self.session.gethookproxy(fspath.parent) + if ihook.pytest_ignore_collect(collection_path=fspath, config=self.config): return False norecursepatterns = self.config.getini("norecursedirs") - if any(path.check(fnmatch=pat) for pat in norecursepatterns): + if any(fnmatch_ex(pat, fspath) for pat in norecursepatterns): return False return True def _collectfile( - self, path: py.path.local, handle_dupes: bool = True + self, fspath: Path, handle_dupes: bool = True ) -> Sequence[nodes.Collector]: assert ( - path.isfile() + fspath.is_file() ), "{!r} is not a file (isdir={!r}, exists={!r}, islink={!r})".format( - path, path.isdir(), path.exists(), path.islink() + fspath, fspath.is_dir(), fspath.exists(), fspath.is_symlink() ) - ihook = self.session.gethookproxy(path) - if not self.session.isinitpath(path): - if ihook.pytest_ignore_collect(path=path, config=self.config): + ihook = self.session.gethookproxy(fspath) + if not self.session.isinitpath(fspath): + if ihook.pytest_ignore_collect(collection_path=fspath, config=self.config): return () if handle_dupes: keepduplicates = self.config.getoption("keepduplicates") if not keepduplicates: duplicate_paths = self.config.pluginmanager._duplicatepaths - if path in duplicate_paths: + if fspath in duplicate_paths: return () else: - duplicate_paths.add(path) + duplicate_paths.add(fspath) - return ihook.pytest_collect_file(path=path, parent=self) # type: ignore[no-any-return] + return ihook.pytest_collect_file(file_path=fspath, parent=self) # type: ignore[no-any-return] def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]: - this_path = self.fspath.dirpath() - init_module = this_path.join("__init__.py") - if init_module.check(file=1) and path_matches_patterns( + this_path = self.path.parent + init_module = this_path / "__init__.py" + if init_module.is_file() and path_matches_patterns( init_module, self.config.getini("python_files") ): - yield Module.from_parent(self, fspath=init_module) - pkg_prefixes: Set[py.path.local] = set() + yield Module.from_parent(self, path=init_module) + pkg_prefixes: Set[Path] = set() for direntry in visit(str(this_path), recurse=self._recurse): - path = py.path.local(direntry.path) + path = Path(direntry.path) # We will visit our own __init__.py file, in which case we skip it. if direntry.is_file(): - if direntry.name == "__init__.py" and path.dirpath() == this_path: + if direntry.name == "__init__.py" and path.parent == this_path: continue parts_ = parts(direntry.path) if any( - str(pkg_prefix) in parts_ and pkg_prefix.join("__init__.py") != path + str(pkg_prefix) in parts_ and pkg_prefix / "__init__.py" != path for pkg_prefix in pkg_prefixes ): continue @@ -725,7 +753,7 @@ class Package(Module): elif not direntry.is_dir(): # Broken symlink or invalid/missing file. continue - elif path.join("__init__.py").check(file=1): + elif path.joinpath("__init__.py").is_file(): pkg_prefixes.add(path) @@ -741,22 +769,26 @@ def _call_with_optional_argument(func, arg) -> None: func() -def _get_first_non_fixture_func(obj: object, names: Iterable[str]): +def _get_first_non_fixture_func(obj: object, names: Iterable[str]) -> Optional[object]: """Return the attribute from the given object to be used as a setup/teardown xunit-style function, but only if not marked as a fixture to avoid calling it twice.""" for name in names: - meth = getattr(obj, name, None) + meth: Optional[object] = getattr(obj, name, None) if meth is not None and fixtures.getfixturemarker(meth) is None: return meth + return None class Class(PyCollector): """Collector for test methods.""" @classmethod - def from_parent(cls, parent, *, name, obj=None): + def from_parent(cls, parent, *, name, obj=None, **kw): """The public constructor.""" - return super().from_parent(name=name, parent=parent) + return super().from_parent(name=name, parent=parent, **kw) + + def newinstance(self): + return self.obj() def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]: if not safe_getattr(self.obj, "__test__", True): @@ -785,7 +817,9 @@ class Class(PyCollector): self._inject_setup_class_fixture() self._inject_setup_method_fixture() - return [Instance.from_parent(self, name="()")] + self.session._fixturemanager.parsefactories(self.newinstance(), self.nodeid) + + return super().collect() def _inject_setup_class_fixture(self) -> None: """Inject a hidden autouse, class scoped fixture into the collected class object @@ -803,7 +837,7 @@ class Class(PyCollector): autouse=True, scope="class", # Use a unique name to speed up lookup. - name=f"xunit_setup_class_fixture_{self.obj.__qualname__}", + name=f"_xunit_setup_class_fixture_{self.obj.__qualname__}", ) def xunit_setup_class_fixture(cls) -> Generator[None, None, None]: if setup_class is not None: @@ -823,8 +857,17 @@ class Class(PyCollector): Using a fixture to invoke this methods ensures we play nicely and unsurprisingly with other fixtures (#517). """ - setup_method = _get_first_non_fixture_func(self.obj, ("setup_method",)) - teardown_method = getattr(self.obj, "teardown_method", None) + has_nose = self.config.pluginmanager.has_plugin("nose") + setup_name = "setup_method" + setup_method = _get_first_non_fixture_func(self.obj, (setup_name,)) + if setup_method is None and has_nose: + setup_name = "setup" + setup_method = _get_first_non_fixture_func(self.obj, (setup_name,)) + teardown_name = "teardown_method" + teardown_method = getattr(self.obj, teardown_name, None) + if teardown_method is None and has_nose: + teardown_name = "teardown" + teardown_method = getattr(self.obj, teardown_name, None) if setup_method is None and teardown_method is None: return @@ -832,40 +875,37 @@ class Class(PyCollector): autouse=True, scope="function", # Use a unique name to speed up lookup. - name=f"xunit_setup_method_fixture_{self.obj.__qualname__}", + name=f"_xunit_setup_method_fixture_{self.obj.__qualname__}", ) def xunit_setup_method_fixture(self, request) -> Generator[None, None, None]: method = request.function if setup_method is not None: - func = getattr(self, "setup_method") + func = getattr(self, setup_name) _call_with_optional_argument(func, method) yield if teardown_method is not None: - func = getattr(self, "teardown_method") + func = getattr(self, teardown_name) _call_with_optional_argument(func, method) self.obj.__pytest_setup_method = xunit_setup_method_fixture -class Instance(PyCollector): - _ALLOW_MARKERS = False # hack, destroy later - # Instances share the object with their parents in a way - # that duplicates markers instances if not taken out - # can be removed at node structure reorganization time. +class InstanceDummy: + """Instance used to be a node type between Class and Function. It has been + removed in pytest 7.0. Some plugins exist which reference `pytest.Instance` + only to ignore it; this dummy class keeps them working. This will be removed + in pytest 8.""" - def _getobj(self): - # TODO: Improve the type of `parent` such that assert/ignore aren't needed. - assert self.parent is not None - obj = self.parent.obj # type: ignore[attr-defined] - return obj() + pass - def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]: - self.session._fixturemanager.parsefactories(self) - return super().collect() - def newinstance(self): - self.obj = self._getobj() - return self.obj +# Note: module __getattr__ only works on Python>=3.7. Unfortunately +# we can't provide this deprecation warning on Python 3.6. +def __getattr__(name: str) -> object: + if name == "Instance": + warnings.warn(INSTANCE_COLLECTOR, 2) + return InstanceDummy + raise AttributeError(f"module {__name__} has no attribute {name}") def hasinit(obj: object) -> bool: @@ -883,69 +923,80 @@ def hasnew(obj: object) -> bool: @final +@attr.s(frozen=True, slots=True, auto_attribs=True) class CallSpec2: - def __init__(self, metafunc: "Metafunc") -> None: - self.metafunc = metafunc - self.funcargs: Dict[str, object] = {} - self._idlist: List[str] = [] - self.params: Dict[str, object] = {} - # Used for sorting parametrized resources. - self._arg2scopenum: Dict[str, int] = {} - self.marks: List[Mark] = [] - self.indices: Dict[str, int] = {} - - def copy(self) -> "CallSpec2": - cs = CallSpec2(self.metafunc) - cs.funcargs.update(self.funcargs) - cs.params.update(self.params) - cs.marks.extend(self.marks) - cs.indices.update(self.indices) - cs._arg2scopenum.update(self._arg2scopenum) - cs._idlist = list(self._idlist) - return cs - - def _checkargnotcontained(self, arg: str) -> None: - if arg in self.params or arg in self.funcargs: - raise ValueError(f"duplicate {arg!r}") + """A planned parameterized invocation of a test function. - def getparam(self, name: str) -> object: - try: - return self.params[name] - except KeyError as e: - raise ValueError(name) from e - - @property - def id(self) -> str: - return "-".join(map(str, self._idlist)) + Calculated during collection for a given test function's Metafunc. + Once collection is over, each callspec is turned into a single Item + and stored in item.callspec. + """ - def setmulti2( + # arg name -> arg value which will be passed to the parametrized test + # function (direct parameterization). + funcargs: Dict[str, object] = attr.Factory(dict) + # arg name -> arg value which will be passed to a fixture of the same name + # (indirect parametrization). + params: Dict[str, object] = attr.Factory(dict) + # arg name -> arg index. + indices: Dict[str, int] = attr.Factory(dict) + # Used for sorting parametrized resources. + _arg2scope: Dict[str, Scope] = attr.Factory(dict) + # Parts which will be added to the item's name in `[..]` separated by "-". + _idlist: List[str] = attr.Factory(list) + # Marks which will be applied to the item. + marks: List[Mark] = attr.Factory(list) + + def setmulti( self, + *, valtypes: Mapping[str, "Literal['params', 'funcargs']"], - argnames: Sequence[str], + argnames: Iterable[str], valset: Iterable[object], id: str, marks: Iterable[Union[Mark, MarkDecorator]], - scopenum: int, + scope: Scope, param_index: int, - ) -> None: + ) -> "CallSpec2": + funcargs = self.funcargs.copy() + params = self.params.copy() + indices = self.indices.copy() + arg2scope = self._arg2scope.copy() for arg, val in zip(argnames, valset): - self._checkargnotcontained(arg) + if arg in params or arg in funcargs: + raise ValueError(f"duplicate {arg!r}") valtype_for_arg = valtypes[arg] if valtype_for_arg == "params": - self.params[arg] = val + params[arg] = val elif valtype_for_arg == "funcargs": - self.funcargs[arg] = val - else: # pragma: no cover - assert False, f"Unhandled valtype for arg: {valtype_for_arg}" - self.indices[arg] = param_index - self._arg2scopenum[arg] = scopenum - self._idlist.append(id) - self.marks.extend(normalize_mark_list(marks)) + funcargs[arg] = val + else: + assert_never(valtype_for_arg) + indices[arg] = param_index + arg2scope[arg] = scope + return CallSpec2( + funcargs=funcargs, + params=params, + arg2scope=arg2scope, + indices=indices, + idlist=[*self._idlist, id], + marks=[*self.marks, *normalize_mark_list(marks)], + ) + + def getparam(self, name: str) -> object: + try: + return self.params[name] + except KeyError as e: + raise ValueError(name) from e + + @property + def id(self) -> str: + return "-".join(self._idlist) @final class Metafunc: - """Objects passed to the :func:`pytest_generate_tests <_pytest.hookspec.pytest_generate_tests>` hook. + """Objects passed to the :hook:`pytest_generate_tests` hook. They help to inspect a test function and to generate tests according to test configuration or values specified in the class or module where a @@ -959,11 +1010,15 @@ class Metafunc: config: Config, cls=None, module=None, + *, + _ispytest: bool = False, ) -> None: + check_ispytest(_ispytest) + #: Access to the underlying :class:`_pytest.python.FunctionDefinition`. self.definition = definition - #: Access to the :class:`_pytest.config.Config` object for the test session. + #: Access to the :class:`pytest.Config` object for the test session. self.config = config #: The module object where the test function is defined in. @@ -978,9 +1033,11 @@ class Metafunc: #: Class object where the test function is defined in or ``None``. self.cls = cls - self._calls: List[CallSpec2] = [] self._arg2fixturedefs = fixtureinfo.name2fixturedefs + # Result of parametrize(). + self._calls: List[CallSpec2] = [] + def parametrize( self, argnames: Union[str, List[str], Tuple[str, ...]], @@ -992,14 +1049,23 @@ class Metafunc: Callable[[Any], Optional[object]], ] ] = None, - scope: "Optional[_Scope]" = None, + scope: "Optional[_ScopeName]" = None, *, _param_mark: Optional[Mark] = None, ) -> None: """Add new invocations to the underlying test function using the list - of argvalues for the given argnames. Parametrization is performed - during the collection phase. If you need to setup expensive resources - see about setting indirect to do it rather at test setup time. + of argvalues for the given argnames. Parametrization is performed + 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. + + :: + + unparametrized: t + parametrize ["x", "y"]: t[x], t[y] + parametrize [1, 2]: t[x-1], t[x-2], t[y-1], t[y-2] :param argnames: A comma-separated string denoting one or more argument names, or @@ -1048,8 +1114,6 @@ class Metafunc: It will also override any fixture-function defined scope, allowing to set a dynamic scope using test context or configuration. """ - from _pytest.fixtures import scope2index - argnames, parameters = ParameterSet._for_parametrize( argnames, argvalues, @@ -1065,8 +1129,12 @@ class Metafunc: pytrace=False, ) - if scope is None: - scope = _find_parametrized_scope(argnames, self._arg2fixturedefs, indirect) + if scope is not None: + scope_ = Scope.from_user( + scope, descr=f"parametrize() call in {self.function.__name__}" + ) + else: + scope_ = _find_parametrized_scope(argnames, self._arg2fixturedefs, indirect) self._validate_if_using_arg_names(argnames, indirect) @@ -1086,25 +1154,20 @@ 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) - scopenum = scope2index( - scope, descr=f"parametrize() call in {self.function.__name__}" - ) - # 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. newcalls = [] - for callspec in self._calls or [CallSpec2(self)]: + for callspec in self._calls or [CallSpec2()]: for param_index, (param_id, param_set) in enumerate(zip(ids, parameters)): - newcallspec = callspec.copy() - newcallspec.setmulti2( - arg_values_types, - argnames, - param_set.values, - param_id, - param_set.marks, - scopenum, - param_index, + newcallspec = callspec.setmulti( + valtypes=arg_values_types, + argnames=argnames, + valset=param_set.values, + id=param_id, + marks=param_set.marks, + scope=scope_, + param_index=param_index, ) newcalls.append(newcallspec) self._calls = newcalls @@ -1180,7 +1243,9 @@ class Metafunc: return new_ids def _resolve_arg_value_types( - self, argnames: Sequence[str], indirect: Union[bool, Sequence[str]], + 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 @@ -1218,7 +1283,9 @@ class Metafunc: return valtypes def _validate_if_using_arg_names( - self, argnames: Sequence[str], indirect: Union[bool, Sequence[str]], + self, + argnames: Sequence[str], + indirect: Union[bool, Sequence[str]], ) -> None: """Check if all argnames are being used, by default values, or directly/indirectly. @@ -1252,7 +1319,7 @@ def _find_parametrized_scope( argnames: Sequence[str], arg2fixturedefs: Mapping[str, Sequence[fixtures.FixtureDef[object]]], indirect: Union[bool, Sequence[str]], -) -> "fixtures._Scope": +) -> Scope: """Find the most appropriate scope for a parametrized call based on its arguments. When there's at least one direct argument, always use "function" scope. @@ -1270,17 +1337,14 @@ def _find_parametrized_scope( if all_arguments_are_fixtures: fixturedefs = arg2fixturedefs or {} used_scopes = [ - fixturedef[0].scope + fixturedef[0]._scope for name, fixturedef in fixturedefs.items() if name in argnames ] - if used_scopes: - # Takes the most narrow scope from used fixtures. - for scope in reversed(fixtures.scopes): - if scope in used_scopes: - return scope + # Takes the most narrow scope from used fixtures. + return min(used_scopes, default=Scope.Function) - return "function" + return Scope.Function def _ascii_escaped_by_config(val: Union[str, bytes], config: Optional[Config]) -> str: @@ -1323,9 +1387,9 @@ def _idval( if isinstance(val, STRING_TYPES): return _ascii_escaped_by_config(val, config) - elif val is None or isinstance(val, (float, int, bool)): + elif val is None or isinstance(val, (float, int, bool, complex)): return str(val) - elif isinstance(val, REGEX_TYPE): + elif isinstance(val, Pattern): return ascii_escaped(val.pattern) elif val is NOTSET: # Fallback to default. Note that NOTSET is an enum.Enum. @@ -1416,12 +1480,22 @@ def idmaker( # Suffix non-unique IDs to make them unique. for index, test_id in enumerate(resolved_ids): if test_id_counts[test_id] > 1: - resolved_ids[index] = "{}{}".format(test_id, test_id_suffixes[test_id]) + resolved_ids[index] = f"{test_id}{test_id_suffixes[test_id]}" test_id_suffixes[test_id] += 1 return resolved_ids +def _pretty_fixture_path(func) -> str: + cwd = Path.cwd() + loc = Path(getlocation(func, str(cwd))) + prefix = Path("...", "_pytest") + try: + return str(prefix / loc.relative_to(_PYTEST_DIR)) + except ValueError: + return bestrelpath(cwd, loc) + + def show_fixtures_per_test(config): from _pytest.main import wrap_session @@ -1432,27 +1506,27 @@ def _show_fixtures_per_test(config: Config, session: Session) -> None: import _pytest.config session.perform_collect() - curdir = py.path.local() + curdir = Path.cwd() tw = _pytest.config.create_terminal_writer(config) verbose = config.getvalue("verbose") - def get_best_relpath(func): + def get_best_relpath(func) -> str: loc = getlocation(func, str(curdir)) - return curdir.bestrelpath(py.path.local(loc)) + return bestrelpath(curdir, Path(loc)) def write_fixture(fixture_def: fixtures.FixtureDef[object]) -> None: argname = fixture_def.argname if verbose <= 0 and argname.startswith("_"): return - if verbose > 0: - bestrel = get_best_relpath(fixture_def.func) - funcargspec = f"{argname} -- {bestrel}" - else: - funcargspec = argname - tw.line(funcargspec, green=True) + prettypath = _pretty_fixture_path(fixture_def.func) + tw.write(f"{argname}", green=True) + tw.write(f" -- {prettypath}", yellow=True) + tw.write("\n") fixture_doc = inspect.getdoc(fixture_def.func) if fixture_doc: - write_docstring(tw, fixture_doc) + write_docstring( + tw, fixture_doc.split("\n\n")[0] if verbose <= 0 else fixture_doc + ) else: tw.line(" no docstring available", red=True) @@ -1465,7 +1539,7 @@ def _show_fixtures_per_test(config: Config, session: Session) -> None: tw.line() tw.sep("-", f"fixtures used by {item.name}") # TODO: Fix this type ignore. - tw.sep("-", "({})".format(get_best_relpath(item.function))) # type: ignore[attr-defined] + tw.sep("-", f"({get_best_relpath(item.function)})") # type: ignore[attr-defined] # dict key not used in loop but needed for sorting. for _, fixturedefs in sorted(info.name2fixturedefs.items()): assert fixturedefs is not None @@ -1488,7 +1562,7 @@ def _showfixtures_main(config: Config, session: Session) -> None: import _pytest.config session.perform_collect() - curdir = py.path.local() + curdir = Path.cwd() tw = _pytest.config.create_terminal_writer(config) verbose = config.getvalue("verbose") @@ -1510,7 +1584,7 @@ def _showfixtures_main(config: Config, session: Session) -> None: ( len(fixturedef.baseid), fixturedef.func.__module__, - curdir.bestrelpath(py.path.local(loc)), + _pretty_fixture_path(fixturedef.func), fixturedef.argname, fixturedef, ) @@ -1518,26 +1592,24 @@ def _showfixtures_main(config: Config, session: Session) -> None: available.sort() currentmodule = None - for baseid, module, bestrel, argname, fixturedef in available: + for baseid, module, prettypath, argname, fixturedef in available: if currentmodule != module: if not module.startswith("_pytest."): tw.line() tw.sep("-", f"fixtures defined from {module}") currentmodule = module - if verbose <= 0 and argname[0] == "_": + if verbose <= 0 and argname.startswith("_"): continue - tw.write(argname, green=True) + tw.write(f"{argname}", green=True) if fixturedef.scope != "function": tw.write(" [%s scope]" % fixturedef.scope, cyan=True) - if verbose > 0: - tw.write(" -- %s" % bestrel, yellow=True) + tw.write(f" -- {prettypath}", yellow=True) tw.write("\n") - loc = getlocation(fixturedef.func, str(curdir)) doc = inspect.getdoc(fixturedef.func) if doc: - write_docstring(tw, doc) + write_docstring(tw, doc.split("\n\n")[0] if verbose <= 0 else doc) else: - tw.line(f" {loc}: no docstring available", red=True) + tw.line(" no docstring available", red=True) tw.line() @@ -1549,26 +1621,26 @@ def write_docstring(tw: TerminalWriter, doc: str, indent: str = " ") -> None: class Function(PyobjMixin, nodes.Item): """An Item responsible for setting up and executing a Python test function. - param name: + :param name: The full function name, including any decorations like those added by parametrization (``my_func[my_param]``). - param parent: + :param parent: The parent Node. - param config: + :param config: The pytest Config object. - param callspec: + :param callspec: If given, this is function has been parametrized and the callspec contains meta information about the parametrization. - param callobj: + :param callobj: If given, the object which will be called when the Function is invoked, otherwise the callobj will be obtained from ``parent`` using ``originalname``. - param keywords: + :param keywords: Keywords bound to the function object for "-k" matching. - param session: + :param session: The pytest Session object. - param fixtureinfo: + :param fixtureinfo: Fixture information already resolved at this fixture node.. - param originalname: + :param originalname: The attribute name to use for accessing the underlying function object. Defaults to ``name``. Set this if name is different from the original name, for example when it contains decorations like those added by parametrization @@ -1615,7 +1687,7 @@ class Function(PyobjMixin, nodes.Item): # this will be redeemed later for mark in callspec.marks: # feel free to cry, this was broken for years before - # and keywords cant fix it per design + # and keywords can't fix it per design self.keywords[mark.name] = mark self.own_markers.extend(normalize_mark_list(callspec.marks)) if keywords: @@ -1656,7 +1728,12 @@ class Function(PyobjMixin, nodes.Item): def _getobj(self): assert self.parent is not None - return getattr(self.parent.obj, self.originalname) # type: ignore[attr-defined] + if isinstance(self.parent, Class): + # Each Function gets a fresh class instance. + parent_obj = self.parent.newinstance() + else: + parent_obj = self.parent.obj # type: ignore[attr-defined] + return getattr(parent_obj, self.originalname) @property def _pyfuncitem(self): @@ -1668,9 +1745,6 @@ class Function(PyobjMixin, nodes.Item): self.ihook.pytest_pyfunc_call(pyfuncitem=self) def setup(self) -> None: - if isinstance(self.parent, Instance): - self.parent.newinstance() - self.obj = self._getobj() self._request._fillfixtures() def _prunetraceback(self, excinfo: ExceptionInfo[BaseException]) -> None: @@ -1696,7 +1770,8 @@ class Function(PyobjMixin, nodes.Item): # TODO: Type ignored -- breaks Liskov Substitution. def repr_failure( # type: ignore[override] - self, excinfo: ExceptionInfo[BaseException], + self, + excinfo: ExceptionInfo[BaseException], ) -> Union[str, TerminalRepr]: style = self.config.getoption("tbstyle", "auto") if style == "auto": |