From bde54d1bb9ac1fb304610fe0994f16136a5ef518 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Thu, 23 Jan 2020 10:46:17 +0200 Subject: [PATCH 1/3] Type annotate main.py and some parts related to collection --- src/_pytest/config/__init__.py | 2 +- src/_pytest/doctest.py | 16 +++-- src/_pytest/hookspec.py | 15 +++- src/_pytest/main.py | 121 +++++++++++++++++++++++---------- src/_pytest/nodes.py | 5 +- src/_pytest/python.py | 63 +++++++++++------ src/_pytest/reports.py | 11 ++- src/_pytest/runner.py | 4 +- src/_pytest/unittest.py | 27 +++++--- 9 files changed, 186 insertions(+), 78 deletions(-) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 45aa4d9a824..45710a70177 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -796,7 +796,7 @@ def __init__(self, pluginmanager, *, invocation_params=None) -> None: ) @property - def invocation_dir(self): + def invocation_dir(self) -> py.path.local: """Backward compatibility""" return py.path.local(str(self.invocation_params.dir)) diff --git a/src/_pytest/doctest.py b/src/_pytest/doctest.py index 0140e0b3a98..d0ee40ad865 100644 --- a/src/_pytest/doctest.py +++ b/src/_pytest/doctest.py @@ -7,6 +7,7 @@ import warnings from contextlib import contextmanager from typing import Dict +from typing import Iterable from typing import List from typing import Optional from typing import Sequence @@ -108,13 +109,18 @@ def pytest_unconfigure(): RUNNER_CLASS = None -def pytest_collect_file(path, parent): +def pytest_collect_file( + path: py.path.local, parent +) -> Optional[Union["DoctestModule", "DoctestTextfile"]]: config = parent.config if path.ext == ".py": if config.option.doctestmodules and not _is_setup_py(config, path, parent): - return DoctestModule.from_parent(parent, fspath=path) + mod = DoctestModule.from_parent(parent, fspath=path) # type: DoctestModule + return mod elif _is_doctest(config, path, parent): - return DoctestTextfile.from_parent(parent, fspath=path) + txt = DoctestTextfile.from_parent(parent, fspath=path) # type: DoctestTextfile + return txt + return None def _is_setup_py(config, path, parent): @@ -361,7 +367,7 @@ def _get_continue_on_failure(config): class DoctestTextfile(pytest.Module): obj = None - def collect(self): + def collect(self) -> Iterable[DoctestItem]: import doctest # inspired by doctest.testfile; ideally we would use it directly, @@ -440,7 +446,7 @@ def _mock_aware_unwrap(obj, stop=None): class DoctestModule(pytest.Module): - def collect(self): + def collect(self) -> Iterable[DoctestItem]: import doctest class MockAwareDocTestFinder(doctest.DocTestFinder): diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index 3cd7f5ffebb..9bf59b52ef9 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -1,13 +1,20 @@ """ hook specifications for pytest plugins, invoked from main.py and builtin plugins. """ from typing import Any +from typing import List from typing import Optional +from typing import Union +import py.path from pluggy import HookspecMarker from _pytest.compat import TYPE_CHECKING if TYPE_CHECKING: from _pytest.main import Session + from _pytest.nodes import Collector + from _pytest.nodes import Item + from _pytest.python import Module + from _pytest.python import PyCollector hookspec = HookspecMarker("pytest") @@ -215,7 +222,7 @@ def pytest_collect_directory(path, parent): """ -def pytest_collect_file(path, parent): +def pytest_collect_file(path: py.path.local, parent) -> "Optional[Collector]": """ return collection Node or None for the given path. Any new node needs to have the specified ``parent`` as a parent. @@ -255,7 +262,7 @@ def pytest_make_collect_report(collector): @hookspec(firstresult=True) -def pytest_pycollect_makemodule(path, parent): +def pytest_pycollect_makemodule(path: py.path.local, parent) -> "Optional[Module]": """ return a Module collector or None for the given path. This hook will be called for each matching test module path. The pytest_collect_file hook needs to be used if you want to @@ -268,7 +275,9 @@ def pytest_pycollect_makemodule(path, parent): @hookspec(firstresult=True) -def pytest_pycollect_makeitem(collector, name, obj): +def pytest_pycollect_makeitem( + collector: "PyCollector", name: str, obj +) -> "Union[None, Item, Collector, List[Union[Item, Collector]]]": """ return custom item/collector for a python object in a module, or None. Stops at first non-None result, see :ref:`firstresult` """ diff --git a/src/_pytest/main.py b/src/_pytest/main.py index dbb6236a3ec..9961a2c69bb 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -5,11 +5,14 @@ import os import sys from typing import Callable +from typing import cast from typing import Dict from typing import FrozenSet +from typing import Iterator from typing import List from typing import Optional from typing import Sequence +from typing import Set from typing import Tuple from typing import Union @@ -18,12 +21,14 @@ import _pytest._code from _pytest import nodes +from _pytest.compat import overload from _pytest.compat import TYPE_CHECKING from _pytest.config import Config from _pytest.config import directory_arg from _pytest.config import ExitCode from _pytest.config import hookimpl from _pytest.config import UsageError +from _pytest.config.argparsing import Parser from _pytest.fixtures import FixtureManager from _pytest.outcomes import Exit from _pytest.reports import CollectReport @@ -33,11 +38,12 @@ if TYPE_CHECKING: from typing import Type + from typing_extensions import Literal from _pytest.python import Package -def pytest_addoption(parser): +def pytest_addoption(parser: Parser) -> None: parser.addini( "norecursedirs", "directory patterns to avoid for recursion", @@ -237,7 +243,7 @@ def wrap_session( return session.exitstatus -def pytest_cmdline_main(config): +def pytest_cmdline_main(config: Config) -> Union[int, ExitCode]: return wrap_session(config, _main) @@ -254,11 +260,11 @@ def _main(config: Config, session: "Session") -> Optional[Union[int, ExitCode]]: return None -def pytest_collection(session): +def pytest_collection(session: "Session") -> Sequence[nodes.Item]: return session.perform_collect() -def pytest_runtestloop(session): +def pytest_runtestloop(session: "Session") -> bool: if session.testsfailed and not session.config.option.continue_on_collection_errors: raise session.Interrupted( "%d error%s during collection" @@ -278,7 +284,7 @@ def pytest_runtestloop(session): return True -def _in_venv(path): +def _in_venv(path: py.path.local) -> bool: """Attempts to detect if ``path`` is the root of a Virtual Environment by checking for the existence of the appropriate activate script""" bindir = path.join("Scripts" if sys.platform.startswith("win") else "bin") @@ -295,7 +301,7 @@ def _in_venv(path): return any([fname.basename in activates for fname in bindir.listdir()]) -def pytest_ignore_collect(path, config): +def pytest_ignore_collect(path: py.path.local, config: Config) -> bool: ignore_paths = config._getconftest_pathlist("collect_ignore", path=path.dirpath()) ignore_paths = ignore_paths or [] excludeopt = config.getoption("ignore") @@ -323,7 +329,7 @@ def pytest_ignore_collect(path, config): return False -def pytest_collection_modifyitems(items, config): +def pytest_collection_modifyitems(items, config: Config) -> None: deselect_prefixes = tuple(config.getoption("deselect") or []) if not deselect_prefixes: return @@ -380,8 +386,8 @@ def __init__(self, config: Config) -> None: ) self.testsfailed = 0 self.testscollected = 0 - self.shouldstop = False - self.shouldfail = False + self.shouldstop = False # type: Union[bool, str] + self.shouldfail = False # type: Union[bool, str] self.trace = config.trace.root.get("collection") self.startdir = config.invocation_dir self._initialpaths = frozenset() # type: FrozenSet[py.path.local] @@ -407,10 +413,11 @@ def __init__(self, config: Config) -> None: self.config.pluginmanager.register(self, name="session") @classmethod - def from_config(cls, config): - return cls._create(config) + def from_config(cls, config: Config) -> "Session": + session = cls._create(config) # type: Session + return session - def __repr__(self): + def __repr__(self) -> str: return "<%s %s exitstatus=%r testsfailed=%d testscollected=%d>" % ( self.__class__.__name__, self.name, @@ -424,14 +431,14 @@ def _node_location_to_relpath(self, node_path: py.path.local) -> str: return self._bestrelpathcache[node_path] @hookimpl(tryfirst=True) - def pytest_collectstart(self): + def pytest_collectstart(self) -> None: if self.shouldfail: raise self.Failed(self.shouldfail) if self.shouldstop: raise self.Interrupted(self.shouldstop) @hookimpl(tryfirst=True) - def pytest_runtest_logreport(self, report): + def pytest_runtest_logreport(self, report) -> None: if report.failed and not hasattr(report, "wasxfail"): self.testsfailed += 1 maxfail = self.config.getvalue("maxfail") @@ -440,13 +447,27 @@ def pytest_runtest_logreport(self, report): pytest_collectreport = pytest_runtest_logreport - def isinitpath(self, path): + def isinitpath(self, path: py.path.local) -> bool: return path in self._initialpaths def gethookproxy(self, fspath: py.path.local): return super()._gethookproxy(fspath) - def perform_collect(self, args=None, genitems=True): + @overload + def perform_collect( + self, args: Optional[Sequence[str]] = ..., genitems: "Literal[True]" = ... + ) -> Sequence[nodes.Item]: + raise NotImplementedError() + + @overload # noqa: F811 + def perform_collect( # noqa: F811 + self, args: Optional[Sequence[str]] = ..., genitems: bool = ... + ) -> Sequence[Union[nodes.Item, nodes.Collector]]: + raise NotImplementedError() + + def perform_collect( # noqa: F811 + self, args: Optional[Sequence[str]] = None, genitems: bool = True + ) -> Sequence[Union[nodes.Item, nodes.Collector]]: hook = self.config.hook try: items = self._perform_collect(args, genitems) @@ -459,15 +480,29 @@ def perform_collect(self, args=None, genitems=True): self.testscollected = len(items) return items - def _perform_collect(self, args, genitems): + @overload + def _perform_collect( + self, args: Optional[Sequence[str]], genitems: "Literal[True]" + ) -> Sequence[nodes.Item]: + raise NotImplementedError() + + @overload # noqa: F811 + def _perform_collect( # noqa: F811 + self, args: Optional[Sequence[str]], genitems: bool + ) -> Sequence[Union[nodes.Item, nodes.Collector]]: + raise NotImplementedError() + + def _perform_collect( # noqa: F811 + self, args: Optional[Sequence[str]], genitems: bool + ) -> Sequence[Union[nodes.Item, nodes.Collector]]: if args is None: args = self.config.args self.trace("perform_collect", self, args) self.trace.root.indent += 1 - self._notfound = [] + self._notfound = [] # type: List[Tuple[str, NoMatch]] initialpaths = [] # type: List[py.path.local] self._initial_parts = [] # type: List[Tuple[py.path.local, List[str]]] - self.items = items = [] + self.items = items = [] # type: List[nodes.Item] for arg in args: fspath, parts = self._parsearg(arg) self._initial_parts.append((fspath, parts)) @@ -490,7 +525,7 @@ def _perform_collect(self, args, genitems): self.items.extend(self.genitems(node)) return items - def collect(self): + def collect(self) -> Iterator[Union[nodes.Item, nodes.Collector]]: for fspath, parts in self._initial_parts: self.trace("processing argument", (fspath, parts)) self.trace.root.indent += 1 @@ -500,7 +535,8 @@ def collect(self): report_arg = "::".join((str(fspath), *parts)) # we are inside a make_report hook so # we cannot directly pass through the exception - self._notfound.append((report_arg, sys.exc_info()[1])) + exc = cast(NoMatch, sys.exc_info()[1]) + self._notfound.append((report_arg, exc)) self.trace.root.indent -= 1 self._collection_node_cache1.clear() @@ -508,7 +544,9 @@ def collect(self): self._collection_node_cache3.clear() self._collection_pkg_roots.clear() - def _collect(self, argpath, names): + def _collect( + self, argpath: py.path.local, names: List[str] + ) -> Iterator[Union[nodes.Item, nodes.Collector]]: from _pytest.python import Package # Start with a Session root, and delve to argpath item (dir or file) @@ -536,7 +574,7 @@ def _collect(self, argpath, names): if argpath.check(dir=1): assert not names, "invalid arg {!r}".format((argpath, names)) - seen_dirs = set() + seen_dirs = set() # type: Set[py.path.local] for path in argpath.visit( fil=self._visit_filter, rec=self._recurse, bf=True, sort=True ): @@ -577,8 +615,9 @@ def _collect(self, argpath, names): # Module itself, so just use that. If this special case isn't taken, then all # the files in the package will be yielded. if argpath.basename == "__init__.py": + assert isinstance(m[0], nodes.Collector) try: - yield next(m[0].collect()) + yield next(iter(m[0].collect())) except StopIteration: # The package collects nothing with only an __init__.py # file in it, which gets ignored by the default @@ -587,7 +626,9 @@ def _collect(self, argpath, names): return yield from m - def _collectfile(self, path, handle_dupes=True): + def _collectfile( + self, path: py.path.local, handle_dupes: bool = True + ) -> Sequence[nodes.Collector]: assert ( path.isfile() ), "{!r} is not a file (isdir={!r}, exists={!r}, islink={!r})".format( @@ -607,13 +648,17 @@ def _collectfile(self, path, handle_dupes=True): else: duplicate_paths.add(path) - return ihook.pytest_collect_file(path=path, parent=self) + collector = ihook.pytest_collect_file( + path=path, parent=self + ) # type: List[nodes.Collector] + return collector @staticmethod - def _visit_filter(f): - return f.check(file=1) + def _visit_filter(f: py.path.local) -> bool: + # TODO: Remove type: ignore once `py` is typed. + return f.check(file=1) # type: ignore - def _tryconvertpyarg(self, x): + def _tryconvertpyarg(self, x: str) -> str: """Convert a dotted module name to path.""" try: spec = importlib.util.find_spec(x) @@ -622,14 +667,14 @@ def _tryconvertpyarg(self, x): # ValueError: not a module name except (AttributeError, ImportError, ValueError): return x - if spec is None or spec.origin in {None, "namespace"}: + if spec is None or spec.origin is None or spec.origin == "namespace": return x elif spec.submodule_search_locations: return os.path.dirname(spec.origin) else: return spec.origin - def _parsearg(self, arg): + def _parsearg(self, arg: str) -> Tuple[py.path.local, List[str]]: """ return (fspath, names) tuple after checking the file exists. """ strpath, *parts = str(arg).split("::") if self.config.option.pyargs: @@ -645,7 +690,9 @@ def _parsearg(self, arg): fspath = fspath.realpath() return (fspath, parts) - def matchnodes(self, matching, names): + def matchnodes( + self, matching: Sequence[Union[nodes.Item, nodes.Collector]], names: List[str], + ) -> Sequence[Union[nodes.Item, nodes.Collector]]: self.trace("matchnodes", matching, names) self.trace.root.indent += 1 nodes = self._matchnodes(matching, names) @@ -656,13 +703,15 @@ def matchnodes(self, matching, names): raise NoMatch(matching, names[:1]) return nodes - def _matchnodes(self, matching, names): + def _matchnodes( + self, matching: Sequence[Union[nodes.Item, nodes.Collector]], names: List[str], + ) -> Sequence[Union[nodes.Item, nodes.Collector]]: if not matching or not names: return matching name = names[0] assert name nextnames = names[1:] - resultnodes = [] + resultnodes = [] # type: List[Union[nodes.Item, nodes.Collector]] for node in matching: if isinstance(node, nodes.Item): if not names: @@ -693,7 +742,9 @@ def _matchnodes(self, matching, names): node.ihook.pytest_collectreport(report=rep) return resultnodes - def genitems(self, node): + def genitems( + self, node: Union[nodes.Item, nodes.Collector] + ) -> Iterator[nodes.Item]: self.trace("genitems", node) if isinstance(node, nodes.Item): node.ihook.pytest_itemcollected(item=node) diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index 81a25ddd5c2..99453941124 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -3,6 +3,7 @@ from functools import lru_cache from typing import Any from typing import Dict +from typing import Iterable from typing import List from typing import Optional from typing import Set @@ -205,7 +206,7 @@ def warn(self, warning): # methods for ordering nodes @property - def nodeid(self): + def nodeid(self) -> str: """ a ::-separated string denoting its collection tree address. """ return self._nodeid @@ -390,7 +391,7 @@ class Collector(Node): class CollectError(Exception): """ an error during collection, contains a custom message. """ - def collect(self): + def collect(self) -> Iterable[Union["Item", "Collector"]]: """ returns a list of children (items and collectors) for this collection node. """ diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 6402164f9e4..a9264667119 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -4,14 +4,17 @@ import inspect import os import sys +import typing import warnings from collections import Counter from collections import defaultdict from collections.abc import Sequence from functools import partial from typing import Dict +from typing import Iterable from typing import List from typing import Optional +from typing import Set from typing import Tuple from typing import Union @@ -177,16 +180,20 @@ def pytest_pyfunc_call(pyfuncitem: "Function"): return True -def pytest_collect_file(path, parent): +def pytest_collect_file(path: py.path.local, parent) -> Optional["Module"]: ext = path.ext if ext == ".py": if not parent.session.isinitpath(path): if not path_matches_patterns( path, parent.config.getini("python_files") + ["__init__.py"] ): - return + return None ihook = parent.session.gethookproxy(path) - return ihook.pytest_pycollect_makemodule(path=path, parent=parent) + module = ihook.pytest_pycollect_makemodule( + path=path, parent=parent + ) # type: Module + return module + return None def path_matches_patterns(path, patterns): @@ -194,14 +201,16 @@ def path_matches_patterns(path, patterns): return any(path.fnmatch(pattern) for pattern in patterns) -def pytest_pycollect_makemodule(path, parent): +def pytest_pycollect_makemodule(path: py.path.local, parent) -> "Module": if path.basename == "__init__.py": - return Package.from_parent(parent, fspath=path) - return Module.from_parent(parent, fspath=path) + pkg = Package.from_parent(parent, fspath=path) # type: Package + return pkg + mod = Module.from_parent(parent, fspath=path) # type: Module + return mod @hookimpl(hookwrapper=True) -def pytest_pycollect_makeitem(collector, name, obj): +def pytest_pycollect_makeitem(collector: "PyCollector", name: str, obj): outcome = yield res = outcome.get_result() if res is not None: @@ -353,7 +362,7 @@ def _matches_prefix_or_glob_option(self, option_name, name): return True return False - def collect(self): + def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]: if not getattr(self.obj, "__test__", True): return [] @@ -362,8 +371,8 @@ def collect(self): dicts = [getattr(self.obj, "__dict__", {})] for basecls in inspect.getmro(self.obj.__class__): dicts.append(basecls.__dict__) - seen = {} - values = [] + seen = {} # type: Dict[str, bool] + values = [] # type: List[Union[nodes.Item, nodes.Collector]] for dic in dicts: for name, obj in list(dic.items()): if name in seen: @@ -383,9 +392,16 @@ def sort_key(item): values.sort(key=sort_key) return values - def _makeitem(self, name, obj): + def _makeitem( + self, name: str, obj + ) -> Union[ + None, nodes.Item, nodes.Collector, List[Union[nodes.Item, nodes.Collector]] + ]: # assert self.ihook.fspath == self.fspath, self - return self.ihook.pytest_pycollect_makeitem(collector=self, name=name, obj=obj) + item = self.ihook.pytest_pycollect_makeitem( + collector=self, name=name, obj=obj + ) # type: Union[None, nodes.Item, nodes.Collector, List[Union[nodes.Item, nodes.Collector]]] + return item def _genfunctions(self, name, funcobj): module = self.getparent(Module).obj @@ -437,7 +453,7 @@ class Module(nodes.File, PyCollector): def _getobj(self): return self._importtestmodule() - def collect(self): + def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]: self._inject_setup_module_fixture() self._inject_setup_function_fixture() self.session._fixturemanager.parsefactories(self) @@ -584,7 +600,9 @@ def setup(self): def gethookproxy(self, fspath: py.path.local): return super()._gethookproxy(fspath) - def _collectfile(self, path, handle_dupes=True): + def _collectfile( + self, path: py.path.local, handle_dupes: bool = True + ) -> typing.Sequence[nodes.Collector]: assert ( path.isfile() ), "{!r} is not a file (isdir={!r}, exists={!r}, islink={!r})".format( @@ -607,19 +625,22 @@ def _collectfile(self, path, handle_dupes=True): if self.fspath == path: # __init__.py return [self] - return ihook.pytest_collect_file(path=path, parent=self) + collectors = ihook.pytest_collect_file( + path=path, parent=self + ) # type: List[nodes.Collector] + return collectors - def isinitpath(self, path): + def isinitpath(self, path: py.path.local) -> bool: return path in self.session._initialpaths - def collect(self): + 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( init_module, self.config.getini("python_files") ): yield Module.from_parent(self, fspath=init_module) - pkg_prefixes = set() + pkg_prefixes = set() # type: Set[py.path.local] for path in this_path.visit(rec=self._recurse, bf=True, sort=True): # We will visit our own __init__.py file, in which case we skip it. is_file = path.isfile() @@ -676,10 +697,11 @@ def from_parent(cls, parent, *, name, obj=None): """ return super().from_parent(name=name, parent=parent) - def collect(self): + def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]: if not safe_getattr(self.obj, "__test__", True): return [] if hasinit(self.obj): + assert self.parent is not None self.warn( PytestCollectionWarning( "cannot collect test class %r because it has a " @@ -689,6 +711,7 @@ def collect(self): ) return [] elif hasnew(self.obj): + assert self.parent is not None self.warn( PytestCollectionWarning( "cannot collect test class %r because it has a " @@ -762,7 +785,7 @@ class Instance(PyCollector): def _getobj(self): return self.parent.obj() - def collect(self): + def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]: self.session._fixturemanager.parsefactories(self) return super().collect() diff --git a/src/_pytest/reports.py b/src/_pytest/reports.py index 3ad67c224c4..d3bfc91979a 100644 --- a/src/_pytest/reports.py +++ b/src/_pytest/reports.py @@ -20,7 +20,8 @@ from _pytest._code.code import TerminalRepr from _pytest._io import TerminalWriter from _pytest.compat import TYPE_CHECKING -from _pytest.nodes import Node +from _pytest.nodes import Collector +from _pytest.nodes import Item from _pytest.outcomes import skip from _pytest.pathlib import Path @@ -315,7 +316,13 @@ class CollectReport(BaseReport): when = "collect" def __init__( - self, nodeid: str, outcome, longrepr, result: List[Node], sections=(), **extra + self, + nodeid: str, + outcome, + longrepr, + result: Optional[List[Union[Item, Collector]]], + sections=(), + **extra ) -> None: self.nodeid = nodeid self.outcome = outcome diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py index e10e4d8bdf0..3eb9ca28667 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -373,10 +373,10 @@ def prepare(self, colitem): raise -def collect_one_node(collector): +def collect_one_node(collector: Collector) -> CollectReport: ihook = collector.ihook ihook.pytest_collectstart(collector=collector) - rep = ihook.pytest_make_collect_report(collector=collector) + rep = ihook.pytest_make_collect_report(collector=collector) # type: CollectReport call = rep.__dict__.pop("call", None) if call and check_interactive_exception(call, rep): ihook.pytest_exception_interact(node=collector, call=call, report=rep) diff --git a/src/_pytest/unittest.py b/src/_pytest/unittest.py index a5512e9443c..3740ff1249a 100644 --- a/src/_pytest/unittest.py +++ b/src/_pytest/unittest.py @@ -2,29 +2,40 @@ import functools import sys import traceback +from typing import Iterable +from typing import Optional +from typing import Union import _pytest._code import pytest from _pytest.compat import getimfunc from _pytest.config import hookimpl +from _pytest.nodes import Collector +from _pytest.nodes import Item from _pytest.outcomes import exit from _pytest.outcomes import fail from _pytest.outcomes import skip from _pytest.outcomes import xfail from _pytest.python import Class from _pytest.python import Function +from _pytest.python import PyCollector from _pytest.runner import CallInfo -def pytest_pycollect_makeitem(collector, name, obj): +def pytest_pycollect_makeitem( + collector: PyCollector, name: str, obj +) -> Optional["UnitTestCase"]: # has unittest been imported and is obj a subclass of its TestCase? try: - if not issubclass(obj, sys.modules["unittest"].TestCase): - return + ut = sys.modules["unittest"] + # Type ignored because `ut` is an opaque module. + if not issubclass(obj, ut.TestCase): # type: ignore + return None except Exception: - return + return None # yes, so let's collect it - return UnitTestCase.from_parent(collector, name=name, obj=obj) + item = UnitTestCase.from_parent(collector, name=name, obj=obj) # type: UnitTestCase + return item class UnitTestCase(Class): @@ -32,7 +43,7 @@ class UnitTestCase(Class): # to declare that our children do not support funcargs nofuncargs = True - def collect(self): + def collect(self) -> Iterable[Union[Item, Collector]]: from unittest import TestLoader cls = self.obj @@ -59,8 +70,8 @@ def collect(self): runtest = getattr(self.obj, "runTest", None) if runtest is not None: ut = sys.modules.get("twisted.trial.unittest", None) - if ut is None or runtest != ut.TestCase.runTest: - # TODO: callobj consistency + # Type ignored because `ut` is an opaque module. + if ut is None or runtest != ut.TestCase.runTest: # type: ignore yield TestCaseFunction.from_parent(self, name="runTest") def _inject_setup_teardown_fixtures(self, cls): From 60f5a71ab3969f60be64f3a88623ae229a46c527 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Mon, 10 Feb 2020 14:35:51 +0200 Subject: [PATCH 2/3] Add an assortment of type annotations --- src/_pytest/assertion/__init__.py | 5 +- src/_pytest/cacheprovider.py | 18 ++- src/_pytest/capture.py | 6 +- src/_pytest/compat.py | 2 +- src/_pytest/config/__init__.py | 35 ++-- src/_pytest/debugging.py | 11 +- src/_pytest/doctest.py | 5 +- src/_pytest/faulthandler.py | 20 ++- src/_pytest/fixtures.py | 257 +++++++++++++++++++----------- src/_pytest/helpconfig.py | 14 +- src/_pytest/hookspec.py | 81 ++++++---- src/_pytest/junitxml.py | 19 ++- src/_pytest/logging.py | 18 ++- src/_pytest/mark/__init__.py | 25 ++- src/_pytest/mark/structures.py | 89 ++++++++--- src/_pytest/nodes.py | 5 +- src/_pytest/pastebin.py | 20 ++- src/_pytest/pytester.py | 8 +- src/_pytest/python.py | 161 +++++++++++++------ src/_pytest/resultlog.py | 16 +- src/_pytest/runner.py | 29 ++-- src/_pytest/setuponly.py | 35 ++-- src/_pytest/setupplan.py | 18 ++- src/_pytest/skipping.py | 8 +- src/_pytest/stepwise.py | 11 +- src/_pytest/terminal.py | 13 +- src/_pytest/tmpdir.py | 3 +- src/_pytest/warnings.py | 8 +- testing/python/metafunc.py | 15 +- 29 files changed, 618 insertions(+), 337 deletions(-) diff --git a/src/_pytest/assertion/__init__.py b/src/_pytest/assertion/__init__.py index cdb0347034f..2cf8d65b0ac 100644 --- a/src/_pytest/assertion/__init__.py +++ b/src/_pytest/assertion/__init__.py @@ -9,12 +9,13 @@ from _pytest.assertion import util from _pytest.compat import TYPE_CHECKING from _pytest.config import hookimpl +from _pytest.config.argparsing import Parser if TYPE_CHECKING: from _pytest.main import Session -def pytest_addoption(parser): +def pytest_addoption(parser: Parser) -> None: group = parser.getgroup("debugconfig") group.addoption( "--assert", @@ -162,7 +163,7 @@ def call_assertion_pass_hook(lineno, orig, expl): util._reprcompare, util._assertion_pass = saved_assert_hooks -def pytest_sessionfinish(session): +def pytest_sessionfinish(session: "Session") -> None: assertstate = getattr(session.config, "_assertstate", None) if assertstate: if assertstate.hook is not None: diff --git a/src/_pytest/cacheprovider.py b/src/_pytest/cacheprovider.py index 2f7f8845440..e069c1e325d 100755 --- a/src/_pytest/cacheprovider.py +++ b/src/_pytest/cacheprovider.py @@ -8,6 +8,8 @@ import os from collections import OrderedDict from typing import List +from typing import Optional +from typing import Union import attr import py @@ -19,6 +21,8 @@ from _pytest import nodes from _pytest._io import TerminalWriter from _pytest.config import Config +from _pytest.config import ExitCode +from _pytest.config.argparsing import Parser from _pytest.main import Session README_CONTENT = """\ @@ -264,7 +268,7 @@ def pytest_collection_modifyitems(self, session, config, items): else: self._report_status += "not deselecting items." - def pytest_sessionfinish(self, session): + def pytest_sessionfinish(self, session: Session) -> None: config = self.config if config.getoption("cacheshow") or hasattr(config, "slaveinput"): return @@ -306,7 +310,7 @@ def pytest_collection_modifyitems( def _get_increasing_order(self, items): return sorted(items, key=lambda item: item.fspath.mtime(), reverse=True) - def pytest_sessionfinish(self, session): + def pytest_sessionfinish(self, session: Session) -> None: config = self.config if config.getoption("cacheshow") or hasattr(config, "slaveinput"): return @@ -314,7 +318,7 @@ def pytest_sessionfinish(self, session): config.cache.set("cache/nodeids", self.cached_nodeids) -def pytest_addoption(parser): +def pytest_addoption(parser: Parser) -> None: group = parser.getgroup("general") group.addoption( "--lf", @@ -372,16 +376,18 @@ def pytest_addoption(parser): ) -def pytest_cmdline_main(config): +def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]: if config.option.cacheshow: from _pytest.main import wrap_session return wrap_session(config, cacheshow) + return None @pytest.hookimpl(tryfirst=True) -def pytest_configure(config): - config.cache = Cache.for_config(config) +def pytest_configure(config: Config) -> None: + # Type ignored: pending mechanism to store typed objects scoped to config. + config.cache = Cache.for_config(config) # type: ignore # noqa: F821 config.pluginmanager.register(LFPlugin(config), "lfplugin") config.pluginmanager.register(NFPlugin(config), "nfplugin") diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index bd17d05ef59..e7c7c12bfa2 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -18,12 +18,12 @@ from _pytest.compat import CaptureAndPassthroughIO from _pytest.compat import CaptureIO from _pytest.config import Config -from _pytest.fixtures import FixtureRequest +from _pytest.config.argparsing import Parser patchsysdict = {0: "stdin", 1: "stdout", 2: "stderr"} -def pytest_addoption(parser): +def pytest_addoption(parser: Parser) -> None: group = parser.getgroup("general") group._addoption( "--capture", @@ -150,7 +150,7 @@ def read_global_capture(self): @contextlib.contextmanager def _capturing_for_request( - self, request: FixtureRequest + self, request ) -> Generator["CaptureFixture", None, None]: if self._capture_fixture: other_name = next( diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index a7c582470a4..1845d9d91ef 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -227,7 +227,7 @@ def _bytes_to_ascii(val: bytes) -> str: return val.decode("ascii", "backslashreplace") -def ascii_escaped(val: Union[bytes, str]): +def ascii_escaped(val: Union[bytes, str]) -> str: """If val is pure ascii, returns it as a str(). Otherwise, escapes bytes objects into a sequence of escaped bytes: diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 45710a70177..46df9144b96 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -282,7 +282,7 @@ class PytestPluginManager(PluginManager): * ``conftest.py`` loading during start-up; """ - def __init__(self): + def __init__(self) -> None: import _pytest.assertion super().__init__("pytest") @@ -362,7 +362,7 @@ def parse_hookspec_opts(self, module_or_class, name): } return opts - def register(self, plugin, name=None): + def register(self, plugin: _PluggyPlugin, name: Optional[str] = None): if name in _pytest.deprecated.DEPRECATED_EXTERNAL_PLUGINS: warnings.warn( PytestConfigWarning( @@ -391,7 +391,7 @@ def hasplugin(self, name): """Return True if the plugin with the given name is registered.""" return bool(self.get_plugin(name)) - def pytest_configure(self, config): + def pytest_configure(self, config: "Config") -> None: # XXX now that the pluginmanager exposes hookimpl(tryfirst...) # we should remove tryfirst/trylast as markers config.addinivalue_line( @@ -522,7 +522,7 @@ def _importconftest(self, conftestpath): # # - def consider_preparse(self, args, *, exclude_only=False): + def consider_preparse(self, args, *, exclude_only: bool = False) -> None: i = 0 n = len(args) while i < n: @@ -543,7 +543,7 @@ def consider_preparse(self, args, *, exclude_only=False): continue self.consider_pluginarg(parg) - def consider_pluginarg(self, arg): + def consider_pluginarg(self, arg) -> None: if arg.startswith("no:"): name = arg[3:] if name in essential_plugins: @@ -568,13 +568,13 @@ def consider_pluginarg(self, arg): del self._name2plugin["pytest_" + name] self.import_plugin(arg, consider_entry_points=True) - def consider_conftest(self, conftestmodule): + def consider_conftest(self, conftestmodule) -> None: self.register(conftestmodule, name=conftestmodule.__file__) - def consider_env(self): + def consider_env(self) -> None: self._import_plugin_specs(os.environ.get("PYTEST_PLUGINS")) - def consider_module(self, mod): + def consider_module(self, mod: types.ModuleType) -> None: self._import_plugin_specs(getattr(mod, "pytest_plugins", [])) def _import_plugin_specs(self, spec): @@ -582,7 +582,7 @@ def _import_plugin_specs(self, spec): for import_spec in plugins: self.import_plugin(import_spec) - def import_plugin(self, modname, consider_entry_points=False): + def import_plugin(self, modname: str, consider_entry_points: bool = False) -> None: """ Imports a plugin with ``modname``. If ``consider_entry_points`` is True, entry point names are also considered to find a plugin. @@ -766,7 +766,12 @@ class InvocationParams: plugins = attr.ib() dir = attr.ib(type=Path) - def __init__(self, pluginmanager, *, invocation_params=None) -> None: + def __init__( + self, + pluginmanager: PytestPluginManager, + *, + invocation_params: Optional[InvocationParams] = None, + ) -> None: from .argparsing import Parser, FILE_OR_DIR if invocation_params is None: @@ -800,19 +805,19 @@ def invocation_dir(self) -> py.path.local: """Backward compatibility""" return py.path.local(str(self.invocation_params.dir)) - def add_cleanup(self, func): + def add_cleanup(self, func) -> None: """ Add a function to be called when the config object gets out of use (usually coninciding with pytest_unconfigure).""" self._cleanup.append(func) - def _do_configure(self): + def _do_configure(self) -> None: assert not self._configured self._configured = True with warnings.catch_warnings(): warnings.simplefilter("default") self.hook.pytest_configure.call_historic(kwargs=dict(config=self)) - def _ensure_unconfigure(self): + def _ensure_unconfigure(self) -> None: if self._configured: self._configured = False self.hook.pytest_unconfigure(config=self) @@ -824,7 +829,9 @@ def _ensure_unconfigure(self): def get_terminal_writer(self): return self.pluginmanager.get_plugin("terminalreporter")._tw - def pytest_cmdline_parse(self, pluginmanager, args): + def pytest_cmdline_parse( + self, pluginmanager: PytestPluginManager, args: List[str] + ) -> object: try: self.parse(args) except UsageError: diff --git a/src/_pytest/debugging.py b/src/_pytest/debugging.py index 9155d7e98e3..fdbacded891 100644 --- a/src/_pytest/debugging.py +++ b/src/_pytest/debugging.py @@ -4,7 +4,10 @@ import sys from _pytest import outcomes +from _pytest.config import Config from _pytest.config import hookimpl +from _pytest.config import PytestPluginManager +from _pytest.config.argparsing import Parser from _pytest.config.exceptions import UsageError @@ -19,7 +22,7 @@ def _validate_usepdb_cls(value): return (modname, classname) -def pytest_addoption(parser): +def pytest_addoption(parser: Parser) -> None: group = parser.getgroup("general") group._addoption( "--pdb", @@ -43,7 +46,7 @@ def pytest_addoption(parser): ) -def pytest_configure(config): +def pytest_configure(config: Config) -> None: import pdb if config.getvalue("trace"): @@ -73,8 +76,8 @@ def fin(): class pytestPDB: """ Pseudo PDB that defers to the real pdb. """ - _pluginmanager = None - _config = None + _pluginmanager = None # type: PytestPluginManager + _config = None # type: Config _saved = [] # type: list _recursive_debug = 0 _wrapped_pdb_cls = None diff --git a/src/_pytest/doctest.py b/src/_pytest/doctest.py index d0ee40ad865..8c7709db6cd 100644 --- a/src/_pytest/doctest.py +++ b/src/_pytest/doctest.py @@ -24,6 +24,7 @@ from _pytest._io import TerminalWriter from _pytest.compat import safe_getattr from _pytest.compat import TYPE_CHECKING +from _pytest.config.argparsing import Parser from _pytest.fixtures import FixtureRequest from _pytest.outcomes import Skipped from _pytest.python_api import approx @@ -53,7 +54,7 @@ CHECKER_CLASS = None # type: Optional[Type[doctest.OutputChecker]] -def pytest_addoption(parser): +def pytest_addoption(parser: Parser) -> None: parser.addini( "doctest_optionflags", "option flags for doctests", @@ -103,7 +104,7 @@ def pytest_addoption(parser): ) -def pytest_unconfigure(): +def pytest_unconfigure() -> None: global RUNNER_CLASS RUNNER_CLASS = None diff --git a/src/_pytest/faulthandler.py b/src/_pytest/faulthandler.py index ed2dfd02504..398321bf028 100644 --- a/src/_pytest/faulthandler.py +++ b/src/_pytest/faulthandler.py @@ -3,9 +3,11 @@ import sys import pytest +from _pytest.config import Config +from _pytest.config.argparsing import Parser -def pytest_addoption(parser): +def pytest_addoption(parser: Parser) -> None: help = ( "Dump the traceback of all threads if a test takes " "more than TIMEOUT seconds to finish.\n" @@ -14,7 +16,7 @@ def pytest_addoption(parser): parser.addini("faulthandler_timeout", help, default=0.0) -def pytest_configure(config): +def pytest_configure(config: Config) -> None: import faulthandler if not faulthandler.is_enabled(): @@ -42,14 +44,16 @@ class FaultHandlerHooks: """Implements hooks that will actually install fault handler before tests execute, as well as correctly handle pdb and internal errors.""" - def pytest_configure(self, config): + def pytest_configure(self, config: Config) -> None: import faulthandler stderr_fd_copy = os.dup(self._get_stderr_fileno()) - config.fault_handler_stderr = os.fdopen(stderr_fd_copy, "w") - faulthandler.enable(file=config.fault_handler_stderr) + fault_handler_stderr = os.fdopen(stderr_fd_copy, "w") + # Type ignored: pending mechanism to store typed objects scoped to config. + config.fault_handler_stderr = fault_handler_stderr # type: ignore # noqa: F821 + faulthandler.enable(file=fault_handler_stderr) - def pytest_unconfigure(self, config): + def pytest_unconfigure(self, config: Config) -> None: import faulthandler faulthandler.disable() @@ -57,8 +61,8 @@ def pytest_unconfigure(self, config): # re-enable the faulthandler, attaching it to the default sys.stderr # so we can see crashes after pytest has finished, usually during # garbage collection during interpreter shutdown - config.fault_handler_stderr.close() - del config.fault_handler_stderr + config.fault_handler_stderr.close() # type: ignore[attr-defined] # noqa: F821 + del config.fault_handler_stderr # type: ignore[attr-defined] # noqa: F821 faulthandler.enable(file=self._get_stderr_fileno()) @staticmethod diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index bd2abb3855d..1a8a6d49500 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -5,9 +5,18 @@ from collections import defaultdict from collections import deque from collections import OrderedDict +from types import TracebackType +from typing import Callable +from typing import cast from typing import Dict +from typing import Iterable +from typing import Iterator from typing import List +from typing import Optional +from typing import Sequence +from typing import Set from typing import Tuple +from typing import Union import attr import py @@ -28,6 +37,9 @@ from _pytest.compat import NOTSET from _pytest.compat import safe_getattr from _pytest.compat import TYPE_CHECKING +from _pytest.config import _PluggyPlugin +from _pytest.config import Config +from _pytest.config.argparsing import Parser from _pytest.deprecated import FIXTURE_POSITIONAL_ARGUMENTS from _pytest.deprecated import FUNCARGNAMES from _pytest.mark import ParameterSet @@ -35,19 +47,34 @@ from _pytest.outcomes import TEST_OUTCOME if TYPE_CHECKING: + from typing import Literal + from typing import NoReturn from typing import Type from _pytest import nodes from _pytest.main import Session + from _pytest.python import Metafunc + + _Scope = Literal["session", "package", "module", "class", "function"] + + +_FixtureCachedResult = Tuple[ + # The result. + Optional[object], + # Cache key> + object, + # Exc info if raised. + Optional[Tuple["Type[BaseException]", BaseException, TracebackType]], +] @attr.s(frozen=True) class PseudoFixtureDef: - cached_result = attr.ib() - scope = attr.ib() + cached_result = attr.ib(type="_FixtureCachedResult") + scope = attr.ib(type="_Scope") -def pytest_sessionstart(session: "Session"): +def pytest_sessionstart(session: "Session") -> None: import _pytest.python import _pytest.nodes @@ -89,7 +116,7 @@ def provide(self): return decoratescope -def get_scope_package(node, fixturedef): +def get_scope_package(node, fixturedef: "FixtureDef"): import pytest cls = pytest.Package @@ -111,7 +138,9 @@ def get_scope_node(node, scope): return node.getparent(cls) -def add_funcarg_pseudo_fixture_def(collector, metafunc, fixturemanager): +def add_funcarg_pseudo_fixture_def( + collector, metafunc: "Metafunc", fixturemanager: "FixtureManager" +) -> None: # this function will transform all collected calls to a functions # if they use direct funcargs (i.e. direct parametrization) # because we want later test execution to be able to rely on @@ -121,8 +150,8 @@ def add_funcarg_pseudo_fixture_def(collector, metafunc, fixturemanager): if not metafunc._calls[0].funcargs: return # this function call does not have direct parametrization # collect funcargs of all callspecs into a list of values - arg2params = {} - arg2scope = {} + arg2params = {} # type: Dict[str, List[object]] + arg2scope = {} # type: Dict[str, _Scope] for callspec in metafunc._calls: for argname, argvalue in callspec.funcargs.items(): assert argname not in callspec.params @@ -228,7 +257,7 @@ def reorder_items(items): return list(reorder_items_atscope(items, argkeys_cache, items_by_argkey, 0)) -def fix_cache_order(item, argkeys_cache, items_by_argkey): +def fix_cache_order(item, argkeys_cache, items_by_argkey) -> None: for scopenum in range(0, scopenum_function): for key in argkeys_cache[scopenum].get(item, []): items_by_argkey[scopenum][key].appendleft(item) @@ -274,7 +303,7 @@ def reorder_items_atscope(items, argkeys_cache, items_by_argkey, scopenum): return items_done -def fillfixtures(function): +def fillfixtures(function) -> None: """ fill missing funcargs for a test function. """ try: request = function._request @@ -303,15 +332,15 @@ def get_direct_param_fixture_func(request): @attr.s(slots=True) class FuncFixtureInfo: # original function argument names - argnames = attr.ib(type=tuple) + argnames = attr.ib(type=Tuple[str, ...]) # argnames that function immediately requires. These include argnames + # fixture names specified via usefixtures and via autouse=True in fixture # definitions. - initialnames = attr.ib(type=tuple) - names_closure = attr.ib() # List[str] - name2fixturedefs = attr.ib() # List[str, List[FixtureDef]] + initialnames = attr.ib(type=Tuple[str, ...]) + names_closure = attr.ib(type=List[str]) + name2fixturedefs = attr.ib(type=Dict[str, Sequence["FixtureDef"]]) - def prune_dependency_tree(self): + def prune_dependency_tree(self) -> None: """Recompute names_closure from initialnames and name2fixturedefs Can only reduce names_closure, which means that the new closure will @@ -322,7 +351,7 @@ def prune_dependency_tree(self): tree. In this way the dependency tree can get pruned, and the closure of argnames may get reduced. """ - closure = set() + closure = set() # type: Set[str] working_set = set(self.initialnames) while working_set: argname = working_set.pop() @@ -347,27 +376,29 @@ class FixtureRequest: the fixture is parametrized indirectly. """ - def __init__(self, pyfuncitem): + def __init__(self, pyfuncitem) -> None: self._pyfuncitem = pyfuncitem #: fixture for which this request is being performed self.fixturename = None #: Scope string, one of "function", "class", "module", "session" - self.scope = "function" + self.scope = "function" # type: _Scope self._fixture_defs = {} # type: Dict[str, FixtureDef] - fixtureinfo = pyfuncitem._fixtureinfo + fixtureinfo = pyfuncitem._fixtureinfo # type: FuncFixtureInfo self._arg2fixturedefs = fixtureinfo.name2fixturedefs.copy() - self._arg2index = {} - self._fixturemanager = pyfuncitem.session._fixturemanager + self._arg2index = {} # type: Dict[str, int] + self._fixturemanager = ( + pyfuncitem.session._fixturemanager + ) # type: FixtureManager @property - def fixturenames(self): + def fixturenames(self) -> List[str]: """names of all active fixtures in this request""" result = list(self._pyfuncitem._fixtureinfo.names_closure) result.extend(set(self._fixture_defs).difference(result)) return result @property - def funcargnames(self): + def funcargnames(self) -> List[str]: """ alias attribute for ``fixturenames`` for pre-2.3 compatibility""" warnings.warn(FUNCARGNAMES, stacklevel=2) return self.fixturenames @@ -377,15 +408,18 @@ def node(self): """ underlying collection node (depends on current request scope)""" return self._getscopeitem(self.scope) - def _getnextfixturedef(self, argname): + def _getnextfixturedef(self, argname: str) -> "FixtureDef": fixturedefs = self._arg2fixturedefs.get(argname, None) if fixturedefs is None: # we arrive here because of a dynamic call to # getfixturevalue(argname) usage which was naturally # not known at parsing/collection time + assert self._pyfuncitem.parent is not None parentid = self._pyfuncitem.parent.nodeid fixturedefs = self._fixturemanager.getfixturedefs(argname, parentid) - self._arg2fixturedefs[argname] = fixturedefs + # TODO: Fix this type ignore. Either add assert or adjust types. + # Can this be None here? + self._arg2fixturedefs[argname] = fixturedefs # type: ignore[assignment] # noqa: F821 # fixturedefs list is immutable so we maintain a decreasing index index = self._arg2index.get(argname, 0) - 1 if fixturedefs is None or (-index > len(fixturedefs)): @@ -441,20 +475,20 @@ def session(self): """ pytest session object. """ return self._pyfuncitem.session - def addfinalizer(self, finalizer): + def addfinalizer(self, finalizer: Callable[[], object]) -> None: """ add finalizer/teardown function to be called after the last test within the requesting test context finished execution. """ # XXX usually this method is shadowed by fixturedef specific ones self._addfinalizer(finalizer, scope=self.scope) - def _addfinalizer(self, finalizer, scope): + def _addfinalizer(self, finalizer: Callable[[], object], scope) -> None: colitem = self._getscopeitem(scope) self._pyfuncitem.session._setupstate.addfinalizer( finalizer=finalizer, colitem=colitem ) - def applymarker(self, marker): + def applymarker(self, marker) -> None: """ Apply a marker to a single test function invocation. This method is useful if you don't want to have a keyword/marker on all function invocations. @@ -464,18 +498,18 @@ def applymarker(self, marker): """ self.node.add_marker(marker) - def raiseerror(self, msg): + def raiseerror(self, msg: Optional[str]) -> "NoReturn": """ raise a FixtureLookupError with the given message. """ raise self._fixturemanager.FixtureLookupError(None, self, msg) - def _fillfixtures(self): + 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): + def getfixturevalue(self, argname: str) -> object: """ Dynamically run a named fixture function. Declaring fixtures via function argument is recommended where possible. @@ -483,9 +517,13 @@ def getfixturevalue(self, argname): setup time, you may use this function to retrieve it inside a fixture or test function body. """ - return self._get_active_fixturedef(argname).cached_result[0] + fixturedef = self._get_active_fixturedef(argname) + assert fixturedef.cached_result is not None + return fixturedef.cached_result[0] - def _get_active_fixturedef(self, argname): + def _get_active_fixturedef( + self, argname: str + ) -> Union["FixtureDef", PseudoFixtureDef]: try: return self._fixture_defs[argname] except KeyError: @@ -494,7 +532,7 @@ def _get_active_fixturedef(self, argname): except FixtureLookupError: if argname == "request": cached_result = (self, [0], None) - scope = "function" + scope = "function" # type: _Scope return PseudoFixtureDef(cached_result, scope) raise # remove indent to prevent the python3 exception @@ -503,15 +541,16 @@ def _get_active_fixturedef(self, argname): self._fixture_defs[argname] = fixturedef return fixturedef - def _get_fixturestack(self): + def _get_fixturestack(self) -> List["FixtureDef"]: current = self - values = [] + values = [] # type: List[FixtureDef] while 1: fixturedef = getattr(current, "_fixturedef", None) if fixturedef is None: values.reverse() return values values.append(fixturedef) + assert isinstance(current, SubRequest) current = current._parent_request def _compute_fixture_value(self, fixturedef: "FixtureDef") -> None: @@ -584,13 +623,15 @@ def _compute_fixture_value(self, fixturedef: "FixtureDef") -> None: finally: self._schedule_finalizers(fixturedef, subrequest) - def _schedule_finalizers(self, fixturedef, subrequest): + def _schedule_finalizers( + self, fixturedef: "FixtureDef", subrequest: "SubRequest" + ) -> None: # if fixture function failed it might have registered finalizers self.session._setupstate.addfinalizer( functools.partial(fixturedef.finish, request=subrequest), subrequest.node ) - def _check_scope(self, argname, invoking_scope, requested_scope): + def _check_scope(self, argname, invoking_scope: "_Scope", requested_scope) -> None: if argname == "request": return if scopemismatch(invoking_scope, requested_scope): @@ -604,7 +645,7 @@ def _check_scope(self, argname, invoking_scope, requested_scope): pytrace=False, ) - def _factorytraceback(self): + def _factorytraceback(self) -> List[str]: lines = [] for fixturedef in self._get_fixturestack(): factory = fixturedef.func @@ -630,7 +671,7 @@ def _getscopeitem(self, scope): ) return node - def __repr__(self): + def __repr__(self) -> str: return "" % (self.node) @@ -638,7 +679,9 @@ class SubRequest(FixtureRequest): """ a sub request for handling getting a fixture from a test function/fixture. """ - def __init__(self, request, scope, param, param_index, fixturedef): + def __init__( + self, request: "FixtureRequest", scope: "_Scope", param, param_index, fixturedef + ) -> None: self._parent_request = request self.fixturename = fixturedef.argname if param is not NOTSET: @@ -652,13 +695,15 @@ def __init__(self, request, scope, param, param_index, fixturedef): self._arg2index = request._arg2index self._fixturemanager = request._fixturemanager - def __repr__(self): + def __repr__(self) -> str: return "".format(self.fixturename, self._pyfuncitem) - def addfinalizer(self, finalizer): + def addfinalizer(self, finalizer: Callable[[], object]) -> None: self._fixturedef.addfinalizer(finalizer) - def _schedule_finalizers(self, fixturedef, subrequest): + def _schedule_finalizers( + self, fixturedef: "FixtureDef", subrequest: "SubRequest" + ) -> None: # if the executing fixturedef was not explicitly requested in the argument list (via # getfixturevalue inside the fixture call) then ensure this fixture def will be finished # first @@ -669,20 +714,21 @@ def _schedule_finalizers(self, fixturedef, subrequest): super()._schedule_finalizers(fixturedef, subrequest) -scopes = "session package module class function".split() +scopes = ["session", "package", "module", "class", "function"] # type: List[_Scope] scopenum_function = scopes.index("function") -def scopemismatch(currentscope, newscope): +def scopemismatch(currentscope: "_Scope", newscope: "_Scope") -> bool: return scopes.index(newscope) > scopes.index(currentscope) -def scope2index(scope, descr, where=None): +def scope2index(scope: str, descr: str, where: Optional[str] = None) -> int: """Look up the index of ``scope`` and raise a descriptive value error if not defined. """ + strscopes = scopes # type: Sequence[str] try: - return scopes.index(scope) + return strscopes.index(scope) except ValueError: fail( "{} {}got an unexpected scope value '{}'".format( @@ -695,7 +741,7 @@ def scope2index(scope, descr, where=None): class FixtureLookupError(LookupError): """ could not return a requested Fixture (missing or invalid). """ - def __init__(self, argname, request, msg=None): + def __init__(self, argname, request, msg: Optional[str] = None) -> None: self.argname = argname self.request = request self.fixturestack = request._get_fixturestack() @@ -773,14 +819,14 @@ def toterminal(self, tw: TerminalWriter) -> None: tw.line("%s:%d" % (self.filename, self.firstlineno + 1)) -def fail_fixturefunc(fixturefunc, msg): +def fail_fixturefunc(fixturefunc, msg: str) -> "NoReturn": fs, lineno = getfslineno(fixturefunc) location = "{}:{}".format(fs, lineno + 1) source = _pytest._code.Source(fixturefunc) fail(msg + ":\n\n" + str(source.indent()) + "\n" + location, pytrace=False) -def call_fixture_func(fixturefunc, request, kwargs): +def call_fixture_func(fixturefunc, request: FixtureRequest, kwargs) -> object: yieldctx = is_generator(fixturefunc) if yieldctx: it = fixturefunc(**kwargs) @@ -792,7 +838,7 @@ def call_fixture_func(fixturefunc, request, kwargs): return res -def _teardown_yield_fixture(fixturefunc, it): +def _teardown_yield_fixture(fixturefunc, it) -> None: """Executes the teardown of a fixture function by advancing the iterator after the yield and ensure the iteration ends (if not it means there is more than one yield in the function)""" try: @@ -805,7 +851,7 @@ def _teardown_yield_fixture(fixturefunc, it): ) -def _eval_scope_callable(scope_callable, fixture_name, config): +def _eval_scope_callable(scope_callable, fixture_name: str, config: Config) -> str: try: result = scope_callable(fixture_name=fixture_name, config=config) except Exception: @@ -829,15 +875,15 @@ class FixtureDef: def __init__( self, - fixturemanager, + fixturemanager: "FixtureManager", baseid, - argname, + argname: str, func, - scope, - params, - unittest=False, + scope: str, + params: Optional[Sequence[object]], + unittest: bool = False, ids=None, - ): + ) -> None: self._fixturemanager = fixturemanager self.baseid = baseid or "" self.has_location = baseid is not None @@ -845,22 +891,28 @@ def __init__( self.argname = argname if callable(scope): scope = _eval_scope_callable(scope, argname, fixturemanager.config) - self.scope = scope self.scopenum = scope2index( scope or "function", descr="Fixture '{}'".format(func.__name__), where=baseid, ) - self.params = params - self.argnames = getfuncargnames(func, name=argname, is_method=unittest) + # The cast is verified by scope2index. + # (Some of the type annotations below are supposed to be inferred, + # but mypy 0.761 has some trouble without them.) + self.scope = cast("_Scope", scope) # type: _Scope + self.params = params # type: Optional[Sequence[object]] + self.argnames = getfuncargnames( + func, name=argname, is_method=unittest + ) # type: Tuple[str, ...] self.unittest = unittest self.ids = ids - self._finalizers = [] + self.cached_result = None # type: Optional[_FixtureCachedResult] + self._finalizers = [] # type: List[Callable[[], object]] - def addfinalizer(self, finalizer): + def addfinalizer(self, finalizer: Callable[[], object]) -> None: self._finalizers.append(finalizer) - def finish(self, request): + def finish(self, request: SubRequest) -> None: exceptions = [] try: while self._finalizers: @@ -871,6 +923,7 @@ def finish(self, request): exceptions.append(sys.exc_info()) if exceptions: _, val, tb = exceptions[0] + assert val is not None # Ensure to not keep frame references through traceback. del exceptions raise val.with_traceback(tb) @@ -881,20 +934,21 @@ def finish(self, request): # the cached fixture value and remove # all finalizers because they may be bound methods which will # keep instances alive - if hasattr(self, "cached_result"): - del self.cached_result + self.cached_result = None self._finalizers = [] - def execute(self, request): + def execute(self, request: SubRequest): # get required arguments and register our own finish() # with their finalization for argname in self.argnames: fixturedef = request._get_active_fixturedef(argname) if argname != "request": + # PseudoFixtureDef is only for "request". + assert isinstance(fixturedef, FixtureDef) fixturedef.addfinalizer(functools.partial(self.finish, request=request)) my_cache_key = self.cache_key(request) - cached_result = getattr(self, "cached_result", None) + cached_result = self.cached_result if cached_result is not None: result, cache_key, err = cached_result # note: comparison with `==` can fail (or be expensive) for e.g. @@ -908,21 +962,21 @@ def execute(self, request): # we have a previous but differently parametrized fixture instance # so we need to tear it down before creating a new one self.finish(request) - assert not hasattr(self, "cached_result") + assert self.cached_result is None hook = self._fixturemanager.session.gethookproxy(request.node.fspath) return hook.pytest_fixture_setup(fixturedef=self, request=request) - def cache_key(self, request): + def cache_key(self, request: SubRequest) -> object: return request.param_index if not hasattr(request, "param") else request.param - def __repr__(self): + def __repr__(self) -> str: return "".format( self.argname, self.scope, self.baseid ) -def resolve_fixture_function(fixturedef, request): +def resolve_fixture_function(fixturedef: FixtureDef, request: FixtureRequest): """Gets the actual callable that can be called to obtain the fixture value, dealing with unittest-specific instances and bound methods. """ @@ -948,11 +1002,12 @@ def resolve_fixture_function(fixturedef, request): return fixturefunc -def pytest_fixture_setup(fixturedef, request): +def pytest_fixture_setup(fixturedef: FixtureDef, request: SubRequest) -> object: """ Execution of fixture setup. """ kwargs = {} for argname in fixturedef.argnames: fixdef = request._get_active_fixturedef(argname) + assert fixdef.cached_result is not None result, arg_cache_key, exc = fixdef.cached_result request._check_scope(argname, request.scope, fixdef.scope) kwargs[argname] = result @@ -962,7 +1017,9 @@ def pytest_fixture_setup(fixturedef, request): try: result = call_fixture_func(fixturefunc, request, kwargs) except TEST_OUTCOME: - fixturedef.cached_result = (None, my_cache_key, sys.exc_info()) + exc_info = sys.exc_info() + assert exc_info[0] is not None + fixturedef.cached_result = (None, my_cache_key, exc_info) raise fixturedef.cached_result = (result, my_cache_key, None) return result @@ -1187,7 +1244,7 @@ def yield_fixture( @fixture(scope="session") -def pytestconfig(request): +def pytestconfig(request: FixtureRequest): """Session-scoped fixture that returns the :class:`_pytest.config.Config` object. Example:: @@ -1200,7 +1257,7 @@ def test_foo(pytestconfig): return request.config -def pytest_addoption(parser): +def pytest_addoption(parser: Parser) -> None: parser.addini( "usefixtures", type="args", @@ -1244,15 +1301,17 @@ class FixtureManager: FixtureLookupError = FixtureLookupError FixtureLookupErrorRepr = FixtureLookupErrorRepr - def __init__(self, session): + def __init__(self, session: "Session") -> None: self.session = session self.config = session.config - self._arg2fixturedefs = {} - self._holderobjseen = set() - self._nodeid_and_autousenames = [("", self.config.getini("usefixtures"))] + self._arg2fixturedefs = {} # type: Dict[str, List[FixtureDef]] + self._holderobjseen = set() # type: Set + self._nodeid_and_autousenames = [ + ("", self.config.getini("usefixtures")) + ] # type: List[Tuple[str, List[str]]] session.config.pluginmanager.register(self, "funcmanage") - def _get_direct_parametrize_args(self, node): + def _get_direct_parametrize_args(self, node) -> List[str]: """This function returns all the direct parametrization arguments of a node, so we don't mistake them for fixtures @@ -1261,7 +1320,7 @@ def _get_direct_parametrize_args(self, node): This things are done later as well when dealing with parametrization so this could be improved """ - parametrize_argnames = [] + parametrize_argnames = [] # type: List[str] for marker in node.iter_markers(name="parametrize"): if not marker.kwargs.get("indirect", False): p_argnames, _ = ParameterSet._parse_parametrize_args( @@ -1271,7 +1330,7 @@ def _get_direct_parametrize_args(self, node): return parametrize_argnames - def getfixtureinfo(self, node, func, cls, funcargs=True): + def getfixtureinfo(self, node, func, cls, funcargs: bool = True) -> FuncFixtureInfo: if funcargs and not getattr(node, "nofuncargs", False): argnames = getfuncargnames(func, name=node.name, cls=cls) else: @@ -1285,10 +1344,10 @@ def getfixtureinfo(self, node, func, cls, funcargs=True): ) return FuncFixtureInfo(argnames, initialnames, names_closure, arg2fixturedefs) - def pytest_plugin_registered(self, plugin): + def pytest_plugin_registered(self, plugin: _PluggyPlugin) -> None: nodeid = None try: - p = py.path.local(plugin.__file__).realpath() + p = py.path.local(plugin.__file__).realpath() # type: ignore[attr-defined] # noqa: F821 except AttributeError: pass else: @@ -1304,9 +1363,9 @@ def pytest_plugin_registered(self, plugin): self.parsefactories(plugin, nodeid) - def _getautousenames(self, nodeid): + def _getautousenames(self, nodeid: str) -> List[str]: """ return a tuple of fixture names to be used. """ - autousenames = [] + autousenames = [] # type: List[str] for baseid, basenames in self._nodeid_and_autousenames: if nodeid.startswith(baseid): if baseid: @@ -1317,7 +1376,9 @@ def _getautousenames(self, nodeid): autousenames.extend(basenames) return autousenames - def getfixtureclosure(self, fixturenames, parentnode, ignore_args=()): + def getfixtureclosure( + self, fixturenames: Tuple[str, ...], parentnode, ignore_args: Sequence[str] = () + ) -> Tuple[Tuple[str, ...], List[str], Dict[str, Sequence[FixtureDef]]]: # collect the closure of all fixtures , starting with the given # fixturenames as the initial set. As we have to visit all # factory definitions anyway, we also return an arg2fixturedefs @@ -1328,7 +1389,7 @@ def getfixtureclosure(self, fixturenames, parentnode, ignore_args=()): parentid = parentnode.nodeid fixturenames_closure = self._getautousenames(parentid) - def merge(otherlist): + def merge(otherlist: Iterable[str]) -> None: for arg in otherlist: if arg not in fixturenames_closure: fixturenames_closure.append(arg) @@ -1340,7 +1401,7 @@ def merge(otherlist): # need to return it as well, so save this. initialnames = tuple(fixturenames_closure) - arg2fixturedefs = {} + arg2fixturedefs = {} # type: Dict[str, Sequence[FixtureDef]] lastlen = -1 while lastlen != len(fixturenames_closure): lastlen = len(fixturenames_closure) @@ -1354,7 +1415,7 @@ def merge(otherlist): arg2fixturedefs[argname] = fixturedefs merge(fixturedefs[-1].argnames) - def sort_by_scope(arg_name): + def sort_by_scope(arg_name: str) -> int: try: fixturedefs = arg2fixturedefs[arg_name] except KeyError: @@ -1365,7 +1426,7 @@ def sort_by_scope(arg_name): fixturenames_closure.sort(key=sort_by_scope) return initialnames, fixturenames_closure, arg2fixturedefs - def pytest_generate_tests(self, metafunc): + def pytest_generate_tests(self, metafunc: "Metafunc") -> None: for argname in metafunc.fixturenames: faclist = metafunc._arg2fixturedefs.get(argname) if faclist: @@ -1399,7 +1460,9 @@ def pytest_collection_modifyitems(self, items): # separate parametrized setups items[:] = reorder_items(items) - def parsefactories(self, node_or_obj, nodeid=NOTSET, unittest=False): + def parsefactories( + self, node_or_obj, nodeid=NOTSET, unittest: bool = False + ) -> None: if nodeid is not NOTSET: holderobj = node_or_obj else: @@ -1455,7 +1518,9 @@ def parsefactories(self, node_or_obj, nodeid=NOTSET, unittest=False): if autousenames: self._nodeid_and_autousenames.append((nodeid or "", autousenames)) - def getfixturedefs(self, argname, nodeid): + def getfixturedefs( + self, argname: str, nodeid: str + ) -> Optional[Sequence[FixtureDef]]: """ Gets a list of fixtures which are applicable to the given node id. @@ -1469,7 +1534,9 @@ def getfixturedefs(self, argname, nodeid): return None return tuple(self._matchfactories(fixturedefs, nodeid)) - def _matchfactories(self, fixturedefs, nodeid): + def _matchfactories( + self, fixturedefs: Iterable[FixtureDef], nodeid: str + ) -> Iterator[FixtureDef]: from _pytest import nodes for fixturedef in fixturedefs: diff --git a/src/_pytest/helpconfig.py b/src/_pytest/helpconfig.py index 87cd2c0a763..1d04ae408d0 100644 --- a/src/_pytest/helpconfig.py +++ b/src/_pytest/helpconfig.py @@ -2,11 +2,16 @@ import os import sys from argparse import Action +from typing import Optional +from typing import Union import py import pytest +from _pytest.config import Config +from _pytest.config import ExitCode from _pytest.config import PrintHelp +from _pytest.config.argparsing import Parser class HelpAction(Action): @@ -36,7 +41,7 @@ def __call__(self, parser, namespace, values, option_string=None): raise PrintHelp -def pytest_addoption(parser): +def pytest_addoption(parser: Parser) -> None: group = parser.getgroup("debugconfig") group.addoption( "--version", @@ -105,7 +110,7 @@ def pytest_cmdline_parse(): undo_tracing = config.pluginmanager.enable_tracing() sys.stderr.write("writing pytestdebug information to %s\n" % path) - def unset_tracing(): + def unset_tracing() -> None: debugfile.close() sys.stderr.write("wrote pytestdebug information to %s\n" % debugfile.name) config.trace.root.setwriter(None) @@ -126,7 +131,7 @@ def showversion(config): sys.stderr.write(line + "\n") -def pytest_cmdline_main(config): +def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]: if config.option.version: showversion(config) return 0 @@ -135,9 +140,10 @@ def pytest_cmdline_main(config): showhelp(config) config._ensure_unconfigure() return 0 + return None -def showhelp(config): +def showhelp(config: Config) -> None: import textwrap reporter = config.pluginmanager.get_plugin("terminalreporter") diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index 9bf59b52ef9..3b532895b5a 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -10,13 +10,20 @@ from _pytest.compat import TYPE_CHECKING if TYPE_CHECKING: + from _pytest.config import Config + from _pytest.config import PytestPluginManager + from _pytest.config import _PluggyPlugin + from _pytest.config.argparsing import Parser + from _pytest.fixtures import FixtureDef + from _pytest.fixtures import SubRequest + from _pytest.config import ExitCode from _pytest.main import Session from _pytest.nodes import Collector from _pytest.nodes import Item + from _pytest.python import Metafunc from _pytest.python import Module from _pytest.python import PyCollector - hookspec = HookspecMarker("pytest") # ------------------------------------------------------------------------- @@ -25,7 +32,7 @@ @hookspec(historic=True) -def pytest_addhooks(pluginmanager): +def pytest_addhooks(pluginmanager: "PytestPluginManager") -> None: """called at plugin registration time to allow adding new hooks via a call to ``pluginmanager.add_hookspecs(module_or_class, prefix)``. @@ -38,7 +45,9 @@ def pytest_addhooks(pluginmanager): @hookspec(historic=True) -def pytest_plugin_registered(plugin, manager): +def pytest_plugin_registered( + plugin: "_PluggyPlugin", manager: "PytestPluginManager" +) -> None: """ a new pytest plugin got registered. :param plugin: the plugin module or instance @@ -50,7 +59,7 @@ def pytest_plugin_registered(plugin, manager): @hookspec(historic=True) -def pytest_addoption(parser, pluginmanager): +def pytest_addoption(parser: "Parser", pluginmanager: "PytestPluginManager") -> None: """register argparse-style options and ini-style config values, called once at the beginning of a test run. @@ -88,7 +97,7 @@ def pytest_addoption(parser, pluginmanager): @hookspec(historic=True) -def pytest_configure(config): +def pytest_configure(config: "Config") -> None: """ Allows plugins and conftest files to perform initial configuration. @@ -112,7 +121,9 @@ def pytest_configure(config): @hookspec(firstresult=True) -def pytest_cmdline_parse(pluginmanager, args): +def pytest_cmdline_parse( + pluginmanager: "PytestPluginManager", args: List[str] +) -> Optional[object]: """return initialized config object, parsing the specified args. Stops at first non-None result, see :ref:`firstresult` @@ -126,7 +137,7 @@ def pytest_cmdline_parse(pluginmanager, args): """ -def pytest_cmdline_preparse(config, args): +def pytest_cmdline_preparse(config: "Config", args: List[str]) -> None: """(**Deprecated**) modify command line arguments before option parsing. This hook is considered deprecated and will be removed in a future pytest version. Consider @@ -141,7 +152,7 @@ def pytest_cmdline_preparse(config, args): @hookspec(firstresult=True) -def pytest_cmdline_main(config): +def pytest_cmdline_main(config: "Config") -> "Optional[Union[ExitCode, int]]": """ called for performing the main command line action. The default implementation will invoke the configure hooks and runtest_mainloop. @@ -154,7 +165,9 @@ def pytest_cmdline_main(config): """ -def pytest_load_initial_conftests(early_config, parser, args): +def pytest_load_initial_conftests( + early_config: "Config", parser: "Parser", args: List[str] +) -> None: """ implements the loading of initial conftest files ahead of command line option parsing. @@ -182,7 +195,7 @@ def pytest_collection(session: "Session") -> Optional[Any]: """ -def pytest_collection_modifyitems(session, config, items): +def pytest_collection_modifyitems(session: "Session", config: "Config", items): """ called after collection has been performed, may filter or re-order the items in-place. @@ -192,7 +205,7 @@ def pytest_collection_modifyitems(session, config, items): """ -def pytest_collection_finish(session): +def pytest_collection_finish(session: "Session"): """ called after collection has been performed and modified. :param _pytest.main.Session session: the pytest session object @@ -200,7 +213,7 @@ def pytest_collection_finish(session): @hookspec(firstresult=True) -def pytest_ignore_collect(path, config): +def pytest_ignore_collect(path, config: "Config"): """ return True to prevent considering this path for collection. This hook is consulted for all files and directories prior to calling more specific hooks. @@ -290,12 +303,12 @@ def pytest_pyfunc_call(pyfuncitem): Stops at first non-None result, see :ref:`firstresult` """ -def pytest_generate_tests(metafunc): +def pytest_generate_tests(metafunc: "Metafunc") -> None: """ generate (multiple) parametrized calls to a test function.""" @hookspec(firstresult=True) -def pytest_make_parametrize_id(config, val, argname): +def pytest_make_parametrize_id(config: "Config", val, argname) -> Optional[str]: """Return a user-friendly string representation of the given ``val`` that will be used by @pytest.mark.parametrize calls. Return None if the hook doesn't know about ``val``. The parameter name is available as ``argname``, if required. @@ -314,7 +327,7 @@ def pytest_make_parametrize_id(config, val, argname): @hookspec(firstresult=True) -def pytest_runtestloop(session): +def pytest_runtestloop(session: "Session"): """ called for performing the main runtest loop (after collection finished). @@ -397,7 +410,7 @@ def pytest_runtest_logreport(report): @hookspec(firstresult=True) -def pytest_report_to_serializable(config, report): +def pytest_report_to_serializable(config: "Config", report): """ Serializes the given report object into a data structure suitable for sending over the wire, e.g. converted to JSON. @@ -405,7 +418,7 @@ def pytest_report_to_serializable(config, report): @hookspec(firstresult=True) -def pytest_report_from_serializable(config, data): +def pytest_report_from_serializable(config: "Config", data): """ Restores a report object previously serialized with pytest_report_to_serializable(). """ @@ -417,7 +430,9 @@ def pytest_report_from_serializable(config, data): @hookspec(firstresult=True) -def pytest_fixture_setup(fixturedef, request): +def pytest_fixture_setup( + fixturedef: "FixtureDef", request: "SubRequest" +) -> Optional[object]: """ performs fixture setup execution. :return: The return value of the call to the fixture function @@ -431,10 +446,12 @@ def pytest_fixture_setup(fixturedef, request): """ -def pytest_fixture_post_finalizer(fixturedef, request): +def pytest_fixture_post_finalizer( + fixturedef: "FixtureDef", request: "SubRequest" +) -> None: """ called after fixture teardown, but before the cache is cleared so - the fixture result cache ``fixturedef.cached_result`` can - still be accessed.""" + the fixture result cache ``fixturedef.cached_result`` is still + available.""" # ------------------------------------------------------------------------- @@ -442,7 +459,7 @@ def pytest_fixture_post_finalizer(fixturedef, request): # ------------------------------------------------------------------------- -def pytest_sessionstart(session): +def pytest_sessionstart(session: "Session") -> None: """ called after the ``Session`` object has been created and before performing collection and entering the run test loop. @@ -450,7 +467,9 @@ def pytest_sessionstart(session): """ -def pytest_sessionfinish(session, exitstatus): +def pytest_sessionfinish( + session: "Session", exitstatus: "Union[int, ExitCode]" +) -> None: """ called after whole test run finished, right before returning the exit status to the system. :param _pytest.main.Session session: the pytest session object @@ -458,7 +477,7 @@ def pytest_sessionfinish(session, exitstatus): """ -def pytest_unconfigure(config): +def pytest_unconfigure(config: "Config") -> None: """ called before test process is exited. :param _pytest.config.Config config: pytest config object @@ -470,7 +489,7 @@ def pytest_unconfigure(config): # ------------------------------------------------------------------------- -def pytest_assertrepr_compare(config, op, left, right): +def pytest_assertrepr_compare(config: "Config", op, left, right): """return explanation for comparisons in failing assert expressions. Return None for no custom explanation, otherwise return a list @@ -525,7 +544,7 @@ def pytest_assertion_pass(item, lineno, orig, expl): # ------------------------------------------------------------------------- -def pytest_report_header(config, startdir): +def pytest_report_header(config: "Config", startdir): """ return a string or list of strings to be displayed as header info for terminal reporting. :param _pytest.config.Config config: pytest config object @@ -539,7 +558,7 @@ def pytest_report_header(config, startdir): """ -def pytest_report_collectionfinish(config, startdir, items): +def pytest_report_collectionfinish(config: "Config", startdir, items): """ .. versionadded:: 3.2 @@ -554,7 +573,7 @@ def pytest_report_collectionfinish(config, startdir, items): @hookspec(firstresult=True) -def pytest_report_teststatus(report, config): +def pytest_report_teststatus(report, config: "Config"): """ return result-category, shortletter and verbose word for reporting. :param _pytest.config.Config config: pytest config object @@ -562,7 +581,7 @@ def pytest_report_teststatus(report, config): Stops at first non-None result, see :ref:`firstresult` """ -def pytest_terminal_summary(terminalreporter, exitstatus, config): +def pytest_terminal_summary(terminalreporter, exitstatus, config: "Config"): """Add a section to terminal summary reporting. :param _pytest.terminal.TerminalReporter terminalreporter: the internal terminal reporter object @@ -636,7 +655,7 @@ def pytest_exception_interact(node, call, report): """ -def pytest_enter_pdb(config, pdb): +def pytest_enter_pdb(config: "Config", pdb): """ called upon pdb.set_trace(), can be used by plugins to take special action just before the python debugger enters in interactive mode. @@ -645,7 +664,7 @@ def pytest_enter_pdb(config, pdb): """ -def pytest_leave_pdb(config, pdb): +def pytest_leave_pdb(config: "Config", pdb): """ called when leaving pdb (e.g. with continue after pdb.set_trace()). Can be used by plugins to take special action just after the python diff --git a/src/_pytest/junitxml.py b/src/_pytest/junitxml.py index 6ef5358397f..8538fa4042c 100644 --- a/src/_pytest/junitxml.py +++ b/src/_pytest/junitxml.py @@ -21,7 +21,9 @@ import pytest from _pytest import deprecated from _pytest import nodes +from _pytest.config import Config from _pytest.config import filename_arg +from _pytest.config.argparsing import Parser from _pytest.warnings import _issue_warning_captured @@ -359,7 +361,7 @@ def record_func(name, value): return record_func -def pytest_addoption(parser): +def pytest_addoption(parser: Parser) -> None: group = parser.getgroup("terminal reporting") group.addoption( "--junitxml", @@ -404,7 +406,7 @@ def pytest_addoption(parser): ) -def pytest_configure(config): +def pytest_configure(config: Config) -> None: xmlpath = config.option.xmlpath # prevent opening xmllog on slave nodes (xdist) if xmlpath and not hasattr(config, "slaveinput"): @@ -412,7 +414,8 @@ def pytest_configure(config): if not junit_family: _issue_warning_captured(deprecated.JUNIT_XML_DEFAULT_FAMILY, config.hook, 2) junit_family = "xunit1" - config._xml = LogXML( + # Type ignored: pending mechanism to store typed objects scoped to config. + _xml = config._xml = LogXML( # type: ignore # noqa: F821 xmlpath, config.option.junitprefix, config.getini("junit_suite_name"), @@ -421,13 +424,13 @@ def pytest_configure(config): junit_family, config.getini("junit_log_passing_tests"), ) - config.pluginmanager.register(config._xml) + config.pluginmanager.register(_xml) -def pytest_unconfigure(config): +def pytest_unconfigure(config: Config) -> None: xml = getattr(config, "_xml", None) if xml: - del config._xml + del config._xml # type: ignore[attr-defined] # noqa: F821 config.pluginmanager.unregister(xml) @@ -622,10 +625,10 @@ def pytest_internalerror(self, excrepr): reporter.attrs.update(classname="pytest", name="internal") reporter._add_simple(Junit.error, "internal error", excrepr) - def pytest_sessionstart(self): + def pytest_sessionstart(self) -> None: self.suite_start_time = time.time() - def pytest_sessionfinish(self): + def pytest_sessionfinish(self) -> None: dirname = os.path.dirname(os.path.abspath(self.logfile)) if not os.path.isdir(dirname): os.makedirs(dirname) diff --git a/src/_pytest/logging.py b/src/_pytest/logging.py index 1df7c1691f6..d3d8bda82e9 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -13,7 +13,9 @@ from _pytest import nodes from _pytest.compat import nullcontext from _pytest.config import _strtobool +from _pytest.config import Config from _pytest.config import create_terminal_writer +from _pytest.config.argparsing import Parser from _pytest.pathlib import Path DEFAULT_LOG_FORMAT = "%(levelname)-8s %(name)s:%(filename)s:%(lineno)d %(message)s" @@ -171,7 +173,7 @@ def get_option_ini(config, *names): return ret -def pytest_addoption(parser): +def pytest_addoption(parser: Parser) -> None: """Add options to control log capturing.""" group = parser.getgroup("logging") @@ -470,7 +472,7 @@ def get_actual_log_level(config, *setting_names): # run after terminalreporter/capturemanager are configured @pytest.hookimpl(trylast=True) -def pytest_configure(config): +def pytest_configure(config: Config) -> None: config.pluginmanager.register(LoggingPlugin(config), "logging-plugin") @@ -478,7 +480,7 @@ class LoggingPlugin: """Attaches to the logging module and captures log messages for each test. """ - def __init__(self, config): + def __init__(self, config: Config) -> None: """Creates a new plugin to capture log messages. The formatter can be safely shared across all handlers so @@ -510,13 +512,13 @@ def __init__(self, config): ) log_file = get_option_ini(config, "log_file") - if log_file: + if not log_file: + self.log_file_handler = None + else: self.log_file_handler = logging.FileHandler( log_file, mode="w", encoding="UTF-8" ) self.log_file_handler.setFormatter(self.log_file_formatter) - else: - self.log_file_handler = None self.log_cli_handler = None @@ -683,7 +685,7 @@ def pytest_runtest_logreport(self): yield @pytest.hookimpl(hookwrapper=True, tryfirst=True) - def pytest_sessionfinish(self): + def pytest_sessionfinish(self) -> Generator[None, None, None]: with self.live_logs_context(): if self.log_cli_handler: self.log_cli_handler.set_when("sessionfinish") @@ -701,7 +703,7 @@ def pytest_sessionfinish(self): yield @pytest.hookimpl(hookwrapper=True, tryfirst=True) - def pytest_sessionstart(self): + def pytest_sessionstart(self) -> Generator[None, None, None]: with self.live_logs_context(): if self.log_cli_handler: self.log_cli_handler.set_when("sessionstart") diff --git a/src/_pytest/mark/__init__.py b/src/_pytest/mark/__init__.py index f493bd839f1..aafd80c3faf 100644 --- a/src/_pytest/mark/__init__.py +++ b/src/_pytest/mark/__init__.py @@ -1,4 +1,7 @@ """ generic mechanism for marking and selecting python functions. """ +from typing import Optional +from typing import Union + from .legacy import matchkeyword from .legacy import matchmark from .structures import EMPTY_PARAMETERSET_OPTION @@ -8,13 +11,16 @@ from .structures import MarkDecorator from .structures import MarkGenerator from .structures import ParameterSet +from _pytest.config import Config +from _pytest.config import ExitCode from _pytest.config import hookimpl from _pytest.config import UsageError +from _pytest.config.argparsing import Parser __all__ = ["Mark", "MarkDecorator", "MarkGenerator", "get_empty_parameterset_mark"] -def param(*values, **kw): +def param(*values, **kw) -> ParameterSet: """Specify a parameter in `pytest.mark.parametrize`_ calls or :ref:`parametrized fixtures `. @@ -34,7 +40,7 @@ def test_eval(test_input, expected): return ParameterSet.param(*values, **kw) -def pytest_addoption(parser): +def pytest_addoption(parser: Parser) -> None: group = parser.getgroup("general") group._addoption( "-k", @@ -77,7 +83,7 @@ def pytest_addoption(parser): @hookimpl(tryfirst=True) -def pytest_cmdline_main(config): +def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]: import _pytest.config if config.option.markers: @@ -93,8 +99,10 @@ def pytest_cmdline_main(config): config._ensure_unconfigure() return 0 + return None + -def deselect_by_keyword(items, config): +def deselect_by_keyword(items, config: Config) -> None: keywordexpr = config.option.keyword.lstrip() if not keywordexpr: return @@ -121,7 +129,7 @@ def deselect_by_keyword(items, config): items[:] = remaining -def deselect_by_mark(items, config): +def deselect_by_mark(items, config: Config) -> None: matchexpr = config.option.markexpr if not matchexpr: return @@ -144,8 +152,9 @@ def pytest_collection_modifyitems(items, config): deselect_by_mark(items, config) -def pytest_configure(config): - config._old_mark_config = MARK_GEN._config +def pytest_configure(config: Config) -> None: + # Type ignored: pending mechanism to store typed objects scoped to config. + config._old_mark_config = MARK_GEN._config # type: ignore[attr-defined] # noqa: F821 MARK_GEN._config = config empty_parameterset = config.getini(EMPTY_PARAMETERSET_OPTION) @@ -157,5 +166,5 @@ def pytest_configure(config): ) -def pytest_unconfigure(config): +def pytest_unconfigure(config: Config) -> None: MARK_GEN._config = getattr(config, "_old_mark_config", None) diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index 1ab22b7c758..1583fd9f8bc 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -1,11 +1,13 @@ import inspect import warnings -from collections import namedtuple from collections.abc import MutableMapping from typing import Iterable from typing import List +from typing import NamedTuple from typing import Optional +from typing import Sequence from typing import Set +from typing import Tuple from typing import Union import attr @@ -13,20 +15,21 @@ from .._code.source import getfslineno from ..compat import ascii_escaped from ..compat import NOTSET +from _pytest.config import Config from _pytest.outcomes import fail from _pytest.warning_types import PytestUnknownMarkWarning EMPTY_PARAMETERSET_OPTION = "empty_parameter_set_mark" -def istestfunc(func): +def istestfunc(func) -> bool: return ( hasattr(func, "__call__") and getattr(func, "__name__", "") != "" ) -def get_empty_parameterset_mark(config, argnames, func): +def get_empty_parameterset_mark(config: Config, argnames, func) -> "MarkDecorator": from ..nodes import Collector requested_mark = config.getini(EMPTY_PARAMETERSET_OPTION) @@ -49,12 +52,26 @@ def get_empty_parameterset_mark(config, argnames, func): fs, lineno, ) - return mark(reason=reason) - - -class ParameterSet(namedtuple("ParameterSet", "values, marks, id")): + # Type ignored because MarkDecorator.__call__() is a bit tough to + # annotate ATM. + return mark(reason=reason) # type: ignore[no-any-return] # noqa: F723 + + +class ParameterSet( + NamedTuple( + "ParameterSet", + [ + ("values", Sequence[object]), + # TODO: Work on this type. + ("marks", Union[Tuple, List, Set]), + ("id", Optional[str]), + ], + ) +): @classmethod - def param(cls, *values, marks=(), id=None): + def param( + cls, *values: object, marks=(), id: Optional[str] = None + ) -> "ParameterSet": if isinstance(marks, MarkDecorator): marks = (marks,) else: @@ -69,7 +86,11 @@ def param(cls, *values, marks=(), id=None): return cls(values, marks, id) @classmethod - def extract_from(cls, parameterset, force_tuple=False): + def extract_from( + cls, + parameterset: Union["ParameterSet", Sequence[object], object], + force_tuple: bool = False, + ) -> "ParameterSet": """ :param parameterset: a legacy style parameterset that may or may not be a tuple, @@ -85,10 +106,20 @@ def extract_from(cls, parameterset, force_tuple=False): if force_tuple: return cls.param(parameterset) else: - return cls(parameterset, marks=[], id=None) + # TODO: Refactor to fix this type-ignore. Currently the following + # type-checks but crashes: + # + # @pytest.mark.parametrize(('x', 'y'), [1, 2]) + # def test_foo(x, y): pass + return cls(parameterset, marks=[], id=None) # type: ignore[arg-type] # noqa: F821 @staticmethod - def _parse_parametrize_args(argnames, argvalues, *args, **kwargs): + def _parse_parametrize_args( + argnames: Union[str, List[str], Tuple[str, ...]], + argvalues: Iterable[Union["ParameterSet", Sequence[object], object]], + *args, + **kwargs + ) -> Tuple[Union[List[str], Tuple[str, ...]], bool]: if not isinstance(argnames, (tuple, list)): argnames = [x.strip() for x in argnames.split(",") if x.strip()] force_tuple = len(argnames) == 1 @@ -97,13 +128,23 @@ def _parse_parametrize_args(argnames, argvalues, *args, **kwargs): return argnames, force_tuple @staticmethod - def _parse_parametrize_parameters(argvalues, force_tuple): + def _parse_parametrize_parameters( + argvalues: Iterable[Union["ParameterSet", Sequence[object], object]], + force_tuple: bool, + ) -> List["ParameterSet"]: return [ ParameterSet.extract_from(x, force_tuple=force_tuple) for x in argvalues ] @classmethod - def _for_parametrize(cls, argnames, argvalues, func, config, function_definition): + def _for_parametrize( + cls, + argnames: Union[str, List[str], Tuple[str, ...]], + argvalues: Iterable[Union["ParameterSet", Sequence[object], object]], + func, + config: Config, + function_definition, + ) -> Tuple[Union[List[str], Tuple[str, ...]], List["ParameterSet"]]: argnames, force_tuple = cls._parse_parametrize_args(argnames, argvalues) parameters = cls._parse_parametrize_parameters(argvalues, force_tuple) del argvalues @@ -152,7 +193,7 @@ class Mark: #: resolved/generated ids with parametrize Marks _param_ids_generated = attr.ib(type=Optional[List[str]], default=None, repr=False) - def _has_param_ids(self): + def _has_param_ids(self) -> bool: return "ids" in self.kwargs or len(self.args) >= 4 def combined_with(self, other: "Mark") -> "Mark": @@ -215,10 +256,10 @@ def test_function(): """ - mark = attr.ib(validator=attr.validators.instance_of(Mark)) + mark = attr.ib(type=Mark, validator=attr.validators.instance_of(Mark)) @property - def name(self): + def name(self) -> str: """alias for mark.name""" return self.mark.name @@ -233,13 +274,13 @@ def kwargs(self): return self.mark.kwargs @property - def markname(self): + def markname(self) -> str: return self.name # for backward-compat (2.4.1 had this attr) - def __repr__(self): + def __repr__(self) -> str: return "".format(self.mark) - def with_args(self, *args, **kwargs): + def with_args(self, *args, **kwargs) -> "MarkDecorator": """ return a MarkDecorator with extra arguments added unlike call this can be used even if the sole argument is a callable/class @@ -262,7 +303,7 @@ def __call__(self, *args, **kwargs): return self.with_args(*args, **kwargs) -def get_unpacked_marks(obj): +def get_unpacked_marks(obj) -> List[Mark]: """ obtain the unpacked marks that are stored on an object """ @@ -288,7 +329,7 @@ def normalize_mark_list(mark_list: Iterable[Union[Mark, MarkDecorator]]) -> List return [x for x in extracted if isinstance(x, Mark)] -def store_mark(obj, mark): +def store_mark(obj, mark: Mark) -> None: """store a Mark on an object this is used to implement the Mark declarations/decorators correctly """ @@ -310,7 +351,7 @@ def test_function(): will set a 'slowtest' :class:`MarkInfo` object on the ``test_function`` object. """ - _config = None + _config = None # type: Optional[Config] _markers = set() # type: Set[str] def __getattr__(self, name: str) -> MarkDecorator: @@ -387,8 +428,8 @@ def _seen(self): seen.update(self.parent.keywords) return seen - def __len__(self): + def __len__(self) -> int: return len(self._seen()) - def __repr__(self): + def __repr__(self) -> str: return "".format(self.node) diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index 99453941124..f9a873204df 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -2,6 +2,7 @@ import warnings from functools import lru_cache from typing import Any +from typing import Callable from typing import Dict from typing import Iterable from typing import List @@ -107,7 +108,7 @@ def __init__( #: the pytest config object if config: - self.config = config + self.config = config # type: Config else: if not parent: raise TypeError("config or parent must be provided") @@ -293,7 +294,7 @@ def listextrakeywords(self): def listnames(self): return [x.name for x in self.listchain()] - def addfinalizer(self, fin): + def addfinalizer(self, fin: Callable[[], object]) -> None: """ register a function to be called when this node is finalized. This method can only be called when this node is active diff --git a/src/_pytest/pastebin.py b/src/_pytest/pastebin.py index 77b4e2621eb..60f335d44e3 100644 --- a/src/_pytest/pastebin.py +++ b/src/_pytest/pastebin.py @@ -2,9 +2,11 @@ import tempfile import pytest +from _pytest.config import Config +from _pytest.config.argparsing import Parser -def pytest_addoption(parser): +def pytest_addoption(parser: Parser) -> None: group = parser.getgroup("terminal reporting") group._addoption( "--pastebin", @@ -18,7 +20,7 @@ def pytest_addoption(parser): @pytest.hookimpl(trylast=True) -def pytest_configure(config): +def pytest_configure(config: Config) -> None: if config.option.pastebin == "all": tr = config.pluginmanager.getplugin("terminalreporter") # if no terminal reporter plugin is present, nothing we can do here; @@ -26,7 +28,8 @@ def pytest_configure(config): # when using pytest-xdist, for example if tr is not None: # pastebin file will be utf-8 encoded binary file - config._pastebinfile = tempfile.TemporaryFile("w+b") + # Type ignored: pending mechanism to store typed objects scoped to config. + config._pastebinfile = tempfile.TemporaryFile("w+b") # type: ignore # noqa: F821 oldwrite = tr._tw.write def tee_write(s, **kwargs): @@ -38,13 +41,14 @@ def tee_write(s, **kwargs): tr._tw.write = tee_write -def pytest_unconfigure(config): +def pytest_unconfigure(config: Config) -> None: if hasattr(config, "_pastebinfile"): # get terminal contents and delete file - config._pastebinfile.seek(0) - sessionlog = config._pastebinfile.read() - config._pastebinfile.close() - del config._pastebinfile + pastebinfile = config._pastebinfile # type: ignore[attr-defined] # noqa: F821 + del config._pastebinfile # type: ignore[attr-defined] # noqa: F821 + pastebinfile.seek(0) + sessionlog = pastebinfile.read() + pastebinfile.close() # undo our patching in the terminal reporter tr = config.pluginmanager.getplugin("terminalreporter") del tr._tw.__dict__["write"] diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index fee2dc2b49d..810a2192fca 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -30,7 +30,9 @@ from _pytest.capture import SysCapture from _pytest.compat import TYPE_CHECKING from _pytest.config import _PluggyPlugin +from _pytest.config import Config from _pytest.config import ExitCode +from _pytest.config.argparsing import Parser from _pytest.fixtures import FixtureRequest from _pytest.main import Session from _pytest.monkeypatch import MonkeyPatch @@ -52,7 +54,7 @@ ] -def pytest_addoption(parser): +def pytest_addoption(parser: Parser) -> None: parser.addoption( "--lsof", action="store_true", @@ -77,7 +79,7 @@ def pytest_addoption(parser): ) -def pytest_configure(config): +def pytest_configure(config: Config) -> None: if config.getvalue("lsof"): checker = LsofFdLeakChecker() if checker.matching_platform(): @@ -888,7 +890,7 @@ def inline_run(self, *args, plugins=(), no_reraise_ctrlc: bool = False): rec = [] class Collect: - def pytest_configure(x, config): + def pytest_configure(x, config: Config) -> None: rec.append(self.make_hook_recorder(config.pluginmanager)) plugins.append(Collect()) diff --git a/src/_pytest/python.py b/src/_pytest/python.py index a9264667119..f2399d65a27 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -13,6 +13,7 @@ from typing import Dict from typing import Iterable from typing import List +from typing import Mapping from typing import Optional from typing import Set from typing import Tuple @@ -26,6 +27,7 @@ from _pytest._code import filter_traceback from _pytest._code.code import ExceptionInfo from _pytest._code.source import getfslineno +from _pytest._io import TerminalWriter from _pytest.compat import ascii_escaped from _pytest.compat import get_default_arg_names from _pytest.compat import get_real_func @@ -38,8 +40,14 @@ from _pytest.compat import safe_getattr from _pytest.compat import safe_isclass from _pytest.compat import STRING_TYPES +from _pytest.compat import TYPE_CHECKING +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 FUNCARGNAMES +from _pytest.fixtures import FuncFixtureInfo +from _pytest.main import Session from _pytest.mark import MARK_GEN from _pytest.mark import ParameterSet from _pytest.mark.structures import get_unpacked_marks @@ -51,6 +59,11 @@ 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 + def pyobj_property(name): def get(self): @@ -64,7 +77,7 @@ def get(self): return property(get, None, None, doc) -def pytest_addoption(parser): +def pytest_addoption(parser: Parser) -> None: group = parser.getgroup("general") group.addoption( "--fixtures", @@ -119,21 +132,23 @@ def pytest_addoption(parser): ) -def pytest_cmdline_main(config): +def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]: if config.option.showfixtures: showfixtures(config) return 0 if config.option.show_fixtures_per_test: show_fixtures_per_test(config) return 0 + return None -def pytest_generate_tests(metafunc): +def pytest_generate_tests(metafunc: "Metafunc") -> None: for marker in metafunc.definition.iter_markers(name="parametrize"): - metafunc.parametrize(*marker.args, **marker.kwargs, _param_mark=marker) + # TODO: Fix this type-ignore (overlapping kwargs). + metafunc.parametrize(*marker.args, **marker.kwargs, _param_mark=marker) # type: ignore[misc] # noqa: F821 -def pytest_configure(config): +def pytest_configure(config: Config) -> None: config.addinivalue_line( "markers", "parametrize(argnames, argvalues): call a test function multiple " @@ -807,18 +822,19 @@ def hasnew(obj): class CallSpec2: - def __init__(self, metafunc): + def __init__(self, metafunc: "Metafunc") -> None: self.metafunc = metafunc - self.funcargs = {} - self._idlist = [] - self.params = {} + self.funcargs = {} # type: Dict[str, object] + self._idlist = [] # type: List[str] + self.params = {} # type: Dict[str, object] self._globalid = NOTSET self._globalparam = NOTSET - self._arg2scopenum = {} # used for sorting parametrized resources - self.marks = [] - self.indices = {} + # Used for sorting parametrized resources. + self._arg2scopenum = {} # type: Dict[str, int] + self.marks = [] # type: List[Mark] + self.indices = {} # type: Dict[str, int] - def copy(self): + def copy(self) -> "CallSpec2": cs = CallSpec2(self.metafunc) cs.funcargs.update(self.funcargs) cs.params.update(self.params) @@ -830,11 +846,11 @@ def copy(self): cs._globalparam = self._globalparam return cs - def _checkargnotcontained(self, arg): + def _checkargnotcontained(self, arg: str) -> None: if arg in self.params or arg in self.funcargs: raise ValueError("duplicate {!r}".format(arg)) - def getparam(self, name): + def getparam(self, name: str): try: return self.params[name] except KeyError: @@ -843,14 +859,28 @@ def getparam(self, name): return self._globalparam @property - def id(self): + def id(self) -> str: return "-".join(map(str, self._idlist)) - def setmulti2(self, valtypes, argnames, valset, id, marks, scopenum, param_index): + def setmulti2( + self, + valtypes: "Mapping[str, Literal['params', 'funcargs']]", + argnames: typing.Sequence[str], + valset: Iterable[object], + id: str, + marks, + scopenum: int, + param_index: int, + ) -> None: for arg, val in zip(argnames, valset): self._checkargnotcontained(arg) valtype_for_arg = valtypes[arg] - getattr(self, valtype_for_arg)[arg] = val + if valtype_for_arg == "params": + self.params[arg] = val + elif valtype_for_arg == "funcargs": + self.funcargs[arg] = val + else: + assert False, "Unhandled valtype for arg: {}".format(valtype_for_arg) self.indices[arg] = param_index self._arg2scopenum[arg] = scopenum self._idlist.append(id) @@ -868,8 +898,8 @@ class Metafunc: def __init__( self, definition: "FunctionDefinition", - fixtureinfo, - config, + fixtureinfo: fixtures.FuncFixtureInfo, + config: Config, cls=None, module=None, ) -> None: @@ -894,21 +924,21 @@ def __init__( self._arg2fixturedefs = fixtureinfo.name2fixturedefs @property - def funcargnames(self): + def funcargnames(self) -> List[str]: """ alias attribute for ``fixturenames`` for pre-2.3 compatibility""" warnings.warn(FUNCARGNAMES, stacklevel=2) return self.fixturenames def parametrize( self, - argnames, - argvalues, + argnames: Union[str, List[str], Tuple[str, ...]], + argvalues: Iterable[Union[ParameterSet, typing.Sequence[object], object]], indirect=False, ids=None, - scope=None, + scope: "Optional[_Scope]" = 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 @@ -1017,8 +1047,12 @@ def parametrize( self._calls = newcalls def _resolve_arg_ids( - self, argnames: List[str], ids, parameters: List[ParameterSet], item: nodes.Item - ): + self, + argnames: typing.Sequence[str], + ids, + parameters: typing.Sequence[ParameterSet], + item, + ) -> List[str]: """Resolves the actual ids for the given argnames, based on the ``ids`` parameter given to ``parametrize``. @@ -1036,8 +1070,7 @@ def _resolve_arg_ids( if ids: func_name = self.function.__name__ ids = self._validate_ids(ids, parameters, func_name) - ids = idmaker(argnames, parameters, idfn, ids, self.config, item=item) - return ids + return idmaker(argnames, parameters, idfn, ids, self.config, item=item) def _validate_ids(self, ids, parameters, func_name): try: @@ -1071,7 +1104,9 @@ def _validate_ids(self, ids, parameters, func_name): ) return new_ids - def _resolve_arg_value_types(self, argnames: List[str], indirect) -> Dict[str, str]: + def _resolve_arg_value_types( + self, argnames: typing.Sequence[str], indirect + ) -> "Dict[str, Literal['params', 'funcargs']]": """Resolves 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. @@ -1083,7 +1118,9 @@ def _resolve_arg_value_types(self, argnames: List[str], indirect) -> Dict[str, s * "funcargs" if the argname should be a parameter to the parametrized test function. """ if isinstance(indirect, bool): - valtypes = dict.fromkeys(argnames, "params" if indirect else "funcargs") + valtypes = dict.fromkeys( + argnames, "params" if indirect else "funcargs" + ) # type: Dict[str, Literal['params', 'funcargs']] elif isinstance(indirect, Sequence): valtypes = dict.fromkeys(argnames, "funcargs") for arg in indirect: @@ -1198,17 +1235,20 @@ def _find_parametrized_scope(argnames, arg2fixturedefs, indirect): return "function" -def _ascii_escaped_by_config(val, config): +def _ascii_escaped_by_config(val: Union[str, bytes], config: Optional[Config]) -> str: if config is None: escape_option = False else: escape_option = config.getini( "disable_test_id_escaping_and_forfeit_all_rights_to_community_support" ) - return val if escape_option else ascii_escaped(val) + # TODO: If escaping is turned off and the user passes bytes, + # will return a bytes. For now we ignore this but the + # code *probably* doesn't handle this case. + return val if escape_option else ascii_escaped(val) # type: ignore -def _idval(val, argname, idx, idfn, item, config): +def _idval(val, argname, idx, idfn, item, config: Optional[Config]) -> str: if idfn: try: generated_id = idfn(val) @@ -1221,7 +1261,7 @@ def _idval(val, argname, idx, idfn, item, config): elif config: hook_id = config.hook.pytest_make_parametrize_id( config=config, val=val, argname=argname - ) + ) # type: Optional[str] if hook_id: return hook_id @@ -1239,7 +1279,15 @@ def _idval(val, argname, idx, idfn, item, config): return str(argname) + str(idx) -def _idvalset(idx, parameterset, argnames, idfn, ids, item, config): +def _idvalset( + idx, + parameterset: ParameterSet, + argnames: Iterable[str], + idfn, + ids, + item, + config: Optional[Config], +) -> str: if parameterset.id is not None: return parameterset.id if ids is None or (idx >= len(ids) or ids[idx] is None): @@ -1252,29 +1300,36 @@ def _idvalset(idx, parameterset, argnames, idfn, ids, item, config): return _ascii_escaped_by_config(ids[idx], config) -def idmaker(argnames, parametersets, idfn=None, ids=None, config=None, item=None): - ids = [ +def idmaker( + argnames: Iterable[str], + parametersets: Iterable[ParameterSet], + idfn=None, + ids=None, + config: Optional[Config] = None, + item=None, +) -> List[str]: + resolved_ids = [ _idvalset(valindex, parameterset, argnames, idfn, ids, config=config, item=item) for valindex, parameterset in enumerate(parametersets) ] # All IDs must be unique! - unique_ids = set(ids) - if len(unique_ids) != len(ids): + unique_ids = set(resolved_ids) + if len(unique_ids) != len(resolved_ids): # Record the number of occurrences of each test ID - test_id_counts = Counter(ids) + test_id_counts = Counter(resolved_ids) # Map the test ID to its next suffix - test_id_suffixes = defaultdict(int) + test_id_suffixes = defaultdict(int) # type: Dict[str, int] # Suffix non-unique IDs to make them unique - for index, test_id in enumerate(ids): + for index, test_id in enumerate(resolved_ids): if test_id_counts[test_id] > 1: - ids[index] = "{}{}".format(test_id, test_id_suffixes[test_id]) + resolved_ids[index] = "{}{}".format(test_id, test_id_suffixes[test_id]) test_id_suffixes[test_id] += 1 - return ids + return resolved_ids def show_fixtures_per_test(config): @@ -1335,13 +1390,13 @@ def write_item(item): write_item(session_item) -def showfixtures(config): +def showfixtures(config: Config) -> Union[int, ExitCode]: from _pytest.main import wrap_session return wrap_session(config, _showfixtures_main) -def _showfixtures_main(config, session): +def _showfixtures_main(config: Config, session: Session) -> None: import _pytest.config session.perform_collect() @@ -1352,7 +1407,7 @@ def _showfixtures_main(config, session): fm = session._fixturemanager available = [] - seen = set() + seen = set() # type: Set[Tuple[str, str]] for argname, fixturedefs in fm._arg2fixturedefs.items(): assert fixturedefs is not None @@ -1398,7 +1453,7 @@ def _showfixtures_main(config, session): tw.line() -def write_docstring(tw, doc, indent=" "): +def write_docstring(tw: TerminalWriter, doc: str, indent: str = " ") -> None: for line in doc.split("\n"): tw.write(indent + line + "\n") @@ -1417,13 +1472,13 @@ def __init__( parent, args=None, config=None, - callspec=None, + callspec: Optional[CallSpec2] = None, callobj=NOTSET, keywords=None, session=None, - fixtureinfo=None, + fixtureinfo: Optional[FuncFixtureInfo] = None, originalname=None, - ): + ) -> None: super().__init__(name, parent, config=config, session=session) self._args = args if callobj is not NOTSET: @@ -1459,7 +1514,7 @@ def __init__( fixtureinfo = self.session._fixturemanager.getfixtureinfo( self, self.obj, self.cls, funcargs=True ) - self._fixtureinfo = fixtureinfo + self._fixtureinfo = fixtureinfo # type: FuncFixtureInfo self.fixturenames = fixtureinfo.names_closure self._initrequest() diff --git a/src/_pytest/resultlog.py b/src/_pytest/resultlog.py index a977b29da43..5c40c03ad7a 100644 --- a/src/_pytest/resultlog.py +++ b/src/_pytest/resultlog.py @@ -5,8 +5,11 @@ import py +from _pytest.config import Config +from _pytest.config.argparsing import Parser -def pytest_addoption(parser): + +def pytest_addoption(parser: Parser) -> None: group = parser.getgroup("terminal reporting", "resultlog plugin options") group.addoption( "--resultlog", @@ -18,7 +21,7 @@ def pytest_addoption(parser): ) -def pytest_configure(config): +def pytest_configure(config: Config) -> None: resultlog = config.option.resultlog # prevent opening resultlog on slave nodes (xdist) if resultlog and not hasattr(config, "slaveinput"): @@ -26,8 +29,9 @@ def pytest_configure(config): if not os.path.isdir(dirname): os.makedirs(dirname) logfile = open(resultlog, "w", 1) # line buffered - config._resultlog = ResultLog(config, logfile) - config.pluginmanager.register(config._resultlog) + # Type ignored: pending mechanism to store typed objects scoped to config. + _resultlog = config._resultlog = ResultLog(config, logfile) # type: ignore # noqa: F821 + config.pluginmanager.register(_resultlog) from _pytest.deprecated import RESULT_LOG from _pytest.warnings import _issue_warning_captured @@ -35,11 +39,11 @@ def pytest_configure(config): _issue_warning_captured(RESULT_LOG, config.hook, stacklevel=2) -def pytest_unconfigure(config): +def pytest_unconfigure(config: Config) -> None: resultlog = getattr(config, "_resultlog", None) if resultlog: + del config._resultlog # type: ignore[attr-defined] # noqa: F821 resultlog.logfile.close() - del config._resultlog config.pluginmanager.unregister(resultlog) diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py index 3eb9ca28667..1f51a05e431 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -17,6 +17,7 @@ from _pytest._code.code import ExceptionInfo from _pytest._code.code import ExceptionRepr from _pytest.compat import TYPE_CHECKING +from _pytest.config.argparsing import Parser from _pytest.nodes import Collector from _pytest.nodes import Node from _pytest.outcomes import Exit @@ -26,11 +27,13 @@ if TYPE_CHECKING: from typing import Type + from _pytest.main import Session + # # pytest plugin hooks -def pytest_addoption(parser): +def pytest_addoption(parser: Parser) -> None: group = parser.getgroup("terminal reporting", "reporting", after="general") group.addoption( "--durations", @@ -71,11 +74,11 @@ def pytest_terminal_summary(terminalreporter): tr.write_line("{:02.2f}s {:<8} {}".format(rep.duration, rep.when, rep.nodeid)) -def pytest_sessionstart(session): +def pytest_sessionstart(session: "Session") -> None: session._setupstate = SetupState() -def pytest_sessionfinish(session): +def pytest_sessionfinish(session: "Session") -> None: session._setupstate.teardown_all() @@ -289,9 +292,9 @@ class SetupState: def __init__(self): self.stack = [] # type: List[Node] - self._finalizers = {} # type: Dict[Node, List[Callable[[], None]]] + self._finalizers = {} # type: Dict[Node, List[Callable[[], object]]] - def addfinalizer(self, finalizer, colitem): + def addfinalizer(self, finalizer: Callable[[], object], colitem) -> None: """ attach a finalizer to the given colitem. """ assert colitem and not isinstance(colitem, tuple) assert callable(finalizer) @@ -302,7 +305,7 @@ def _pop_and_teardown(self): colitem = self.stack.pop() self._teardown_with_finalization(colitem) - def _callfinalizers(self, colitem): + def _callfinalizers(self, colitem) -> None: finalizers = self._finalizers.pop(colitem, None) exc = None while finalizers: @@ -319,24 +322,24 @@ def _callfinalizers(self, colitem): assert val is not None raise val.with_traceback(tb) - def _teardown_with_finalization(self, colitem): + def _teardown_with_finalization(self, colitem) -> None: self._callfinalizers(colitem) colitem.teardown() for colitem in self._finalizers: assert colitem in self.stack - def teardown_all(self): + def teardown_all(self) -> None: while self.stack: self._pop_and_teardown() for key in list(self._finalizers): self._teardown_with_finalization(key) assert not self._finalizers - def teardown_exact(self, item, nextitem): + def teardown_exact(self, item, nextitem) -> None: needed_collectors = nextitem and nextitem.listchain() or [] self._teardown_towards(needed_collectors) - def _teardown_towards(self, needed_collectors): + def _teardown_towards(self, needed_collectors) -> None: exc = None while self.stack: if self.stack == needed_collectors[: len(self.stack)]: @@ -353,7 +356,7 @@ def _teardown_towards(self, needed_collectors): assert val is not None raise val.with_traceback(tb) - def prepare(self, colitem): + def prepare(self, colitem) -> None: """ setup objects along the collector chain to the test-method and teardown previously setup objects.""" needed_collectors = colitem.listchain() @@ -362,14 +365,14 @@ def prepare(self, colitem): # check if the last collection node has raised an error for col in self.stack: if hasattr(col, "_prepare_exc"): - _, val, tb = col._prepare_exc + _, val, tb = col._prepare_exc # type: ignore[attr-defined] # noqa: F821 raise val.with_traceback(tb) for col in needed_collectors[len(self.stack) :]: self.stack.append(col) try: col.setup() except TEST_OUTCOME: - col._prepare_exc = sys.exc_info() + col._prepare_exc = sys.exc_info() # type: ignore[attr-defined] # noqa: F821 raise diff --git a/src/_pytest/setuponly.py b/src/_pytest/setuponly.py index a277ebc8545..dd650379120 100644 --- a/src/_pytest/setuponly.py +++ b/src/_pytest/setuponly.py @@ -1,7 +1,16 @@ +from typing import Generator +from typing import Optional +from typing import Union + import pytest +from _pytest.config import Config +from _pytest.config import ExitCode +from _pytest.config.argparsing import Parser +from _pytest.fixtures import FixtureDef +from _pytest.fixtures import SubRequest -def pytest_addoption(parser): +def pytest_addoption(parser: Parser) -> None: group = parser.getgroup("debugconfig") group.addoption( "--setuponly", @@ -18,7 +27,9 @@ def pytest_addoption(parser): @pytest.hookimpl(hookwrapper=True) -def pytest_fixture_setup(fixturedef, request): +def pytest_fixture_setup( + fixturedef: FixtureDef, request: SubRequest +) -> Generator[None, None, None]: yield if request.config.option.setupshow: if hasattr(request, "param"): @@ -26,24 +37,25 @@ def pytest_fixture_setup(fixturedef, request): # display it now and during the teardown (in .finish()). if fixturedef.ids: if callable(fixturedef.ids): - fixturedef.cached_param = fixturedef.ids(request.param) + param = fixturedef.ids(request.param) else: - fixturedef.cached_param = fixturedef.ids[request.param_index] + param = fixturedef.ids[request.param_index] else: - fixturedef.cached_param = request.param + param = request.param + fixturedef.cached_param = param # type: ignore[attr-defined] # noqa: F821 _show_fixture_action(fixturedef, "SETUP") -def pytest_fixture_post_finalizer(fixturedef): - if hasattr(fixturedef, "cached_result"): +def pytest_fixture_post_finalizer(fixturedef: FixtureDef) -> None: + if fixturedef.cached_result is not None: config = fixturedef._fixturemanager.config if config.option.setupshow: _show_fixture_action(fixturedef, "TEARDOWN") if hasattr(fixturedef, "cached_param"): - del fixturedef.cached_param + del fixturedef.cached_param # type: ignore[attr-defined] # noqa: F821 -def _show_fixture_action(fixturedef, msg): +def _show_fixture_action(fixturedef: FixtureDef, msg: str) -> None: config = fixturedef._fixturemanager.config capman = config.pluginmanager.getplugin("capturemanager") if capman: @@ -66,13 +78,14 @@ def _show_fixture_action(fixturedef, msg): tw.write(" (fixtures used: {})".format(", ".join(deps))) if hasattr(fixturedef, "cached_param"): - tw.write("[{}]".format(fixturedef.cached_param)) + tw.write("[{}]".format(fixturedef.cached_param)) # type: ignore[attr-defined] # noqa: F821 if capman: capman.resume_global_capture() @pytest.hookimpl(tryfirst=True) -def pytest_cmdline_main(config): +def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]: if config.option.setuponly: config.option.setupshow = True + return None diff --git a/src/_pytest/setupplan.py b/src/_pytest/setupplan.py index 6fdd3aed064..0994ebbf207 100644 --- a/src/_pytest/setupplan.py +++ b/src/_pytest/setupplan.py @@ -1,7 +1,15 @@ +from typing import Optional +from typing import Union + import pytest +from _pytest.config import Config +from _pytest.config import ExitCode +from _pytest.config.argparsing import Parser +from _pytest.fixtures import FixtureDef +from _pytest.fixtures import SubRequest -def pytest_addoption(parser): +def pytest_addoption(parser: Parser) -> None: group = parser.getgroup("debugconfig") group.addoption( "--setupplan", @@ -13,16 +21,20 @@ def pytest_addoption(parser): @pytest.hookimpl(tryfirst=True) -def pytest_fixture_setup(fixturedef, request): +def pytest_fixture_setup( + fixturedef: FixtureDef, request: SubRequest +) -> Optional[object]: # Will return a dummy fixture if the setuponly option is provided. if request.config.option.setupplan: my_cache_key = fixturedef.cache_key(request) fixturedef.cached_result = (None, my_cache_key, None) return fixturedef.cached_result + return None @pytest.hookimpl(tryfirst=True) -def pytest_cmdline_main(config): +def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]: if config.option.setupplan: config.option.setuponly = True config.option.setupshow = True + return None diff --git a/src/_pytest/skipping.py b/src/_pytest/skipping.py index f70ef7f591c..c784b0b2526 100644 --- a/src/_pytest/skipping.py +++ b/src/_pytest/skipping.py @@ -1,12 +1,14 @@ """ support for skip/xfail functions and markers. """ +from _pytest.config import Config from _pytest.config import hookimpl +from _pytest.config.argparsing import Parser from _pytest.mark.evaluate import MarkEvaluator from _pytest.outcomes import fail from _pytest.outcomes import skip from _pytest.outcomes import xfail -def pytest_addoption(parser): +def pytest_addoption(parser: Parser) -> None: group = parser.getgroup("general") group.addoption( "--runxfail", @@ -25,7 +27,7 @@ def pytest_addoption(parser): ) -def pytest_configure(config): +def pytest_configure(config: Config) -> None: if config.option.runxfail: # yay a hack import pytest @@ -36,7 +38,7 @@ def pytest_configure(config): def nop(*args, **kwargs): pass - nop.Exception = xfail.Exception + nop.Exception = xfail.Exception # type: ignore[attr-defined] # noqa: F821 setattr(pytest, "xfail", nop) config.addinivalue_line( diff --git a/src/_pytest/stepwise.py b/src/_pytest/stepwise.py index 6fa21cd1c65..3cbf0be9fc0 100644 --- a/src/_pytest/stepwise.py +++ b/src/_pytest/stepwise.py @@ -1,7 +1,10 @@ import pytest +from _pytest.config import Config +from _pytest.config.argparsing import Parser +from _pytest.main import Session -def pytest_addoption(parser): +def pytest_addoption(parser: Parser) -> None: group = parser.getgroup("general") group.addoption( "--sw", @@ -19,7 +22,7 @@ def pytest_addoption(parser): @pytest.hookimpl -def pytest_configure(config): +def pytest_configure(config: Config) -> None: config.pluginmanager.register(StepwisePlugin(config), "stepwiseplugin") @@ -34,7 +37,7 @@ def __init__(self, config): self.lastfailed = config.cache.get("cache/stepwise", None) self.skip = config.getvalue("stepwise_skip") - def pytest_sessionstart(self, session): + def pytest_sessionstart(self, session: Session) -> None: self.session = session def pytest_collection_modifyitems(self, session, config, items): @@ -100,7 +103,7 @@ def pytest_report_collectionfinish(self): if self.active and self.config.getoption("verbose") >= 0 and self.report_status: return "stepwise: %s" % self.report_status - def pytest_sessionfinish(self, session): + def pytest_sessionfinish(self, session: Session) -> None: if self.active: self.config.cache.set("cache/stepwise", self.lastfailed) else: diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 9f12015d6c9..c6073e7010b 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -17,6 +17,7 @@ from typing import Optional from typing import Set from typing import Tuple +from typing import Union import attr import pluggy @@ -25,8 +26,10 @@ import pytest from _pytest import nodes +from _pytest.config import _PluggyPlugin from _pytest.config import Config from _pytest.config import ExitCode +from _pytest.config.argparsing import Parser from _pytest.main import Session from _pytest.reports import CollectReport from _pytest.reports import TestReport @@ -61,7 +64,7 @@ def __call__(self, parser, namespace, values, option_string=None): namespace.quiet = getattr(namespace, "quiet", 0) + 1 -def pytest_addoption(parser): +def pytest_addoption(parser: Parser) -> None: group = parser.getgroup("terminal reporting", "reporting", after="general") group._addoption( "-v", @@ -391,7 +394,7 @@ def pytest_warning_captured(self, warning_message, item): ) warnings.append(warning_report) - def pytest_plugin_registered(self, plugin): + def pytest_plugin_registered(self, plugin: _PluggyPlugin) -> None: if self.config.option.traceconfig: msg = "PLUGIN registered: {}".format(plugin) # XXX this event may happen during setup/teardown time @@ -683,7 +686,7 @@ def _printcollecteditems(self, items): self._tw.line("{}{}".format(indent + " ", line.strip())) @pytest.hookimpl(hookwrapper=True) - def pytest_sessionfinish(self, session: Session, exitstatus: ExitCode): + def pytest_sessionfinish(self, session: Session, exitstatus: Union[int, ExitCode]): outcome = yield outcome.get_result() self._tw.line("") @@ -718,10 +721,10 @@ def pytest_terminal_summary(self): # Display any extra warnings from teardown here (if any). self.summary_warnings() - def pytest_keyboard_interrupt(self, excinfo): + def pytest_keyboard_interrupt(self, excinfo) -> None: self._keyboardinterrupt_memo = excinfo.getrepr(funcargs=True) - def pytest_unconfigure(self): + def pytest_unconfigure(self) -> None: if hasattr(self, "_keyboardinterrupt_memo"): self._report_keyboardinterrupt() diff --git a/src/_pytest/tmpdir.py b/src/_pytest/tmpdir.py index 85c5b838101..aa0338dbf58 100644 --- a/src/_pytest/tmpdir.py +++ b/src/_pytest/tmpdir.py @@ -13,6 +13,7 @@ from .pathlib import make_numbered_dir from .pathlib import make_numbered_dir_with_cleanup from .pathlib import Path +from _pytest.config import Config from _pytest.fixtures import FixtureRequest from _pytest.monkeypatch import MonkeyPatch @@ -135,7 +136,7 @@ def get_user() -> Optional[str]: return None -def pytest_configure(config) -> None: +def pytest_configure(config: Config) -> None: """Create a TempdirFactory and attach it to the config object. This is to comply with existing plugins which expect the handler to be diff --git a/src/_pytest/warnings.py b/src/_pytest/warnings.py index 18e4def2116..bbe11ac73de 100644 --- a/src/_pytest/warnings.py +++ b/src/_pytest/warnings.py @@ -4,10 +4,12 @@ from typing import Generator import pytest +from _pytest.config import Config +from _pytest.config.argparsing import Parser from _pytest.main import Session -def _setoption(wmod, arg): +def _setoption(wmod, arg) -> None: """ Copy of the warning._setoption function but does not escape arguments. """ @@ -31,7 +33,7 @@ def _setoption(wmod, arg): wmod.filterwarnings(action, message, category, module, lineno) -def pytest_addoption(parser): +def pytest_addoption(parser: Parser) -> None: group = parser.getgroup("pytest-warnings") group.addoption( "-W", @@ -48,7 +50,7 @@ def pytest_addoption(parser): ) -def pytest_configure(config): +def pytest_configure(config: Config) -> None: config.addinivalue_line( "markers", "filterwarnings(warning): add a warning filter to the given test. " diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index ac13011e370..f720b8012c4 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -34,7 +34,7 @@ def listchain(self): names = fixtures.getfuncargnames(func) fixtureinfo = FixtureInfo(names) definition = DefinitionMock._create(func) - return python.Metafunc(definition, fixtureinfo, config) + return python.Metafunc(definition, fixtureinfo, config) # type: ignore[arg-type] # noqa: F821 def test_no_funcargs(self): def function(): @@ -318,7 +318,8 @@ def getini(self, name): ("ação", MockConfig({option: False}), "a\\xe7\\xe3o"), ] for val, config, expected in values: - assert _idval(val, "a", 6, None, item=None, config=config) == expected + actual = _idval(val, "a", 6, None, item=None, config=config) # type: ignore[arg-type] # noqa: F821 + assert actual == expected def test_bytes_idval(self): """unittest for the expected behavior to obtain ids for parametrized @@ -511,7 +512,10 @@ def getini(self, name): ] for config, expected in values: result = idmaker( - ("a",), [pytest.param("string")], idfn=lambda _: "ação", config=config + ("a",), + [pytest.param("string")], + idfn=lambda _: "ação", + config=config, # type: ignore[arg-type] # noqa: F821 ) assert result == [expected] @@ -544,7 +548,10 @@ def getini(self, name): ] for config, expected in values: result = idmaker( - ("a",), [pytest.param("string")], ids=["ação"], config=config + ("a",), + [pytest.param("string")], + ids=["ação"], + config=config, # type: ignore[arg-type] # noqa: F821 ) assert result == [expected] From c4508a46e00af38b0db1b592052e54aaa9e97108 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 12 Feb 2020 21:04:45 +0200 Subject: [PATCH 3/3] Py35 fixes --- src/_pytest/config/__init__.py | 2 +- src/_pytest/fixtures.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 46df9144b96..62b522330e1 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -770,7 +770,7 @@ def __init__( self, pluginmanager: PytestPluginManager, *, - invocation_params: Optional[InvocationParams] = None, + invocation_params: Optional[InvocationParams] = None ) -> None: from .argparsing import Parser, FILE_OR_DIR diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 1a8a6d49500..a0ba0078d84 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -47,9 +47,9 @@ from _pytest.outcomes import TEST_OUTCOME if TYPE_CHECKING: - from typing import Literal from typing import NoReturn from typing import Type + from typing_extensions import Literal from _pytest import nodes from _pytest.main import Session