diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index 18f576332..1a35521ad 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -549,7 +549,7 @@ class ExceptionInfo(Generic[_E]): funcargs: bool = False, truncate_locals: bool = True, chain: bool = True, - ): + ) -> Union["ReprExceptionInfo", "ExceptionChainRepr"]: """ Return str()able representation of this exception info. @@ -818,19 +818,19 @@ class FormattedExcinfo: return traceback, extraline - def repr_excinfo(self, excinfo): - + def repr_excinfo(self, excinfo: ExceptionInfo) -> "ExceptionChainRepr": repr_chain = ( [] ) # type: List[Tuple[ReprTraceback, Optional[ReprFileLocation], Optional[str]]] e = excinfo.value + excinfo_ = excinfo # type: Optional[ExceptionInfo] descr = None seen = set() # type: Set[int] while e is not None and id(e) not in seen: seen.add(id(e)) - if excinfo: - reprtraceback = self.repr_traceback(excinfo) - reprcrash = excinfo._getreprcrash() + if excinfo_: + reprtraceback = self.repr_traceback(excinfo_) + reprcrash = excinfo_._getreprcrash() # type: Optional[ReprFileLocation] else: # fallback to native repr if the exception doesn't have a traceback: # ExceptionInfo objects require a full traceback to work @@ -842,7 +842,7 @@ class FormattedExcinfo: repr_chain += [(reprtraceback, reprcrash, descr)] if e.__cause__ is not None and self.chain: e = e.__cause__ - excinfo = ( + excinfo_ = ( ExceptionInfo((type(e), e, e.__traceback__)) if e.__traceback__ else None @@ -852,7 +852,7 @@ class FormattedExcinfo: e.__context__ is not None and not e.__suppress_context__ and self.chain ): e = e.__context__ - excinfo = ( + excinfo_ = ( ExceptionInfo((type(e), e, e.__traceback__)) if e.__traceback__ else None @@ -876,6 +876,9 @@ class TerminalRepr: def __repr__(self): return "<{} instance at {:0x}>".format(self.__class__, id(self)) + def toterminal(self, tw) -> None: + raise NotImplementedError() + class ExceptionRepr(TerminalRepr): def __init__(self) -> None: @@ -884,7 +887,7 @@ class ExceptionRepr(TerminalRepr): def addsection(self, name, content, sep="-"): self.sections.append((name, content, sep)) - def toterminal(self, tw): + def toterminal(self, tw) -> None: for name, content, sep in self.sections: tw.sep(sep, name) tw.line(content) @@ -899,7 +902,7 @@ class ExceptionChainRepr(ExceptionRepr): self.reprtraceback = chain[-1][0] self.reprcrash = chain[-1][1] - def toterminal(self, tw): + def toterminal(self, tw) -> None: for element in self.chain: element[0].toterminal(tw) if element[2] is not None: @@ -914,7 +917,7 @@ class ReprExceptionInfo(ExceptionRepr): self.reprtraceback = reprtraceback self.reprcrash = reprcrash - def toterminal(self, tw): + def toterminal(self, tw) -> None: self.reprtraceback.toterminal(tw) super().toterminal(tw) @@ -927,7 +930,7 @@ class ReprTraceback(TerminalRepr): self.extraline = extraline self.style = style - def toterminal(self, tw): + def toterminal(self, tw) -> None: # the entries might have different styles for i, entry in enumerate(self.reprentries): if entry.style == "long": @@ -959,7 +962,7 @@ class ReprEntryNative(TerminalRepr): def __init__(self, tblines): self.lines = tblines - def toterminal(self, tw): + def toterminal(self, tw) -> None: tw.write("".join(self.lines)) @@ -971,7 +974,7 @@ class ReprEntry(TerminalRepr): self.reprfileloc = filelocrepr self.style = style - def toterminal(self, tw): + def toterminal(self, tw) -> None: if self.style == "short": self.reprfileloc.toterminal(tw) for line in self.lines: @@ -1003,7 +1006,7 @@ class ReprFileLocation(TerminalRepr): self.lineno = lineno self.message = message - def toterminal(self, tw): + def toterminal(self, tw) -> None: # filename and lineno output for each entry, # using an output format that most editors unterstand msg = self.message @@ -1018,7 +1021,7 @@ class ReprLocals(TerminalRepr): def __init__(self, lines): self.lines = lines - def toterminal(self, tw): + def toterminal(self, tw) -> None: for line in self.lines: tw.line(line) @@ -1027,7 +1030,7 @@ class ReprFuncArgs(TerminalRepr): def __init__(self, args): self.args = args - def toterminal(self, tw): + def toterminal(self, tw) -> None: if self.args: linesofar = "" for name, value in self.args: diff --git a/src/_pytest/doctest.py b/src/_pytest/doctest.py index 48c934e3a..f7d96257e 100644 --- a/src/_pytest/doctest.py +++ b/src/_pytest/doctest.py @@ -305,7 +305,7 @@ class DoctestItem(pytest.Item): else: return super().repr_failure(excinfo) - def reportinfo(self): + def reportinfo(self) -> Tuple[str, int, str]: return self.fspath, self.dtest.lineno, "[doctest] %s" % self.name diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index aae1371ec..fc55ef2cf 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -7,13 +7,13 @@ from collections import defaultdict from collections import deque from collections import OrderedDict from typing import Dict +from typing import List from typing import Tuple import attr import py import _pytest -from _pytest import nodes from _pytest._code.code import FormattedExcinfo from _pytest._code.code import TerminalRepr from _pytest.compat import _format_args @@ -35,6 +35,8 @@ from _pytest.outcomes import TEST_OUTCOME if False: # TYPE_CHECKING from typing import Type + from _pytest import nodes + @attr.s(frozen=True) class PseudoFixtureDef: @@ -689,8 +691,8 @@ class FixtureLookupError(LookupError): self.fixturestack = request._get_fixturestack() self.msg = msg - def formatrepr(self): - tblines = [] + def formatrepr(self) -> "FixtureLookupErrorRepr": + tblines = [] # type: List[str] addline = tblines.append stack = [self.request._pyfuncitem.obj] stack.extend(map(lambda x: x.func, self.fixturestack)) @@ -742,7 +744,7 @@ class FixtureLookupErrorRepr(TerminalRepr): self.firstlineno = firstlineno self.argname = argname - def toterminal(self, tw): + def toterminal(self, tw) -> None: # tw.line("FixtureLookupError: %s" %(self.argname), red=True) for tbline in self.tblines: tw.line(tbline.rstrip()) @@ -1283,6 +1285,8 @@ class FixtureManager: except AttributeError: pass else: + from _pytest import nodes + # construct the base nodeid which is later used to check # what fixtures are visible for particular tests (as denoted # by their test id) @@ -1459,6 +1463,8 @@ class FixtureManager: return tuple(self._matchfactories(fixturedefs, nodeid)) def _matchfactories(self, fixturedefs, nodeid): + from _pytest import nodes + for fixturedef in fixturedefs: if nodes.ischildnode(fixturedef.baseid, nodeid): yield fixturedef diff --git a/src/_pytest/main.py b/src/_pytest/main.py index 7b3855e6c..084d68dab 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -5,6 +5,7 @@ import functools import importlib import os import sys +from typing import Dict import attr import py @@ -16,6 +17,7 @@ from _pytest.config import hookimpl from _pytest.config import UsageError from _pytest.outcomes import exit from _pytest.runner import collect_one_node +from _pytest.runner import SetupState class ExitCode(enum.IntEnum): @@ -359,8 +361,8 @@ class Failed(Exception): class _bestrelpath_cache(dict): path = attr.ib() - def __missing__(self, path): - r = self.path.bestrelpath(path) + def __missing__(self, path: str) -> str: + r = self.path.bestrelpath(path) # type: str self[path] = r return r @@ -368,6 +370,7 @@ class _bestrelpath_cache(dict): class Session(nodes.FSCollector): Interrupted = Interrupted Failed = Failed + _setupstate = None # type: SetupState def __init__(self, config): nodes.FSCollector.__init__( @@ -383,7 +386,9 @@ class Session(nodes.FSCollector): self._initialpaths = frozenset() # Keep track of any collected nodes in here, so we don't duplicate fixtures self._node_cache = {} - self._bestrelpathcache = _bestrelpath_cache(config.rootdir) + self._bestrelpathcache = _bestrelpath_cache( + config.rootdir + ) # type: Dict[str, str] # Dirnames of pkgs with dunder-init files. self._pkg_roots = {} @@ -398,7 +403,7 @@ class Session(nodes.FSCollector): self.testscollected, ) - def _node_location_to_relpath(self, node_path): + def _node_location_to_relpath(self, node_path: str) -> str: # bestrelpath is a quite slow function return self._bestrelpathcache[node_path] diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index 71036dc7e..d72eaeb0a 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -4,6 +4,7 @@ from functools import lru_cache from typing import Any from typing import Dict from typing import List +from typing import Optional from typing import Set from typing import Tuple from typing import Union @@ -11,15 +12,21 @@ from typing import Union import py import _pytest._code +from _pytest._code.code import ExceptionChainRepr +from _pytest._code.code import ExceptionInfo +from _pytest._code.code import ReprExceptionInfo from _pytest.compat import getfslineno +from _pytest.fixtures import FixtureDef +from _pytest.fixtures import FixtureLookupError +from _pytest.fixtures import FixtureLookupErrorRepr from _pytest.mark.structures import Mark from _pytest.mark.structures import MarkDecorator from _pytest.mark.structures import NodeKeywords -from _pytest.outcomes import fail +from _pytest.outcomes import Failed if False: # TYPE_CHECKING # Imported here due to circular import. - from _pytest.fixtures import FixtureDef + from _pytest.main import Session # noqa: F401 SEP = "/" @@ -69,8 +76,14 @@ class Node: Collector subclasses have children, Items are terminal nodes.""" def __init__( - self, name, parent=None, config=None, session=None, fspath=None, nodeid=None - ): + self, + name, + parent=None, + config=None, + session: Optional["Session"] = None, + fspath=None, + nodeid=None, + ) -> None: #: a unique name within the scope of the parent node self.name = name @@ -81,7 +94,11 @@ class Node: self.config = config or parent.config #: the session this node is part of - self.session = session or parent.session + if session is None: + assert parent.session is not None + self.session = parent.session + else: + self.session = session #: filesystem path where this node was collected from (can be None) self.fspath = fspath or getattr(parent, "fspath", None) @@ -254,13 +271,13 @@ class Node: def _prunetraceback(self, excinfo): pass - def _repr_failure_py(self, excinfo, style=None): - # Type ignored: see comment where fail.Exception is defined. - if excinfo.errisinstance(fail.Exception): # type: ignore + def _repr_failure_py( + self, excinfo: ExceptionInfo[Union[Failed, FixtureLookupError]], style=None + ) -> Union[str, ReprExceptionInfo, ExceptionChainRepr, FixtureLookupErrorRepr]: + if isinstance(excinfo.value, Failed): if not excinfo.value.pytrace: return str(excinfo.value) - fm = self.session._fixturemanager - if excinfo.errisinstance(fm.FixtureLookupError): + if isinstance(excinfo.value, FixtureLookupError): return excinfo.value.formatrepr() if self.config.getoption("fulltrace", False): style = "long" @@ -298,7 +315,9 @@ class Node: truncate_locals=truncate_locals, ) - def repr_failure(self, excinfo, style=None): + def repr_failure( + self, excinfo, style=None + ) -> Union[str, ReprExceptionInfo, ExceptionChainRepr, FixtureLookupErrorRepr]: return self._repr_failure_py(excinfo, style) @@ -425,16 +444,20 @@ class Item(Node): if content: self._report_sections.append((when, key, content)) - def reportinfo(self): + def reportinfo(self) -> Tuple[str, Optional[int], str]: return self.fspath, None, "" @property - def location(self): + def location(self) -> Tuple[str, Optional[int], str]: try: return self._location except AttributeError: location = self.reportinfo() fspath = self.session._node_location_to_relpath(location[0]) - location = (fspath, location[1], str(location[2])) - self._location = location - return location + assert type(location[2]) is str + self._location = ( + fspath, + location[1], + location[2], + ) # type: Tuple[str, Optional[int], str] + return self._location diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 913a93bc0..61cbfec8a 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -9,6 +9,7 @@ from collections import Counter from collections.abc import Sequence from functools import partial from textwrap import dedent +from typing import Tuple import py @@ -288,7 +289,7 @@ class PyobjMixin(PyobjContext): s = ".".join(parts) return s.replace(".[", "[") - def reportinfo(self): + def reportinfo(self) -> Tuple[str, int, str]: # XXX caching? obj = self.obj compat_co_firstlineno = getattr(obj, "compat_co_firstlineno", None) diff --git a/src/_pytest/reports.py b/src/_pytest/reports.py index b1592f817..53f28e73f 100644 --- a/src/_pytest/reports.py +++ b/src/_pytest/reports.py @@ -1,6 +1,8 @@ from io import StringIO from pprint import pprint +from typing import List from typing import Optional +from typing import Tuple from typing import Union import py @@ -15,6 +17,7 @@ from _pytest._code.code import ReprFuncArgs from _pytest._code.code import ReprLocals from _pytest._code.code import ReprTraceback from _pytest._code.code import TerminalRepr +from _pytest.nodes import Node from _pytest.outcomes import skip from _pytest.pathlib import Path @@ -34,13 +37,16 @@ def getslaveinfoline(node): class BaseReport: when = None # type: Optional[str] location = None + longrepr = None + sections = [] # type: List[Tuple[str, str]] + nodeid = None # type: str def __init__(self, **kw): self.__dict__.update(kw) - def toterminal(self, out): + def toterminal(self, out) -> None: if hasattr(self, "node"): - out.line(getslaveinfoline(self.node)) + out.line(getslaveinfoline(self.node)) # type: ignore longrepr = self.longrepr if longrepr is None: @@ -300,7 +306,9 @@ class TestReport(BaseReport): class CollectReport(BaseReport): when = "collect" - def __init__(self, nodeid, outcome, longrepr, result, sections=(), **extra): + def __init__( + self, nodeid: str, outcome, longrepr, result: List[Node], sections=(), **extra + ) -> None: self.nodeid = nodeid self.outcome = outcome self.longrepr = longrepr @@ -322,7 +330,7 @@ class CollectErrorRepr(TerminalRepr): def __init__(self, msg): self.longrepr = msg - def toterminal(self, out): + def toterminal(self, out) -> None: out.line(self.longrepr, red=True) @@ -472,7 +480,9 @@ def _report_kwargs_from_json(reportdict): description, ) ) - exception_info = ExceptionChainRepr(chain) + exception_info = ExceptionChainRepr( + chain + ) # type: Union[ExceptionChainRepr,ReprExceptionInfo] else: exception_info = ReprExceptionInfo(reprtraceback, reprcrash) diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py index 29f9658ee..c383146c3 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -6,6 +6,7 @@ from time import time from typing import Callable from typing import Dict from typing import List +from typing import Optional from typing import Tuple import attr @@ -207,8 +208,7 @@ class CallInfo: """ Result/Exception info a function invocation. """ _result = attr.ib() - # Optional[ExceptionInfo] - excinfo = attr.ib() + excinfo = attr.ib(type=Optional[ExceptionInfo]) start = attr.ib() stop = attr.ib() when = attr.ib() @@ -220,7 +220,7 @@ class CallInfo: return self._result @classmethod - def from_call(cls, func, when, reraise=None): + def from_call(cls, func, when, reraise=None) -> "CallInfo": #: context of invocation: one of "setup", "call", #: "teardown", "memocollect" start = time() diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index 3f205b131..b431bb66d 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -902,7 +902,7 @@ raise ValueError() from _pytest._code.code import TerminalRepr class MyRepr(TerminalRepr): - def toterminal(self, tw): + def toterminal(self, tw) -> None: tw.line("я") x = str(MyRepr())