55from inspect import CO_VARARGS
66from inspect import CO_VARKEYWORDS
77from 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
815from weakref import ref
916
1017import attr
1522from _pytest ._io .saferepr import safeformat
1623from _pytest ._io .saferepr import saferepr
1724
25+ if False : # TYPE_CHECKING
26+ from typing import Type
27+
1828
1929class 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
0 commit comments