Skip to content

Commit faf222f

Browse files
authored
Merge pull request #5593 from bluetech/type-annotations-1
Type-annotate pytest.{exit,skip,fail,xfail,importorskip,warns,raises}
2 parents 9258fd1 + 11f1f79 commit faf222f

File tree

8 files changed

+304
-87
lines changed

8 files changed

+304
-87
lines changed

src/_pytest/_code/code.py

Lines changed: 85 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@
55
from inspect import CO_VARARGS
66
from inspect import CO_VARKEYWORDS
77
from traceback import format_exception_only
8+
from types import TracebackType
9+
from typing import Generic
10+
from typing import Optional
11+
from typing import Pattern
12+
from typing import Tuple
13+
from typing import TypeVar
14+
from typing import Union
815
from weakref import ref
916

1017
import attr
@@ -15,6 +22,9 @@
1522
from _pytest._io.saferepr import safeformat
1623
from _pytest._io.saferepr import saferepr
1724

25+
if False: # TYPE_CHECKING
26+
from typing import Type
27+
1828

1929
class Code:
2030
""" wrapper around Python code objects """
@@ -371,21 +381,28 @@ def recursionindex(self):
371381
)
372382

373383

384+
_E = TypeVar("_E", bound=BaseException)
385+
386+
374387
@attr.s(repr=False)
375-
class ExceptionInfo:
388+
class ExceptionInfo(Generic[_E]):
376389
""" wraps sys.exc_info() objects and offers
377390
help for navigating the traceback.
378391
"""
379392

380393
_assert_start_repr = "AssertionError('assert "
381394

382-
_excinfo = attr.ib()
383-
_striptext = attr.ib(default="")
384-
_traceback = attr.ib(default=None)
395+
_excinfo = attr.ib(type=Optional[Tuple["Type[_E]", "_E", TracebackType]])
396+
_striptext = attr.ib(type=str, default="")
397+
_traceback = attr.ib(type=Optional[Traceback], default=None)
385398

386399
@classmethod
387-
def from_current(cls, exprinfo=None):
388-
"""returns an ExceptionInfo matching the current traceback
400+
def from_exc_info(
401+
cls,
402+
exc_info: Tuple["Type[_E]", "_E", TracebackType],
403+
exprinfo: Optional[str] = None,
404+
) -> "ExceptionInfo[_E]":
405+
"""returns an ExceptionInfo for an existing exc_info tuple.
389406
390407
.. warning::
391408
@@ -396,61 +413,98 @@ def from_current(cls, exprinfo=None):
396413
strip ``AssertionError`` from the output, defaults
397414
to the exception message/``__str__()``
398415
"""
399-
tup = sys.exc_info()
400-
assert tup[0] is not None, "no current exception"
401416
_striptext = ""
402-
if exprinfo is None and isinstance(tup[1], AssertionError):
403-
exprinfo = getattr(tup[1], "msg", None)
417+
if exprinfo is None and isinstance(exc_info[1], AssertionError):
418+
exprinfo = getattr(exc_info[1], "msg", None)
404419
if exprinfo is None:
405-
exprinfo = saferepr(tup[1])
420+
exprinfo = saferepr(exc_info[1])
406421
if exprinfo and exprinfo.startswith(cls._assert_start_repr):
407422
_striptext = "AssertionError: "
408423

409-
return cls(tup, _striptext)
424+
return cls(exc_info, _striptext)
410425

411426
@classmethod
412-
def for_later(cls):
427+
def from_current(
428+
cls, exprinfo: Optional[str] = None
429+
) -> "ExceptionInfo[BaseException]":
430+
"""returns an ExceptionInfo matching the current traceback
431+
432+
.. warning::
433+
434+
Experimental API
435+
436+
437+
:param exprinfo: a text string helping to determine if we should
438+
strip ``AssertionError`` from the output, defaults
439+
to the exception message/``__str__()``
440+
"""
441+
tup = sys.exc_info()
442+
assert tup[0] is not None, "no current exception"
443+
assert tup[1] is not None, "no current exception"
444+
assert tup[2] is not None, "no current exception"
445+
exc_info = (tup[0], tup[1], tup[2])
446+
return cls.from_exc_info(exc_info)
447+
448+
@classmethod
449+
def for_later(cls) -> "ExceptionInfo[_E]":
413450
"""return an unfilled ExceptionInfo
414451
"""
415452
return cls(None)
416453

454+
def fill_unfilled(self, exc_info: Tuple["Type[_E]", _E, TracebackType]) -> None:
455+
"""fill an unfilled ExceptionInfo created with for_later()"""
456+
assert self._excinfo is None, "ExceptionInfo was already filled"
457+
self._excinfo = exc_info
458+
417459
@property
418-
def type(self):
460+
def type(self) -> "Type[_E]":
419461
"""the exception class"""
462+
assert (
463+
self._excinfo is not None
464+
), ".type can only be used after the context manager exits"
420465
return self._excinfo[0]
421466

422467
@property
423-
def value(self):
468+
def value(self) -> _E:
424469
"""the exception value"""
470+
assert (
471+
self._excinfo is not None
472+
), ".value can only be used after the context manager exits"
425473
return self._excinfo[1]
426474

427475
@property
428-
def tb(self):
476+
def tb(self) -> TracebackType:
429477
"""the exception raw traceback"""
478+
assert (
479+
self._excinfo is not None
480+
), ".tb can only be used after the context manager exits"
430481
return self._excinfo[2]
431482

432483
@property
433-
def typename(self):
484+
def typename(self) -> str:
434485
"""the type name of the exception"""
486+
assert (
487+
self._excinfo is not None
488+
), ".typename can only be used after the context manager exits"
435489
return self.type.__name__
436490

437491
@property
438-
def traceback(self):
492+
def traceback(self) -> Traceback:
439493
"""the traceback"""
440494
if self._traceback is None:
441495
self._traceback = Traceback(self.tb, excinfo=ref(self))
442496
return self._traceback
443497

444498
@traceback.setter
445-
def traceback(self, value):
499+
def traceback(self, value: Traceback) -> None:
446500
self._traceback = value
447501

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

453-
def exconly(self, tryshort=False):
507+
def exconly(self, tryshort: bool = False) -> str:
454508
""" return the exception as a string
455509
456510
when 'tryshort' resolves to True, and the exception is a
@@ -466,25 +520,25 @@ def exconly(self, tryshort=False):
466520
text = text[len(self._striptext) :]
467521
return text
468522

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

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

