Skip to content

Type-annotate pytest.{exit,skip,fail,xfail,importorskip,warns,raises} #5593

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Jul 16, 2019
Merged
116 changes: 85 additions & 31 deletions src/_pytest/_code/code.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@
from inspect import CO_VARARGS
from inspect import CO_VARKEYWORDS
from traceback import format_exception_only
from types import TracebackType
from typing import Generic
from typing import Optional
from typing import Pattern
from typing import Tuple
from typing import TypeVar
from typing import Union
from weakref import ref

import attr
Expand All @@ -15,6 +22,9 @@
from _pytest._io.saferepr import safeformat
from _pytest._io.saferepr import saferepr

if False: # TYPE_CHECKING
from typing import Type


class Code:
""" wrapper around Python code objects """
Expand Down Expand Up @@ -371,21 +381,28 @@ def recursionindex(self):
)


_E = TypeVar("_E", bound=BaseException)


@attr.s(repr=False)
class ExceptionInfo:
class ExceptionInfo(Generic[_E]):
""" wraps sys.exc_info() objects and offers
help for navigating the traceback.
"""

_assert_start_repr = "AssertionError('assert "

_excinfo = attr.ib()
_striptext = attr.ib(default="")
_traceback = attr.ib(default=None)
_excinfo = attr.ib(type=Optional[Tuple["Type[_E]", "_E", TracebackType]])
_striptext = attr.ib(type=str, default="")
_traceback = attr.ib(type=Optional[Traceback], default=None)

@classmethod
def from_current(cls, exprinfo=None):
"""returns an ExceptionInfo matching the current traceback
def from_exc_info(
cls,
exc_info: Tuple["Type[_E]", "_E", TracebackType],
exprinfo: Optional[str] = None,
) -> "ExceptionInfo[_E]":
"""returns an ExceptionInfo for an existing exc_info tuple.

.. warning::

Expand All @@ -396,61 +413,98 @@ def from_current(cls, exprinfo=None):
strip ``AssertionError`` from the output, defaults
to the exception message/``__str__()``
"""
tup = sys.exc_info()
assert tup[0] is not None, "no current exception"
_striptext = ""
if exprinfo is None and isinstance(tup[1], AssertionError):
exprinfo = getattr(tup[1], "msg", None)
if exprinfo is None and isinstance(exc_info[1], AssertionError):
exprinfo = getattr(exc_info[1], "msg", None)
if exprinfo is None:
exprinfo = saferepr(tup[1])
exprinfo = saferepr(exc_info[1])
if exprinfo and exprinfo.startswith(cls._assert_start_repr):
_striptext = "AssertionError: "

return cls(tup, _striptext)
return cls(exc_info, _striptext)

@classmethod
def for_later(cls):
def from_current(
cls, exprinfo: Optional[str] = None
) -> "ExceptionInfo[BaseException]":
"""returns an ExceptionInfo matching the current traceback

.. warning::

Experimental API


:param exprinfo: a text string helping to determine if we should
strip ``AssertionError`` from the output, defaults
to the exception message/``__str__()``
"""
tup = sys.exc_info()
assert tup[0] is not None, "no current exception"
assert tup[1] is not None, "no current exception"
assert tup[2] is not None, "no current exception"
exc_info = (tup[0], tup[1], tup[2])
return cls.from_exc_info(exc_info)

@classmethod
def for_later(cls) -> "ExceptionInfo[_E]":
"""return an unfilled ExceptionInfo
"""
return cls(None)

def fill_unfilled(self, exc_info: Tuple["Type[_E]", _E, TracebackType]) -> None:
"""fill an unfilled ExceptionInfo created with for_later()"""
assert self._excinfo is None, "ExceptionInfo was already filled"
self._excinfo = exc_info

@property
def type(self):
def type(self) -> "Type[_E]":
"""the exception class"""
assert (
self._excinfo is not None
), ".type can only be used after the context manager exits"
return self._excinfo[0]

@property
def value(self):
def value(self) -> _E:
"""the exception value"""
assert (
self._excinfo is not None
), ".value can only be used after the context manager exits"
return self._excinfo[1]

@property
def tb(self):
def tb(self) -> TracebackType:
"""the exception raw traceback"""
assert (
self._excinfo is not None
), ".tb can only be used after the context manager exits"
return self._excinfo[2]

@property
def typename(self):
def typename(self) -> str:
"""the type name of the exception"""
assert (
self._excinfo is not None
), ".typename can only be used after the context manager exits"
return self.type.__name__

@property
def traceback(self):
def traceback(self) -> Traceback:
"""the traceback"""
if self._traceback is None:
self._traceback = Traceback(self.tb, excinfo=ref(self))
return self._traceback

@traceback.setter
def traceback(self, value):
def traceback(self, value: Traceback) -> None:
self._traceback = value

def __repr__(self):
def __repr__(self) -> str:
if self._excinfo is None:
return "<ExceptionInfo for raises contextmanager>"
return "<ExceptionInfo %s tblen=%d>" % (self.typename, len(self.traceback))

def exconly(self, tryshort=False):
def exconly(self, tryshort: bool = False) -> str:
""" return the exception as a string

