1
1
"""Per-test stdout/stderr capturing mechanism."""
2
- import collections
3
2
import contextlib
3
+ import functools
4
4
import io
5
5
import os
6
6
import sys
7
7
from io import UnsupportedOperation
8
8
from tempfile import TemporaryFile
9
+ from typing import Any
10
+ from typing import AnyStr
9
11
from typing import Generator
12
+ from typing import Generic
13
+ from typing import Iterator
10
14
from typing import Optional
11
15
from typing import TextIO
12
16
from typing import Tuple
@@ -488,10 +492,64 @@ def writeorg(self, data):
488
492
489
493
# MultiCapture
490
494
491
- CaptureResult = collections .namedtuple ("CaptureResult" , ["out" , "err" ])
492
495
496
+ # This class was a namedtuple, but due to mypy limitation[0] it could not be
497
+ # made generic, so was replaced by a regular class which tries to emulate the
498
+ # pertinent parts of a namedtuple. If the mypy limitation is ever lifted, can
499
+ # make it a namedtuple again.
500
+ # [0]: https://github.com/python/mypy/issues/685
501
+ @functools .total_ordering
502
+ class CaptureResult (Generic [AnyStr ]):
503
+ """The result of :method:`CaptureFixture.readouterr`."""
493
504
494
- class MultiCapture :
505
+ # Can't use slots in Python<3.5.3 due to https://bugs.python.org/issue31272
506
+ if sys .version_info >= (3 , 5 , 3 ):
507
+ __slots__ = ("out" , "err" )
508
+
509
+ def __init__ (self , out : AnyStr , err : AnyStr ) -> None :
510
+ self .out = out # type: AnyStr
511
+ self .err = err # type: AnyStr
512
+
513
+ def __len__ (self ) -> int :
514
+ return 2
515
+
516
+ def __iter__ (self ) -> Iterator [AnyStr ]:
517
+ return iter ((self .out , self .err ))
518
+
519
+ def __getitem__ (self , item : int ) -> AnyStr :
520
+ return tuple (self )[item ]
521
+
522
+ def _replace (
523
+ self , * , out : Optional [AnyStr ] = None , err : Optional [AnyStr ] = None
524
+ ) -> "CaptureResult[AnyStr]" :
525
+ return CaptureResult (
526
+ out = self .out if out is None else out , err = self .err if err is None else err
527
+ )
528
+
529
+ def count (self , value : AnyStr ) -> int :
530
+ return tuple (self ).count (value )
531
+
532
+ def index (self , value ) -> int :
533
+ return tuple (self ).index (value )
534
+
535
+ def __eq__ (self , other : object ) -> bool :
536
+ if not isinstance (other , (CaptureResult , tuple )):
537
+ return NotImplemented
538
+ return tuple (self ) == tuple (other )
539
+
540
+ def __hash__ (self ) -> int :
541
+ return hash (tuple (self ))
542
+
543
+ def __lt__ (self , other : object ) -> bool :
544
+ if not isinstance (other , (CaptureResult , tuple )):
545
+ return NotImplemented
546
+ return tuple (self ) < tuple (other )
547
+
548
+ def __repr__ (self ) -> str :
549
+ return "CaptureResult(out={!r}, err={!r})" .format (self .out , self .err )
550
+
551
+
552
+ class MultiCapture (Generic [AnyStr ]):
495
553
_state = None
496
554
_in_suspended = False
497
555
@@ -514,7 +572,7 @@ def start_capturing(self) -> None:
514
572
if self .err :
515
573
self .err .start ()
516
574
517
- def pop_outerr_to_orig (self ):
575
+ def pop_outerr_to_orig (self ) -> Tuple [ AnyStr , AnyStr ] :
518
576
"""Pop current snapshot out/err capture and flush to orig streams."""
519
577
out , err = self .readouterr ()
520
578
if out :
@@ -555,7 +613,7 @@ def stop_capturing(self) -> None:
555
613
if self .in_ :
556
614
self .in_ .done ()
557
615
558
- def readouterr (self ) -> CaptureResult :
616
+ def readouterr (self ) -> CaptureResult [ AnyStr ] :
559
617
if self .out :
560
618
out = self .out .snap ()
561
619
else :
@@ -567,7 +625,7 @@ def readouterr(self) -> CaptureResult:
567
625
return CaptureResult (out , err )
568
626
569
627
570
- def _get_multicapture (method : "_CaptureMethod" ) -> MultiCapture :
628
+ def _get_multicapture (method : "_CaptureMethod" ) -> MultiCapture [ str ] :
571
629
if method == "fd" :
572
630
return MultiCapture (in_ = FDCapture (0 ), out = FDCapture (1 ), err = FDCapture (2 ))
573
631
elif method == "sys" :
@@ -605,8 +663,8 @@ class CaptureManager:
605
663
606
664
def __init__ (self , method : "_CaptureMethod" ) -> None :
607
665
self ._method = method
608
- self ._global_capturing = None # type: Optional[MultiCapture]
609
- self ._capture_fixture = None # type: Optional[CaptureFixture]
666
+ self ._global_capturing = None # type: Optional[MultiCapture[str] ]
667
+ self ._capture_fixture = None # type: Optional[CaptureFixture[Any] ]
610
668
611
669
def __repr__ (self ) -> str :
612
670
return "<CaptureManager _method={!r} _global_capturing={!r} _capture_fixture={!r}>" .format (
@@ -655,13 +713,13 @@ def resume(self) -> None:
655
713
self .resume_global_capture ()
656
714
self .resume_fixture ()
657
715
658
- def read_global_capture (self ):
716
+ def read_global_capture (self ) -> CaptureResult [ str ] :
659
717
assert self ._global_capturing is not None
660
718
return self ._global_capturing .readouterr ()
661
719
662
720
# Fixture Control
663
721
664
- def set_fixture (self , capture_fixture : "CaptureFixture" ) -> None :
722
+ def set_fixture (self , capture_fixture : "CaptureFixture[Any] " ) -> None :
665
723
if self ._capture_fixture :
666
724
current_fixture = self ._capture_fixture .request .fixturename
667
725
requested_fixture = capture_fixture .request .fixturename
@@ -760,14 +818,14 @@ def pytest_internalerror(self) -> None:
760
818
self .stop_global_capturing ()
761
819
762
820
763
- class CaptureFixture :
821
+ class CaptureFixture ( Generic [ AnyStr ]) :
764
822
"""Object returned by the :py:func:`capsys`, :py:func:`capsysbinary`,
765
823
:py:func:`capfd` and :py:func:`capfdbinary` fixtures."""
766
824
767
825
def __init__ (self , captureclass , request : SubRequest ) -> None :
768
826
self .captureclass = captureclass
769
827
self .request = request
770
- self ._capture = None # type: Optional[MultiCapture]
828
+ self ._capture = None # type: Optional[MultiCapture[AnyStr] ]
771
829
self ._captured_out = self .captureclass .EMPTY_BUFFER
772
830
self ._captured_err = self .captureclass .EMPTY_BUFFER
773
831
@@ -786,7 +844,7 @@ def close(self) -> None:
786
844
self ._capture .stop_capturing ()
787
845
self ._capture = None
788
846
789
- def readouterr (self ):
847
+ def readouterr (self ) -> CaptureResult [ AnyStr ] :
790
848
"""Read and return the captured output so far, resetting the internal
791
849
buffer.
792
850
@@ -825,15 +883,15 @@ def disabled(self) -> Generator[None, None, None]:
825
883
826
884
827
885
@pytest .fixture
828
- def capsys (request : SubRequest ) -> Generator [CaptureFixture , None , None ]:
886
+ def capsys (request : SubRequest ) -> Generator [CaptureFixture [ str ] , None , None ]:
829
887
"""Enable text capturing of writes to ``sys.stdout`` and ``sys.stderr``.
830
888
831
889
The captured output is made available via ``capsys.readouterr()`` method
832
890
calls, which return a ``(out, err)`` namedtuple.
833
891
``out`` and ``err`` will be ``text`` objects.
834
892
"""
835
893
capman = request .config .pluginmanager .getplugin ("capturemanager" )
836
- capture_fixture = CaptureFixture (SysCapture , request )
894
+ capture_fixture = CaptureFixture [ str ] (SysCapture , request )
837
895
capman .set_fixture (capture_fixture )
838
896
capture_fixture ._start ()
839
897
yield capture_fixture
@@ -842,15 +900,15 @@ def capsys(request: SubRequest) -> Generator[CaptureFixture, None, None]:
842
900
843
901
844
902
@pytest .fixture
845
- def capsysbinary (request : SubRequest ) -> Generator [CaptureFixture , None , None ]:
903
+ def capsysbinary (request : SubRequest ) -> Generator [CaptureFixture [ bytes ] , None , None ]:
846
904
"""Enable bytes capturing of writes to ``sys.stdout`` and ``sys.stderr``.
847
905
848
906
The captured output is made available via ``capsysbinary.readouterr()``
849
907
method calls, which return a ``(out, err)`` namedtuple.
850
908
``out`` and ``err`` will be ``bytes`` objects.
851
909
"""
852
910
capman = request .config .pluginmanager .getplugin ("capturemanager" )
853
- capture_fixture = CaptureFixture (SysCaptureBinary , request )
911
+ capture_fixture = CaptureFixture [ bytes ] (SysCaptureBinary , request )
854
912
capman .set_fixture (capture_fixture )
855
913
capture_fixture ._start ()
856
914
yield capture_fixture
@@ -859,15 +917,15 @@ def capsysbinary(request: SubRequest) -> Generator[CaptureFixture, None, None]:
859
917
860
918
861
919
@pytest .fixture
862
- def capfd (request : SubRequest ) -> Generator [CaptureFixture , None , None ]:
920
+ def capfd (request : SubRequest ) -> Generator [CaptureFixture [ str ] , None , None ]:
863
921
"""Enable text capturing of writes to file descriptors ``1`` and ``2``.
864
922
865
923
The captured output is made available via ``capfd.readouterr()`` method
866
924
calls, which return a ``(out, err)`` namedtuple.
867
925
``out`` and ``err`` will be ``text`` objects.
868
926
"""
869
927
capman = request .config .pluginmanager .getplugin ("capturemanager" )
870
- capture_fixture = CaptureFixture (FDCapture , request )
928
+ capture_fixture = CaptureFixture [ str ] (FDCapture , request )
871
929
capman .set_fixture (capture_fixture )
872
930
capture_fixture ._start ()
873
931
yield capture_fixture
@@ -876,15 +934,15 @@ def capfd(request: SubRequest) -> Generator[CaptureFixture, None, None]:
876
934
877
935
878
936
@pytest .fixture
879
- def capfdbinary (request : SubRequest ) -> Generator [CaptureFixture , None , None ]:
937
+ def capfdbinary (request : SubRequest ) -> Generator [CaptureFixture [ bytes ] , None , None ]:
880
938
"""Enable bytes capturing of writes to file descriptors ``1`` and ``2``.
881
939
882
940
The captured output is made available via ``capfd.readouterr()`` method
883
941
calls, which return a ``(out, err)`` namedtuple.
884
942
``out`` and ``err`` will be ``byte`` objects.
885
943
"""
886
944
capman = request .config .pluginmanager .getplugin ("capturemanager" )
887
- capture_fixture = CaptureFixture (FDCaptureBinary , request )
945
+ capture_fixture = CaptureFixture [ bytes ] (FDCaptureBinary , request )
888
946
capman .set_fixture (capture_fixture )
889
947
capture_fixture ._start ()
890
948
yield capture_fixture
0 commit comments