updates
This commit is contained in:
@@ -1,36 +1,30 @@
|
||||
# mypy: allow-untyped-defs
|
||||
"""Per-test stdout/stderr capturing mechanism."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import abc
|
||||
import collections
|
||||
from collections.abc import Generator
|
||||
from collections.abc import Iterable
|
||||
from collections.abc import Iterator
|
||||
import contextlib
|
||||
import io
|
||||
from io import UnsupportedOperation
|
||||
import os
|
||||
import sys
|
||||
from io import UnsupportedOperation
|
||||
from tempfile import TemporaryFile
|
||||
from types import TracebackType
|
||||
from typing import Any
|
||||
from typing import AnyStr
|
||||
from typing import BinaryIO
|
||||
from typing import cast
|
||||
from typing import Final
|
||||
from typing import final
|
||||
from typing import Generator
|
||||
from typing import Generic
|
||||
from typing import Literal
|
||||
from typing import Iterable
|
||||
from typing import Iterator
|
||||
from typing import List
|
||||
from typing import NamedTuple
|
||||
from typing import Optional
|
||||
from typing import TextIO
|
||||
from typing import Tuple
|
||||
from typing import Type
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import Union
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import Self
|
||||
|
||||
from _pytest.compat import final
|
||||
from _pytest.config import Config
|
||||
from _pytest.config import hookimpl
|
||||
from _pytest.config.argparsing import Parser
|
||||
@@ -40,15 +34,17 @@ from _pytest.fixtures import SubRequest
|
||||
from _pytest.nodes import Collector
|
||||
from _pytest.nodes import File
|
||||
from _pytest.nodes import Item
|
||||
from _pytest.reports import CollectReport
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import Final
|
||||
from typing_extensions import Literal
|
||||
|
||||
_CaptureMethod = Literal["fd", "sys", "no", "tee-sys"]
|
||||
_CaptureMethod = Literal["fd", "sys", "no", "tee-sys"]
|
||||
|
||||
|
||||
def pytest_addoption(parser: Parser) -> None:
|
||||
group = parser.getgroup("general")
|
||||
group.addoption(
|
||||
group._addoption(
|
||||
"--capture",
|
||||
action="store",
|
||||
default="fd",
|
||||
@@ -56,7 +52,7 @@ def pytest_addoption(parser: Parser) -> None:
|
||||
choices=["fd", "sys", "no", "tee-sys"],
|
||||
help="Per-test capturing method: one of fd|sys|no|tee-sys",
|
||||
)
|
||||
group._addoption( # private to use reserved lower-case short option
|
||||
group._addoption(
|
||||
"-s",
|
||||
action="store_const",
|
||||
const="no",
|
||||
@@ -80,23 +76,6 @@ def _colorama_workaround() -> None:
|
||||
pass
|
||||
|
||||
|
||||
def _readline_workaround() -> None:
|
||||
"""Ensure readline is imported early so it attaches to the correct stdio handles.
|
||||
|
||||
This isn't a problem with the default GNU readline implementation, but in
|
||||
some configurations, Python uses libedit instead (on macOS, and for prebuilt
|
||||
binaries such as used by uv).
|
||||
|
||||
In theory this is only needed if readline.backend == "libedit", but the
|
||||
workaround consists of importing readline here, so we already worked around
|
||||
the issue by the time we could check if we need to.
|
||||
"""
|
||||
try:
|
||||
import readline # noqa: F401
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
def _windowsconsoleio_workaround(stream: TextIO) -> None:
|
||||
"""Workaround for Windows Unicode console handling.
|
||||
|
||||
@@ -125,16 +104,17 @@ def _windowsconsoleio_workaround(stream: TextIO) -> None:
|
||||
return
|
||||
|
||||
# Bail out if ``stream`` doesn't seem like a proper ``io`` stream (#2666).
|
||||
if not hasattr(stream, "buffer"): # type: ignore[unreachable,unused-ignore]
|
||||
if not hasattr(stream, "buffer"): # type: ignore[unreachable]
|
||||
return
|
||||
|
||||
raw_stdout = stream.buffer.raw if hasattr(stream.buffer, "raw") else stream.buffer
|
||||
buffered = hasattr(stream.buffer, "raw")
|
||||
raw_stdout = stream.buffer.raw if buffered else stream.buffer # type: ignore[attr-defined]
|
||||
|
||||
if not isinstance(raw_stdout, io._WindowsConsoleIO): # type: ignore[attr-defined,unused-ignore]
|
||||
if not isinstance(raw_stdout, io._WindowsConsoleIO): # type: ignore[attr-defined]
|
||||
return
|
||||
|
||||
def _reopen_stdio(f, mode):
|
||||
if not hasattr(stream.buffer, "raw") and mode[0] == "w":
|
||||
if not buffered and mode[0] == "w":
|
||||
buffering = 0
|
||||
else:
|
||||
buffering = -1
|
||||
@@ -152,13 +132,12 @@ def _windowsconsoleio_workaround(stream: TextIO) -> None:
|
||||
sys.stderr = _reopen_stdio(sys.stderr, "wb")
|
||||
|
||||
|
||||
@hookimpl(wrapper=True)
|
||||
def pytest_load_initial_conftests(early_config: Config) -> Generator[None]:
|
||||
@hookimpl(hookwrapper=True)
|
||||
def pytest_load_initial_conftests(early_config: Config):
|
||||
ns = early_config.known_args_namespace
|
||||
if ns.capture == "fd":
|
||||
_windowsconsoleio_workaround(sys.stdout)
|
||||
_colorama_workaround()
|
||||
_readline_workaround()
|
||||
pluginmanager = early_config.pluginmanager
|
||||
capman = CaptureManager(ns.capture)
|
||||
pluginmanager.register(capman, "capturemanager")
|
||||
@@ -168,16 +147,12 @@ def pytest_load_initial_conftests(early_config: Config) -> Generator[None]:
|
||||
|
||||
# Finally trigger conftest loading but while capturing (issue #93).
|
||||
capman.start_global_capturing()
|
||||
try:
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
capman.suspend_global_capture()
|
||||
except BaseException:
|
||||
outcome = yield
|
||||
capman.suspend_global_capture()
|
||||
if outcome.excinfo is not None:
|
||||
out, err = capman.read_global_capture()
|
||||
sys.stdout.write(out)
|
||||
sys.stderr.write(err)
|
||||
raise
|
||||
|
||||
|
||||
# IO Helpers.
|
||||
@@ -196,8 +171,7 @@ class EncodedFile(io.TextIOWrapper):
|
||||
def mode(self) -> str:
|
||||
# TextIOWrapper doesn't expose a mode, but at least some of our
|
||||
# tests check it.
|
||||
assert hasattr(self.buffer, "mode")
|
||||
return cast(str, self.buffer.mode.replace("b", ""))
|
||||
return self.buffer.mode.replace("b", "")
|
||||
|
||||
|
||||
class CaptureIO(io.TextIOWrapper):
|
||||
@@ -222,7 +196,6 @@ class TeeCaptureIO(CaptureIO):
|
||||
class DontReadFromInput(TextIO):
|
||||
@property
|
||||
def encoding(self) -> str:
|
||||
assert sys.__stdin__ is not None
|
||||
return sys.__stdin__.encoding
|
||||
|
||||
def read(self, size: int = -1) -> str:
|
||||
@@ -235,7 +208,7 @@ class DontReadFromInput(TextIO):
|
||||
def __next__(self) -> str:
|
||||
return self.readline()
|
||||
|
||||
def readlines(self, hint: int | None = -1) -> list[str]:
|
||||
def readlines(self, hint: Optional[int] = -1) -> List[str]:
|
||||
raise OSError(
|
||||
"pytest: reading from stdin while output is captured! Consider using `-s`."
|
||||
)
|
||||
@@ -267,7 +240,7 @@ class DontReadFromInput(TextIO):
|
||||
def tell(self) -> int:
|
||||
raise UnsupportedOperation("redirected stdin is pseudofile, has no tell()")
|
||||
|
||||
def truncate(self, size: int | None = None) -> int:
|
||||
def truncate(self, size: Optional[int] = None) -> int:
|
||||
raise UnsupportedOperation("cannot truncate stdin")
|
||||
|
||||
def write(self, data: str) -> int:
|
||||
@@ -279,14 +252,14 @@ class DontReadFromInput(TextIO):
|
||||
def writable(self) -> bool:
|
||||
return False
|
||||
|
||||
def __enter__(self) -> Self:
|
||||
def __enter__(self) -> "DontReadFromInput":
|
||||
return self
|
||||
|
||||
def __exit__(
|
||||
self,
|
||||
type: type[BaseException] | None,
|
||||
value: BaseException | None,
|
||||
traceback: TracebackType | None,
|
||||
type: Optional[Type[BaseException]],
|
||||
value: Optional[BaseException],
|
||||
traceback: Optional[TracebackType],
|
||||
) -> None:
|
||||
pass
|
||||
|
||||
@@ -361,7 +334,7 @@ class NoCapture(CaptureBase[str]):
|
||||
|
||||
class SysCaptureBase(CaptureBase[AnyStr]):
|
||||
def __init__(
|
||||
self, fd: int, tmpfile: TextIO | None = None, *, tee: bool = False
|
||||
self, fd: int, tmpfile: Optional[TextIO] = None, *, tee: bool = False
|
||||
) -> None:
|
||||
name = patchsysdict[fd]
|
||||
self._old: TextIO = getattr(sys, name)
|
||||
@@ -378,7 +351,7 @@ class SysCaptureBase(CaptureBase[AnyStr]):
|
||||
return "<{} {} _old={} _state={!r} tmpfile={!r}>".format(
|
||||
class_name,
|
||||
self.name,
|
||||
(hasattr(self, "_old") and repr(self._old)) or "<UNSET>",
|
||||
hasattr(self, "_old") and repr(self._old) or "<UNSET>",
|
||||
self._state,
|
||||
self.tmpfile,
|
||||
)
|
||||
@@ -387,16 +360,16 @@ class SysCaptureBase(CaptureBase[AnyStr]):
|
||||
return "<{} {} _old={} _state={!r} tmpfile={!r}>".format(
|
||||
self.__class__.__name__,
|
||||
self.name,
|
||||
(hasattr(self, "_old") and repr(self._old)) or "<UNSET>",
|
||||
hasattr(self, "_old") and repr(self._old) or "<UNSET>",
|
||||
self._state,
|
||||
self.tmpfile,
|
||||
)
|
||||
|
||||
def _assert_state(self, op: str, states: tuple[str, ...]) -> None:
|
||||
assert self._state in states, (
|
||||
"cannot {} in state {!r}: expected one of {}".format(
|
||||
op, self._state, ", ".join(states)
|
||||
)
|
||||
def _assert_state(self, op: str, states: Tuple[str, ...]) -> None:
|
||||
assert (
|
||||
self._state in states
|
||||
), "cannot {} in state {!r}: expected one of {}".format(
|
||||
op, self._state, ", ".join(states)
|
||||
)
|
||||
|
||||
def start(self) -> None:
|
||||
@@ -479,7 +452,7 @@ class FDCaptureBase(CaptureBase[AnyStr]):
|
||||
# Further complications are the need to support suspend() and the
|
||||
# possibility of FD reuse (e.g. the tmpfile getting the very same
|
||||
# target FD). The following approach is robust, I believe.
|
||||
self.targetfd_invalid: int | None = os.open(os.devnull, os.O_RDWR)
|
||||
self.targetfd_invalid: Optional[int] = os.open(os.devnull, os.O_RDWR)
|
||||
os.dup2(self.targetfd_invalid, targetfd)
|
||||
else:
|
||||
self.targetfd_invalid = None
|
||||
@@ -504,16 +477,19 @@ class FDCaptureBase(CaptureBase[AnyStr]):
|
||||
self._state = "initialized"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"<{self.__class__.__name__} {self.targetfd} oldfd={self.targetfd_save} "
|
||||
f"_state={self._state!r} tmpfile={self.tmpfile!r}>"
|
||||
return "<{} {} oldfd={} _state={!r} tmpfile={!r}>".format(
|
||||
self.__class__.__name__,
|
||||
self.targetfd,
|
||||
self.targetfd_save,
|
||||
self._state,
|
||||
self.tmpfile,
|
||||
)
|
||||
|
||||
def _assert_state(self, op: str, states: tuple[str, ...]) -> None:
|
||||
assert self._state in states, (
|
||||
"cannot {} in state {!r}: expected one of {}".format(
|
||||
op, self._state, ", ".join(states)
|
||||
)
|
||||
def _assert_state(self, op: str, states: Tuple[str, ...]) -> None:
|
||||
assert (
|
||||
self._state in states
|
||||
), "cannot {} in state {!r}: expected one of {}".format(
|
||||
op, self._state, ", ".join(states)
|
||||
)
|
||||
|
||||
def start(self) -> None:
|
||||
@@ -570,7 +546,7 @@ class FDCaptureBinary(FDCaptureBase[bytes]):
|
||||
res = self.tmpfile.buffer.read()
|
||||
self.tmpfile.seek(0)
|
||||
self.tmpfile.truncate()
|
||||
return res # type: ignore[return-value]
|
||||
return res
|
||||
|
||||
def writeorg(self, data: bytes) -> None:
|
||||
"""Write to original file descriptor."""
|
||||
@@ -609,7 +585,7 @@ if sys.version_info >= (3, 11) or TYPE_CHECKING:
|
||||
|
||||
@final
|
||||
class CaptureResult(NamedTuple, Generic[AnyStr]):
|
||||
"""The result of :method:`caplog.readouterr() <pytest.CaptureFixture.readouterr>`."""
|
||||
"""The result of :method:`CaptureFixture.readouterr`."""
|
||||
|
||||
out: AnyStr
|
||||
err: AnyStr
|
||||
@@ -617,10 +593,9 @@ if sys.version_info >= (3, 11) or TYPE_CHECKING:
|
||||
else:
|
||||
|
||||
class CaptureResult(
|
||||
collections.namedtuple("CaptureResult", ["out", "err"]), # noqa: PYI024
|
||||
Generic[AnyStr],
|
||||
collections.namedtuple("CaptureResult", ["out", "err"]), Generic[AnyStr]
|
||||
):
|
||||
"""The result of :method:`caplog.readouterr() <pytest.CaptureFixture.readouterr>`."""
|
||||
"""The result of :method:`CaptureFixture.readouterr`."""
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
@@ -631,18 +606,21 @@ class MultiCapture(Generic[AnyStr]):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
in_: CaptureBase[AnyStr] | None,
|
||||
out: CaptureBase[AnyStr] | None,
|
||||
err: CaptureBase[AnyStr] | None,
|
||||
in_: Optional[CaptureBase[AnyStr]],
|
||||
out: Optional[CaptureBase[AnyStr]],
|
||||
err: Optional[CaptureBase[AnyStr]],
|
||||
) -> None:
|
||||
self.in_: CaptureBase[AnyStr] | None = in_
|
||||
self.out: CaptureBase[AnyStr] | None = out
|
||||
self.err: CaptureBase[AnyStr] | None = err
|
||||
self.in_: Optional[CaptureBase[AnyStr]] = in_
|
||||
self.out: Optional[CaptureBase[AnyStr]] = out
|
||||
self.err: Optional[CaptureBase[AnyStr]] = err
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"<MultiCapture out={self.out!r} err={self.err!r} in_={self.in_!r} "
|
||||
f"_state={self._state!r} _in_suspended={self._in_suspended!r}>"
|
||||
return "<MultiCapture out={!r} err={!r} in_={!r} _state={!r} _in_suspended={!r}>".format(
|
||||
self.out,
|
||||
self.err,
|
||||
self.in_,
|
||||
self._state,
|
||||
self._in_suspended,
|
||||
)
|
||||
|
||||
def start_capturing(self) -> None:
|
||||
@@ -654,7 +632,7 @@ class MultiCapture(Generic[AnyStr]):
|
||||
if self.err:
|
||||
self.err.start()
|
||||
|
||||
def pop_outerr_to_orig(self) -> tuple[AnyStr, AnyStr]:
|
||||
def pop_outerr_to_orig(self) -> Tuple[AnyStr, AnyStr]:
|
||||
"""Pop current snapshot out/err capture and flush to orig streams."""
|
||||
out, err = self.readouterr()
|
||||
if out:
|
||||
@@ -709,7 +687,7 @@ class MultiCapture(Generic[AnyStr]):
|
||||
return CaptureResult(out, err) # type: ignore[arg-type]
|
||||
|
||||
|
||||
def _get_multicapture(method: _CaptureMethod) -> MultiCapture[str]:
|
||||
def _get_multicapture(method: "_CaptureMethod") -> MultiCapture[str]:
|
||||
if method == "fd":
|
||||
return MultiCapture(in_=FDCapture(0), out=FDCapture(1), err=FDCapture(2))
|
||||
elif method == "sys":
|
||||
@@ -745,22 +723,21 @@ class CaptureManager:
|
||||
needed to ensure the fixtures take precedence over the global capture.
|
||||
"""
|
||||
|
||||
def __init__(self, method: _CaptureMethod) -> None:
|
||||
def __init__(self, method: "_CaptureMethod") -> None:
|
||||
self._method: Final = method
|
||||
self._global_capturing: MultiCapture[str] | None = None
|
||||
self._capture_fixture: CaptureFixture[Any] | None = None
|
||||
self._global_capturing: Optional[MultiCapture[str]] = None
|
||||
self._capture_fixture: Optional[CaptureFixture[Any]] = None
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"<CaptureManager _method={self._method!r} _global_capturing={self._global_capturing!r} "
|
||||
f"_capture_fixture={self._capture_fixture!r}>"
|
||||
return "<CaptureManager _method={!r} _global_capturing={!r} _capture_fixture={!r}>".format(
|
||||
self._method, self._global_capturing, self._capture_fixture
|
||||
)
|
||||
|
||||
def is_capturing(self) -> str | bool:
|
||||
def is_capturing(self) -> Union[str, bool]:
|
||||
if self.is_globally_capturing():
|
||||
return "global"
|
||||
if self._capture_fixture:
|
||||
return f"fixture {self._capture_fixture.request.fixturename}"
|
||||
return "fixture %s" % self._capture_fixture.request.fixturename
|
||||
return False
|
||||
|
||||
# Global capturing control
|
||||
@@ -804,12 +781,14 @@ class CaptureManager:
|
||||
|
||||
# Fixture Control
|
||||
|
||||
def set_fixture(self, capture_fixture: CaptureFixture[Any]) -> None:
|
||||
def set_fixture(self, capture_fixture: "CaptureFixture[Any]") -> None:
|
||||
if self._capture_fixture:
|
||||
current_fixture = self._capture_fixture.request.fixturename
|
||||
requested_fixture = capture_fixture.request.fixturename
|
||||
capture_fixture.request.raiseerror(
|
||||
f"cannot use {requested_fixture} and {current_fixture} at the same time"
|
||||
"cannot use {} and {} at the same time".format(
|
||||
requested_fixture, current_fixture
|
||||
)
|
||||
)
|
||||
self._capture_fixture = capture_fixture
|
||||
|
||||
@@ -838,7 +817,7 @@ class CaptureManager:
|
||||
# Helper context managers
|
||||
|
||||
@contextlib.contextmanager
|
||||
def global_and_fixture_disabled(self) -> Generator[None]:
|
||||
def global_and_fixture_disabled(self) -> Generator[None, None, None]:
|
||||
"""Context manager to temporarily disable global and current fixture capturing."""
|
||||
do_fixture = self._capture_fixture and self._capture_fixture._is_started()
|
||||
if do_fixture:
|
||||
@@ -855,7 +834,7 @@ class CaptureManager:
|
||||
self.resume_fixture()
|
||||
|
||||
@contextlib.contextmanager
|
||||
def item_capture(self, when: str, item: Item) -> Generator[None]:
|
||||
def item_capture(self, when: str, item: Item) -> Generator[None, None, None]:
|
||||
self.resume_global_capture()
|
||||
self.activate_fixture()
|
||||
try:
|
||||
@@ -864,45 +843,41 @@ class CaptureManager:
|
||||
self.deactivate_fixture()
|
||||
self.suspend_global_capture(in_=False)
|
||||
|
||||
out, err = self.read_global_capture()
|
||||
item.add_report_section(when, "stdout", out)
|
||||
item.add_report_section(when, "stderr", err)
|
||||
out, err = self.read_global_capture()
|
||||
item.add_report_section(when, "stdout", out)
|
||||
item.add_report_section(when, "stderr", err)
|
||||
|
||||
# Hooks
|
||||
|
||||
@hookimpl(wrapper=True)
|
||||
def pytest_make_collect_report(
|
||||
self, collector: Collector
|
||||
) -> Generator[None, CollectReport, CollectReport]:
|
||||
@hookimpl(hookwrapper=True)
|
||||
def pytest_make_collect_report(self, collector: Collector):
|
||||
if isinstance(collector, File):
|
||||
self.resume_global_capture()
|
||||
try:
|
||||
rep = yield
|
||||
finally:
|
||||
self.suspend_global_capture()
|
||||
outcome = yield
|
||||
self.suspend_global_capture()
|
||||
out, err = self.read_global_capture()
|
||||
rep = outcome.get_result()
|
||||
if out:
|
||||
rep.sections.append(("Captured stdout", out))
|
||||
if err:
|
||||
rep.sections.append(("Captured stderr", err))
|
||||
else:
|
||||
rep = yield
|
||||
return rep
|
||||
yield
|
||||
|
||||
@hookimpl(wrapper=True)
|
||||
def pytest_runtest_setup(self, item: Item) -> Generator[None]:
|
||||
@hookimpl(hookwrapper=True)
|
||||
def pytest_runtest_setup(self, item: Item) -> Generator[None, None, None]:
|
||||
with self.item_capture("setup", item):
|
||||
return (yield)
|
||||
yield
|
||||
|
||||
@hookimpl(wrapper=True)
|
||||
def pytest_runtest_call(self, item: Item) -> Generator[None]:
|
||||
@hookimpl(hookwrapper=True)
|
||||
def pytest_runtest_call(self, item: Item) -> Generator[None, None, None]:
|
||||
with self.item_capture("call", item):
|
||||
return (yield)
|
||||
yield
|
||||
|
||||
@hookimpl(wrapper=True)
|
||||
def pytest_runtest_teardown(self, item: Item) -> Generator[None]:
|
||||
@hookimpl(hookwrapper=True)
|
||||
def pytest_runtest_teardown(self, item: Item) -> Generator[None, None, None]:
|
||||
with self.item_capture("teardown", item):
|
||||
return (yield)
|
||||
yield
|
||||
|
||||
@hookimpl(tryfirst=True)
|
||||
def pytest_keyboard_interrupt(self) -> None:
|
||||
@@ -919,17 +894,15 @@ class CaptureFixture(Generic[AnyStr]):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
captureclass: type[CaptureBase[AnyStr]],
|
||||
captureclass: Type[CaptureBase[AnyStr]],
|
||||
request: SubRequest,
|
||||
*,
|
||||
config: dict[str, Any] | None = None,
|
||||
_ispytest: bool = False,
|
||||
) -> None:
|
||||
check_ispytest(_ispytest)
|
||||
self.captureclass: type[CaptureBase[AnyStr]] = captureclass
|
||||
self.captureclass: Type[CaptureBase[AnyStr]] = captureclass
|
||||
self.request = request
|
||||
self._config = config if config else {}
|
||||
self._capture: MultiCapture[AnyStr] | None = None
|
||||
self._capture: Optional[MultiCapture[AnyStr]] = None
|
||||
self._captured_out: AnyStr = self.captureclass.EMPTY_BUFFER
|
||||
self._captured_err: AnyStr = self.captureclass.EMPTY_BUFFER
|
||||
|
||||
@@ -937,8 +910,8 @@ class CaptureFixture(Generic[AnyStr]):
|
||||
if self._capture is None:
|
||||
self._capture = MultiCapture(
|
||||
in_=None,
|
||||
out=self.captureclass(1, **self._config),
|
||||
err=self.captureclass(2, **self._config),
|
||||
out=self.captureclass(1),
|
||||
err=self.captureclass(2),
|
||||
)
|
||||
self._capture.start_capturing()
|
||||
|
||||
@@ -984,7 +957,7 @@ class CaptureFixture(Generic[AnyStr]):
|
||||
return False
|
||||
|
||||
@contextlib.contextmanager
|
||||
def disabled(self) -> Generator[None]:
|
||||
def disabled(self) -> Generator[None, None, None]:
|
||||
"""Temporarily disable capturing while inside the ``with`` block."""
|
||||
capmanager: CaptureManager = self.request.config.pluginmanager.getplugin(
|
||||
"capturemanager"
|
||||
@@ -997,7 +970,7 @@ class CaptureFixture(Generic[AnyStr]):
|
||||
|
||||
|
||||
@fixture
|
||||
def capsys(request: SubRequest) -> Generator[CaptureFixture[str]]:
|
||||
def capsys(request: SubRequest) -> Generator[CaptureFixture[str], None, None]:
|
||||
r"""Enable text capturing of writes to ``sys.stdout`` and ``sys.stderr``.
|
||||
|
||||
The captured output is made available via ``capsys.readouterr()`` method
|
||||
@@ -1025,42 +998,7 @@ def capsys(request: SubRequest) -> Generator[CaptureFixture[str]]:
|
||||
|
||||
|
||||
@fixture
|
||||
def capteesys(request: SubRequest) -> Generator[CaptureFixture[str]]:
|
||||
r"""Enable simultaneous text capturing and pass-through of writes
|
||||
to ``sys.stdout`` and ``sys.stderr`` as defined by ``--capture=``.
|
||||
|
||||
|
||||
The captured output is made available via ``capteesys.readouterr()`` method
|
||||
calls, which return a ``(out, err)`` namedtuple.
|
||||
``out`` and ``err`` will be ``text`` objects.
|
||||
|
||||
The output is also passed-through, allowing it to be "live-printed",
|
||||
reported, or both as defined by ``--capture=``.
|
||||
|
||||
Returns an instance of :class:`CaptureFixture[str] <pytest.CaptureFixture>`.
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def test_output(capteesys):
|
||||
print("hello")
|
||||
captured = capteesys.readouterr()
|
||||
assert captured.out == "hello\n"
|
||||
"""
|
||||
capman: CaptureManager = request.config.pluginmanager.getplugin("capturemanager")
|
||||
capture_fixture = CaptureFixture(
|
||||
SysCapture, request, config=dict(tee=True), _ispytest=True
|
||||
)
|
||||
capman.set_fixture(capture_fixture)
|
||||
capture_fixture._start()
|
||||
yield capture_fixture
|
||||
capture_fixture.close()
|
||||
capman.unset_fixture()
|
||||
|
||||
|
||||
@fixture
|
||||
def capsysbinary(request: SubRequest) -> Generator[CaptureFixture[bytes]]:
|
||||
def capsysbinary(request: SubRequest) -> Generator[CaptureFixture[bytes], None, None]:
|
||||
r"""Enable bytes capturing of writes to ``sys.stdout`` and ``sys.stderr``.
|
||||
|
||||
The captured output is made available via ``capsysbinary.readouterr()``
|
||||
@@ -1088,7 +1026,7 @@ def capsysbinary(request: SubRequest) -> Generator[CaptureFixture[bytes]]:
|
||||
|
||||
|
||||
@fixture
|
||||
def capfd(request: SubRequest) -> Generator[CaptureFixture[str]]:
|
||||
def capfd(request: SubRequest) -> Generator[CaptureFixture[str], None, None]:
|
||||
r"""Enable text capturing of writes to file descriptors ``1`` and ``2``.
|
||||
|
||||
The captured output is made available via ``capfd.readouterr()`` method
|
||||
@@ -1116,7 +1054,7 @@ def capfd(request: SubRequest) -> Generator[CaptureFixture[str]]:
|
||||
|
||||
|
||||
@fixture
|
||||
def capfdbinary(request: SubRequest) -> Generator[CaptureFixture[bytes]]:
|
||||
def capfdbinary(request: SubRequest) -> Generator[CaptureFixture[bytes], None, None]:
|
||||
r"""Enable bytes capturing of writes to file descriptors ``1`` and ``2``.
|
||||
|
||||
The captured output is made available via ``capfd.readouterr()`` method
|
||||
|
||||
Reference in New Issue
Block a user