479533
def getrepr(
480534
self,
481-
showlocals=False,
482-
style="long",
483-
abspath=False,
484-
tbfilter=True,
485-
funcargs=False,
486-
truncate_locals=True,
487-
chain=True,
535+
showlocals: bool = False,
536+
style: str = "long",
537+
abspath: bool = False,
538+
tbfilter: bool = True,
539+
funcargs: bool = False,
540+
truncate_locals: bool = True,
541+
chain: bool = True,
488542
):
489543
"""
490544
Return str()able representation of this exception info.
@@ -535,7 +589,7 @@ def getrepr(
535589
)
536590
return fmt.repr_excinfo(self)
537591

538-
def match(self, regexp):
592+
def match(self, regexp: Union[str, Pattern]) -> bool:
539593
"""
540594
Check whether the regular expression 'regexp' is found in the string
541595
representation of the exception using ``re.search``. If it matches

src/_pytest/outcomes.py

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,26 @@
33
as well as functions creating them
44
"""
55
import sys
6+
from typing import Any
7+
from typing import Optional
68

79
from packaging.version import Version
810

11+
if False: # TYPE_CHECKING
12+
from typing import NoReturn
13+
914

1015
class OutcomeException(BaseException):
1116
""" OutcomeException and its subclass instances indicate and
1217
contain info about test and collection outcomes.
1318
"""
1419

15-
def __init__(self, msg=None, pytrace=True):
20+
def __init__(self, msg: Optional[str] = None, pytrace: bool = True) -> None:
1621
BaseException.__init__(self, msg)
1722
self.msg = msg
1823
self.pytrace = pytrace
1924

20-
def __repr__(self):
25+
def __repr__(self) -> str:
2126
if self.msg:
2227
val = self.msg
2328
if isinstance(val, bytes):
@@ -36,7 +41,12 @@ class Skipped(OutcomeException):
3641
# in order to have Skipped exception printing shorter/nicer
3742
__module__ = "builtins"
3843

39-
def __init__(self, msg=None, pytrace=True, allow_module_level=False):
44+
def __init__(
45+
self,
46+
msg: Optional[str] = None,
47+
pytrace: bool = True,
48+
allow_module_level: bool = False,
49+
) -> None:
4050
OutcomeException.__init__(self, msg=msg, pytrace=pytrace)
4151
self.allow_module_level = allow_module_level
4252

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

53-
def __init__(self, msg="unknown reason", returncode=None):
63+
def __init__(
64+
self, msg: str = "unknown reason", returncode: Optional[int] = None
65+
) -> None:
5466
self.msg = msg
5567
self.returncode = returncode
5668
super().__init__(msg)
@@ -59,7 +71,7 @@ def __init__(self, msg="unknown reason", returncode=None):
5971
# exposed helper methods
6072

6173

62-
def exit(msg, returncode=None):
74+
def exit(msg: str, returncode: Optional[int] = None) -> "NoReturn":
6375
"""
6476
Exit testing process.
6577
@@ -74,7 +86,7 @@ def exit(msg, returncode=None):
7486
exit.Exception = Exit # type: ignore
7587

7688

77-
def skip(msg="", *, allow_module_level=False):
89+
def skip(msg: str = "", *, allow_module_level: bool = False) -> "NoReturn":
7890
"""
7991
Skip an executing test with the given message.
8092
@@ -101,7 +113,7 @@ def skip(msg="", *, allow_module_level=False):
101113
skip.Exception = Skipped # type: ignore
102114

103115

104-
def fail(msg="", pytrace=True):
116+
def fail(msg: str = "", pytrace: bool = True) -> "NoReturn":
105117
"""
106118
Explicitly fail an executing test with the given message.
107119
@@ -121,7 +133,7 @@ class XFailed(Failed):
121133
""" raised from an explicit call to pytest.xfail() """
122134

123135

124-
def xfail(reason=""):
136+
def xfail(reason: str = "") -> "NoReturn":
125137
"""
126138
Imperatively xfail an executing test or setup functions with the given reason.
127139
@@ -139,7 +151,9 @@ def xfail(reason=""):
139151
xfail.Exception = XFailed # type: ignore
140152

141153

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

0 commit comments

Comments
 (0)