when 'tryshort' resolves to True, and the exception is a
Expand All @@ -466,25 +520,25 @@ def exconly(self, tryshort=False):
text = text[len(self._striptext) :]
return text

def errisinstance(self, exc):
def errisinstance(self, exc: "Type[BaseException]") -> bool:
""" return True if the exception is an instance of exc """
return isinstance(self.value, exc)

def _getreprcrash(self):
def _getreprcrash(self) -> "ReprFileLocation":
exconly = self.exconly(tryshort=True)
entry = self.traceback.getcrashentry()
path, lineno = entry.frame.code.raw.co_filename, entry.lineno
return ReprFileLocation(path, lineno + 1, exconly)

def getrepr(
self,
showlocals=False,
style="long",
abspath=False,
tbfilter=True,
funcargs=False,
truncate_locals=True,
chain=True,
showlocals: bool = False,
style: str = "long",
abspath: bool = False,
tbfilter: bool = True,
funcargs: bool = False,
truncate_locals: bool = True,
chain: bool = True,
):
"""
Return str()able representation of this exception info.
Expand Down Expand Up @@ -535,7 +589,7 @@ def getrepr(
)
return fmt.repr_excinfo(self)

def match(self, regexp):
def match(self, regexp: Union[str, Pattern]) -> bool:
"""
Check whether the regular expression 'regexp' is found in the string
representation of the exception using ``re.search``. If it matches
Expand Down
32 changes: 23 additions & 9 deletions src/_pytest/outcomes.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,26 @@
as well as functions creating them
"""
import sys
from typing import Any
from typing import Optional

from packaging.version import Version

if False: # TYPE_CHECKING
from typing import NoReturn


class OutcomeException(BaseException):
""" OutcomeException and its subclass instances indicate and
contain info about test and collection outcomes.
"""

def __init__(self, msg=None, pytrace=True):
def __init__(self, msg: Optional[str] = None, pytrace: bool = True) -> None:
BaseException.__init__(self, msg)
self.msg = msg
self.pytrace = pytrace

def __repr__(self):
def __repr__(self) -> str:
if self.msg:
val = self.msg
if isinstance(val, bytes):
Expand All @@ -36,7 +41,12 @@ class Skipped(OutcomeException):
# in order to have Skipped exception printing shorter/nicer
__module__ = "builtins"

def __init__(self, msg=None, pytrace=True, allow_module_level=False):
def __init__(
self,
msg: Optional[str] = None,
pytrace: bool = True,
allow_module_level: bool = False,
) -> None:
OutcomeException.__init__(self, msg=msg, pytrace=pytrace)
self.allow_module_level = allow_module_level

Expand All @@ -50,7 +60,9 @@ class Failed(OutcomeException):
class Exit(Exception):
""" raised for immediate program exits (no tracebacks/summaries)"""

def __init__(self, msg="unknown reason", returncode=None):
def __init__(
self, msg: str = "unknown reason", returncode: Optional[int] = None
) -> None:
self.msg = msg
self.returncode = returncode
super().__init__(msg)
Expand All @@ -59,7 +71,7 @@ def __init__(self, msg="unknown reason", returncode=None):
# exposed helper methods


def exit(msg, returncode=None):
def exit(msg: str, returncode: Optional[int] = None) -> "NoReturn":
"""
Exit testing process.

Expand All @@ -74,7 +86,7 @@ def exit(msg, returncode=None):
exit.Exception = Exit # type: ignore


def skip(msg="", *, allow_module_level=False):
def skip(msg: str = "", *, allow_module_level: bool = False) -> "NoReturn":
"""
Skip an executing test with the given message.

Expand All @@ -101,7 +113,7 @@ def skip(msg="", *, allow_module_level=False):
skip.Exception = Skipped # type: ignore


def fail(msg="", pytrace=True):
def fail(msg: str = "", pytrace: bool = True) -> "NoReturn":
"""
Explicitly fail an executing test with the given message.

Expand All @@ -121,7 +133,7 @@ class XFailed(Failed):
""" raised from an explicit call to pytest.xfail() """


def xfail(reason=""):
def xfail(reason: str = "") -> "NoReturn":
"""
Imperatively xfail an executing test or setup functions with the given reason.

Expand All @@ -139,7 +151,9 @@ def xfail(reason=""):
xfail.Exception = XFailed # type: ignore


def importorskip(modname, minversion=None, reason=None):
def importorskip(
modname: str, minversion: Optional[str] = None, reason: Optional[str] = None
) -> Any:
"""Imports and returns the requested module ``modname``, or skip the current test
if the module cannot be imported.

Expand Down
Loading