Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 20 additions & 17 deletions src/_pytest/_code/code.py
Original file line number Diff line number Diff line change
Expand Up @@ -549,7 +549,7 @@ def getrepr(
funcargs: bool = False,
truncate_locals: bool = True,
chain: bool = True,
):
) -> Union["ReprExceptionInfo", "ExceptionChainRepr"]:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Functions returning a Union are not great, because the caller must disambiguate which one they got.

In this case, both types share a common base class ExceptionRepr. If all the caller cares about is getting an ExceptionRepr from this function, I suggest using that instead. This will also be more future proof.

If however the distinction is important (not in this case AFAIU), I think it will be better to either use two overloads on style, Literal["long"] which only returns ExceptionChainRepr and Literal["native"] which only returns ReprExceptionInfo. Or just split the function to two.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the input.
I've tried to be as strict / verbose as possible, given the code around all this.
Will consider using ExceptionRepr then.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if you decided to keep it this way.

Other than this, LGTM.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, better have it (re)strict(ed) for now.

"""
Return str()able representation of this exception info.

Expand Down Expand Up @@ -818,19 +818,19 @@ def _truncate_recursive_traceback(self, traceback):

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
Expand All @@ -842,7 +842,7 @@ def repr_excinfo(self, excinfo):
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
Expand All @@ -852,7 +852,7 @@ def repr_excinfo(self, excinfo):
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
Expand All @@ -876,6 +876,9 @@ def __str__(self):
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:
Expand All @@ -884,7 +887,7 @@ def __init__(self) -> None:
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)
Expand All @@ -899,7 +902,7 @@ def __init__(self, chain):
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:
Expand All @@ -914,7 +917,7 @@ def __init__(self, reprtraceback, reprcrash):
self.reprtraceback = reprtraceback
self.reprcrash = reprcrash

def toterminal(self, tw):
def toterminal(self, tw) -> None:
self.reprtraceback.toterminal(tw)
super().toterminal(tw)

Expand All @@ -927,7 +930,7 @@ def __init__(self, reprentries, extraline, style):
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":
Expand Down Expand Up @@ -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))


Expand All @@ -971,7 +974,7 @@ def __init__(self, lines, reprfuncargs, reprlocals, filelocrepr, style):
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:
Expand Down Expand Up @@ -1003,7 +1006,7 @@ def __init__(self, path, lineno, message):
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
Expand All @@ -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)

Expand All @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion src/_pytest/doctest.py
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,7 @@ def repr_failure(self, excinfo):
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


Expand Down
14 changes: 10 additions & 4 deletions src/_pytest/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@
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
Expand All @@ -35,6 +35,8 @@
if False: # TYPE_CHECKING
from typing import Type

from _pytest import nodes


@attr.s(frozen=True)
class PseudoFixtureDef:
Expand Down Expand Up @@ -689,8 +691,8 @@ def __init__(self, argname, request, msg=None):
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))
Expand Down Expand Up @@ -742,7 +744,7 @@ def __init__(self, filename, firstlineno, tblines, errorstring, argname):
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())
Expand Down Expand Up @@ -1283,6 +1285,8 @@ def pytest_plugin_registered(self, plugin):
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)
Expand Down Expand Up @@ -1459,6 +1463,8 @@ def getfixturedefs(self, argname, nodeid):
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
13 changes: 9 additions & 4 deletions src/_pytest/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import importlib
import os
import sys
from typing import Dict

import attr
import py
Expand All @@ -16,6 +17,7 @@
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):
Expand Down Expand Up @@ -359,15 +361,16 @@ 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


class Session(nodes.FSCollector):
Interrupted = Interrupted
Failed = Failed
_setupstate = None # type: SetupState

def __init__(self, config):
nodes.FSCollector.__init__(
Expand All @@ -383,7 +386,9 @@ def __init__(self, config):
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 = {}

Expand All @@ -398,7 +403,7 @@ def __repr__(self):
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]

Expand Down
55 changes: 39 additions & 16 deletions src/_pytest/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,29 @@
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

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 = "/"

Expand Down Expand Up @@ -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

Expand All @@ -81,7 +94,11 @@ def __init__(
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)
Expand Down Expand Up @@ -254,13 +271,13 @@ def getparent(self, cls):
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"
Expand Down Expand Up @@ -298,7 +315,9 @@ def _repr_failure_py(self, excinfo, style=None):
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)


Expand Down Expand Up @@ -425,16 +444,20 @@ def add_report_section(self, when: str, key: str, content: str) -> None:
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
Loading