Coverage for /opt/homebrew/lib/python3.11/site-packages/_pytest/capture.py: 62%

567 statements  

« prev     ^ index     » next       coverage.py v7.2.3, created at 2023-05-04 13:14 +0700

1"""Per-test stdout/stderr capturing mechanism.""" 

2import contextlib 

3import functools 

4import io 

5import os 

6import sys 

7from io import UnsupportedOperation 

8from tempfile import TemporaryFile 

9from typing import Any 

10from typing import AnyStr 

11from typing import Generator 

12from typing import Generic 

13from typing import Iterator 

14from typing import Optional 

15from typing import TextIO 

16from typing import Tuple 

17from typing import TYPE_CHECKING 

18from typing import Union 

19 

20from _pytest.compat import final 

21from _pytest.config import Config 

22from _pytest.config import hookimpl 

23from _pytest.config.argparsing import Parser 

24from _pytest.deprecated import check_ispytest 

25from _pytest.fixtures import fixture 

26from _pytest.fixtures import SubRequest 

27from _pytest.nodes import Collector 

28from _pytest.nodes import File 

29from _pytest.nodes import Item 

30 

31if TYPE_CHECKING: 

32 from typing_extensions import Literal 

33 

34 _CaptureMethod = Literal["fd", "sys", "no", "tee-sys"] 

35 

36 

37def pytest_addoption(parser: Parser) -> None: 

38 group = parser.getgroup("general") 

39 group._addoption( 

40 "--capture", 

41 action="store", 

42 default="fd", 

43 metavar="method", 

44 choices=["fd", "sys", "no", "tee-sys"], 

45 help="Per-test capturing method: one of fd|sys|no|tee-sys", 

46 ) 

47 group._addoption( 

48 "-s", 

49 action="store_const", 

50 const="no", 

51 dest="capture", 

52 help="Shortcut for --capture=no", 

53 ) 

54 

55 

56def _colorama_workaround() -> None: 

57 """Ensure colorama is imported so that it attaches to the correct stdio 

58 handles on Windows. 

59 

60 colorama uses the terminal on import time. So if something does the 

61 first import of colorama while I/O capture is active, colorama will 

62 fail in various ways. 

63 """ 

64 if sys.platform.startswith("win32"): 

65 try: 

66 import colorama # noqa: F401 

67 except ImportError: 

68 pass 

69 

70 

71def _windowsconsoleio_workaround(stream: TextIO) -> None: 

72 """Workaround for Windows Unicode console handling. 

73 

74 Python 3.6 implemented Unicode console handling for Windows. This works 

75 by reading/writing to the raw console handle using 

76 ``{Read,Write}ConsoleW``. 

77 

78 The problem is that we are going to ``dup2`` over the stdio file 

79 descriptors when doing ``FDCapture`` and this will ``CloseHandle`` the 

80 handles used by Python to write to the console. Though there is still some 

81 weirdness and the console handle seems to only be closed randomly and not 

82 on the first call to ``CloseHandle``, or maybe it gets reopened with the 

83 same handle value when we suspend capturing. 

84 

85 The workaround in this case will reopen stdio with a different fd which 

86 also means a different handle by replicating the logic in 

87 "Py_lifecycle.c:initstdio/create_stdio". 

88 

89 :param stream: 

90 In practice ``sys.stdout`` or ``sys.stderr``, but given 

91 here as parameter for unittesting purposes. 

92 

93 See https://github.com/pytest-dev/py/issues/103. 

94 """ 

95 if not sys.platform.startswith("win32") or hasattr(sys, "pypy_version_info"): 

96 return 

97 

98 # Bail out if ``stream`` doesn't seem like a proper ``io`` stream (#2666). 

99 if not hasattr(stream, "buffer"): # type: ignore[unreachable] 

100 return 

101 

102 buffered = hasattr(stream.buffer, "raw") 

103 raw_stdout = stream.buffer.raw if buffered else stream.buffer # type: ignore[attr-defined] 

104 

105 if not isinstance(raw_stdout, io._WindowsConsoleIO): # type: ignore[attr-defined] 

106 return 

107 

108 def _reopen_stdio(f, mode): 

109 if not buffered and mode[0] == "w": 

110 buffering = 0 

111 else: 

112 buffering = -1 

113 

114 return io.TextIOWrapper( 

115 open(os.dup(f.fileno()), mode, buffering), 

116 f.encoding, 

117 f.errors, 

118 f.newlines, 

119 f.line_buffering, 

120 ) 

121 

122 sys.stdin = _reopen_stdio(sys.stdin, "rb") 

123 sys.stdout = _reopen_stdio(sys.stdout, "wb") 

124 sys.stderr = _reopen_stdio(sys.stderr, "wb") 

125 

126 

127@hookimpl(hookwrapper=True) 

128def pytest_load_initial_conftests(early_config: Config): 

129 ns = early_config.known_args_namespace 

130 if ns.capture == "fd": 

131 _windowsconsoleio_workaround(sys.stdout) 

132 _colorama_workaround() 

133 pluginmanager = early_config.pluginmanager 

134 capman = CaptureManager(ns.capture) 

135 pluginmanager.register(capman, "capturemanager") 

136 

137 # Make sure that capturemanager is properly reset at final shutdown. 

138 early_config.add_cleanup(capman.stop_global_capturing) 

139 

140 # Finally trigger conftest loading but while capturing (issue #93). 

141 capman.start_global_capturing() 

142 outcome = yield 

143 capman.suspend_global_capture() 

144 if outcome.excinfo is not None: 

145 out, err = capman.read_global_capture() 

146 sys.stdout.write(out) 

147 sys.stderr.write(err) 

148 

149 

150# IO Helpers. 

151 

152 

153class EncodedFile(io.TextIOWrapper): 

154 __slots__ = () 

155 

156 @property 

157 def name(self) -> str: 

158 # Ensure that file.name is a string. Workaround for a Python bug 

159 # fixed in >=3.7.4: https://bugs.python.org/issue36015 

160 return repr(self.buffer) 

161 

162 @property 

163 def mode(self) -> str: 

164 # TextIOWrapper doesn't expose a mode, but at least some of our 

165 # tests check it. 

166 return self.buffer.mode.replace("b", "") 

167 

168 

169class CaptureIO(io.TextIOWrapper): 

170 def __init__(self) -> None: 

171 super().__init__(io.BytesIO(), encoding="UTF-8", newline="", write_through=True) 

172 

173 def getvalue(self) -> str: 

174 assert isinstance(self.buffer, io.BytesIO) 

175 return self.buffer.getvalue().decode("UTF-8") 

176 

177 

178class TeeCaptureIO(CaptureIO): 

179 def __init__(self, other: TextIO) -> None: 

180 self._other = other 

181 super().__init__() 

182 

183 def write(self, s: str) -> int: 

184 super().write(s) 

185 return self._other.write(s) 

186 

187 

188class DontReadFromInput: 

189 encoding = None 

190 

191 def read(self, *args): 

192 raise OSError( 

193 "pytest: reading from stdin while output is captured! Consider using `-s`." 

194 ) 

195 

196 readline = read 

197 readlines = read 

198 __next__ = read 

199 

200 def __iter__(self): 

201 return self 

202 

203 def fileno(self) -> int: 

204 raise UnsupportedOperation("redirected stdin is pseudofile, has no fileno()") 

205 

206 def flush(self) -> None: 

207 raise UnsupportedOperation("redirected stdin is pseudofile, has no flush()") 

208 

209 def isatty(self) -> bool: 

210 return False 

211 

212 def close(self) -> None: 

213 pass 

214 

215 def readable(self) -> bool: 

216 return False 

217 

218 def seek(self, offset: int) -> int: 

219 raise UnsupportedOperation("redirected stdin is pseudofile, has no seek(int)") 

220 

221 def seekable(self) -> bool: 

222 return False 

223 

224 def tell(self) -> int: 

225 raise UnsupportedOperation("redirected stdin is pseudofile, has no tell()") 

226 

227 def truncate(self, size: int) -> None: 

228 raise UnsupportedOperation("cannont truncate stdin") 

229 

230 def write(self, *args) -> None: 

231 raise UnsupportedOperation("cannot write to stdin") 

232 

233 def writelines(self, *args) -> None: 

234 raise UnsupportedOperation("Cannot write to stdin") 

235 

236 def writable(self) -> bool: 

237 return False 

238 

239 @property 

240 def buffer(self): 

241 return self 

242 

243 

244# Capture classes. 

245 

246 

247patchsysdict = {0: "stdin", 1: "stdout", 2: "stderr"} 

248 

249 

250class NoCapture: 

251 EMPTY_BUFFER = None 

252 __init__ = start = done = suspend = resume = lambda *args: None 

253 

254 

255class SysCaptureBinary: 

256 

257 EMPTY_BUFFER = b"" 

258 

259 def __init__(self, fd: int, tmpfile=None, *, tee: bool = False) -> None: 

260 name = patchsysdict[fd] 

261 self._old = getattr(sys, name) 

262 self.name = name 

263 if tmpfile is None: 

264 if name == "stdin": 

265 tmpfile = DontReadFromInput() 

266 else: 

267 tmpfile = CaptureIO() if not tee else TeeCaptureIO(self._old) 

268 self.tmpfile = tmpfile 

269 self._state = "initialized" 

270 

271 def repr(self, class_name: str) -> str: 

272 return "<{} {} _old={} _state={!r} tmpfile={!r}>".format( 

273 class_name, 

274 self.name, 

275 hasattr(self, "_old") and repr(self._old) or "<UNSET>", 

276 self._state, 

277 self.tmpfile, 

278 ) 

279 

280 def __repr__(self) -> str: 

281 return "<{} {} _old={} _state={!r} tmpfile={!r}>".format( 

282 self.__class__.__name__, 

283 self.name, 

284 hasattr(self, "_old") and repr(self._old) or "<UNSET>", 

285 self._state, 

286 self.tmpfile, 

287 ) 

288 

289 def _assert_state(self, op: str, states: Tuple[str, ...]) -> None: 

290 assert ( 

291 self._state in states 

292 ), "cannot {} in state {!r}: expected one of {}".format( 

293 op, self._state, ", ".join(states) 

294 ) 

295 

296 def start(self) -> None: 

297 self._assert_state("start", ("initialized",)) 

298 setattr(sys, self.name, self.tmpfile) 

299 self._state = "started" 

300 

301 def snap(self): 

302 self._assert_state("snap", ("started", "suspended")) 

303 self.tmpfile.seek(0) 

304 res = self.tmpfile.buffer.read() 

305 self.tmpfile.seek(0) 

306 self.tmpfile.truncate() 

307 return res 

308 

309 def done(self) -> None: 

310 self._assert_state("done", ("initialized", "started", "suspended", "done")) 

311 if self._state == "done": 

312 return 

313 setattr(sys, self.name, self._old) 

314 del self._old 

315 self.tmpfile.close() 

316 self._state = "done" 

317 

318 def suspend(self) -> None: 

319 self._assert_state("suspend", ("started", "suspended")) 

320 setattr(sys, self.name, self._old) 

321 self._state = "suspended" 

322 

323 def resume(self) -> None: 

324 self._assert_state("resume", ("started", "suspended")) 

325 if self._state == "started": 

326 return 

327 setattr(sys, self.name, self.tmpfile) 

328 self._state = "started" 

329 

330 def writeorg(self, data) -> None: 

331 self._assert_state("writeorg", ("started", "suspended")) 

332 self._old.flush() 

333 self._old.buffer.write(data) 

334 self._old.buffer.flush() 

335 

336 

337class SysCapture(SysCaptureBinary): 

338 EMPTY_BUFFER = "" # type: ignore[assignment] 

339 

340 def snap(self): 

341 res = self.tmpfile.getvalue() 

342 self.tmpfile.seek(0) 

343 self.tmpfile.truncate() 

344 return res 

345 

346 def writeorg(self, data): 

347 self._assert_state("writeorg", ("started", "suspended")) 

348 self._old.write(data) 

349 self._old.flush() 

350 

351 

352class FDCaptureBinary: 

353 """Capture IO to/from a given OS-level file descriptor. 

354 

355 snap() produces `bytes`. 

356 """ 

357 

358 EMPTY_BUFFER = b"" 

359 

360 def __init__(self, targetfd: int) -> None: 

361 self.targetfd = targetfd 

362 

363 try: 

364 os.fstat(targetfd) 

365 except OSError: 

366 # FD capturing is conceptually simple -- create a temporary file, 

367 # redirect the FD to it, redirect back when done. But when the 

368 # target FD is invalid it throws a wrench into this lovely scheme. 

369 # 

370 # Tests themselves shouldn't care if the FD is valid, FD capturing 

371 # should work regardless of external circumstances. So falling back 

372 # to just sys capturing is not a good option. 

373 # 

374 # Further complications are the need to support suspend() and the 

375 # possibility of FD reuse (e.g. the tmpfile getting the very same 

376 # target FD). The following approach is robust, I believe. 

377 self.targetfd_invalid: Optional[int] = os.open(os.devnull, os.O_RDWR) 

378 os.dup2(self.targetfd_invalid, targetfd) 

379 else: 

380 self.targetfd_invalid = None 

381 self.targetfd_save = os.dup(targetfd) 

382 

383 if targetfd == 0: 

384 self.tmpfile = open(os.devnull, encoding="utf-8") 

385 self.syscapture = SysCapture(targetfd) 

386 else: 

387 self.tmpfile = EncodedFile( 

388 TemporaryFile(buffering=0), 

389 encoding="utf-8", 

390 errors="replace", 

391 newline="", 

392 write_through=True, 

393 ) 

394 if targetfd in patchsysdict: 

395 self.syscapture = SysCapture(targetfd, self.tmpfile) 

396 else: 

397 self.syscapture = NoCapture() 

398 

399 self._state = "initialized" 

400 

401 def __repr__(self) -> str: 

402 return "<{} {} oldfd={} _state={!r} tmpfile={!r}>".format( 

403 self.__class__.__name__, 

404 self.targetfd, 

405 self.targetfd_save, 

406 self._state, 

407 self.tmpfile, 

408 ) 

409 

410 def _assert_state(self, op: str, states: Tuple[str, ...]) -> None: 

411 assert ( 

412 self._state in states 

413 ), "cannot {} in state {!r}: expected one of {}".format( 

414 op, self._state, ", ".join(states) 

415 ) 

416 

417 def start(self) -> None: 

418 """Start capturing on targetfd using memorized tmpfile.""" 

419 self._assert_state("start", ("initialized",)) 

420 os.dup2(self.tmpfile.fileno(), self.targetfd) 

421 self.syscapture.start() 

422 self._state = "started" 

423 

424 def snap(self): 

425 self._assert_state("snap", ("started", "suspended")) 

426 self.tmpfile.seek(0) 

427 res = self.tmpfile.buffer.read() 

428 self.tmpfile.seek(0) 

429 self.tmpfile.truncate() 

430 return res 

431 

432 def done(self) -> None: 

433 """Stop capturing, restore streams, return original capture file, 

434 seeked to position zero.""" 

435 self._assert_state("done", ("initialized", "started", "suspended", "done")) 

436 if self._state == "done": 

437 return 

438 os.dup2(self.targetfd_save, self.targetfd) 

439 os.close(self.targetfd_save) 

440 if self.targetfd_invalid is not None: 

441 if self.targetfd_invalid != self.targetfd: 

442 os.close(self.targetfd) 

443 os.close(self.targetfd_invalid) 

444 self.syscapture.done() 

445 self.tmpfile.close() 

446 self._state = "done" 

447 

448 def suspend(self) -> None: 

449 self._assert_state("suspend", ("started", "suspended")) 

450 if self._state == "suspended": 

451 return 

452 self.syscapture.suspend() 

453 os.dup2(self.targetfd_save, self.targetfd) 

454 self._state = "suspended" 

455 

456 def resume(self) -> None: 

457 self._assert_state("resume", ("started", "suspended")) 

458 if self._state == "started": 

459 return 

460 self.syscapture.resume() 

461 os.dup2(self.tmpfile.fileno(), self.targetfd) 

462 self._state = "started" 

463 

464 def writeorg(self, data): 

465 """Write to original file descriptor.""" 

466 self._assert_state("writeorg", ("started", "suspended")) 

467 os.write(self.targetfd_save, data) 

468 

469 

470class FDCapture(FDCaptureBinary): 

471 """Capture IO to/from a given OS-level file descriptor. 

472 

473 snap() produces text. 

474 """ 

475 

476 # Ignore type because it doesn't match the type in the superclass (bytes). 

477 EMPTY_BUFFER = "" # type: ignore 

478 

479 def snap(self): 

480 self._assert_state("snap", ("started", "suspended")) 

481 self.tmpfile.seek(0) 

482 res = self.tmpfile.read() 

483 self.tmpfile.seek(0) 

484 self.tmpfile.truncate() 

485 return res 

486 

487 def writeorg(self, data): 

488 """Write to original file descriptor.""" 

489 super().writeorg(data.encode("utf-8")) # XXX use encoding of original stream 

490 

491 

492# MultiCapture 

493 

494 

495# This class was a namedtuple, but due to mypy limitation[0] it could not be 

496# made generic, so was replaced by a regular class which tries to emulate the 

497# pertinent parts of a namedtuple. If the mypy limitation is ever lifted, can 

498# make it a namedtuple again. 

499# [0]: https://github.com/python/mypy/issues/685 

500@final 

501@functools.total_ordering 

502class CaptureResult(Generic[AnyStr]): 

503 """The result of :method:`CaptureFixture.readouterr`.""" 

504 

505 __slots__ = ("out", "err") 

506 

507 def __init__(self, out: AnyStr, err: AnyStr) -> None: 

508 self.out: AnyStr = out 

509 self.err: AnyStr = err 

510 

511 def __len__(self) -> int: 

512 return 2 

513 

514 def __iter__(self) -> Iterator[AnyStr]: 

515 return iter((self.out, self.err)) 

516 

517 def __getitem__(self, item: int) -> AnyStr: 

518 return tuple(self)[item] 

519 

520 def _replace( 

521 self, *, out: Optional[AnyStr] = None, err: Optional[AnyStr] = None 

522 ) -> "CaptureResult[AnyStr]": 

523 return CaptureResult( 

524 out=self.out if out is None else out, err=self.err if err is None else err 

525 ) 

526 

527 def count(self, value: AnyStr) -> int: 

528 return tuple(self).count(value) 

529 

530 def index(self, value) -> int: 

531 return tuple(self).index(value) 

532 

533 def __eq__(self, other: object) -> bool: 

534 if not isinstance(other, (CaptureResult, tuple)): 

535 return NotImplemented 

536 return tuple(self) == tuple(other) 

537 

538 def __hash__(self) -> int: 

539 return hash(tuple(self)) 

540 

541 def __lt__(self, other: object) -> bool: 

542 if not isinstance(other, (CaptureResult, tuple)): 

543 return NotImplemented 

544 return tuple(self) < tuple(other) 

545 

546 def __repr__(self) -> str: 

547 return f"CaptureResult(out={self.out!r}, err={self.err!r})" 

548 

549 

550class MultiCapture(Generic[AnyStr]): 

551 _state = None 

552 _in_suspended = False 

553 

554 def __init__(self, in_, out, err) -> None: 

555 self.in_ = in_ 

556 self.out = out 

557 self.err = err 

558 

559 def __repr__(self) -> str: 

560 return "<MultiCapture out={!r} err={!r} in_={!r} _state={!r} _in_suspended={!r}>".format( 

561 self.out, 

562 self.err, 

563 self.in_, 

564 self._state, 

565 self._in_suspended, 

566 ) 

567 

568 def start_capturing(self) -> None: 

569 self._state = "started" 

570 if self.in_: 

571 self.in_.start() 

572 if self.out: 

573 self.out.start() 

574 if self.err: 

575 self.err.start() 

576 

577 def pop_outerr_to_orig(self) -> Tuple[AnyStr, AnyStr]: 

578 """Pop current snapshot out/err capture and flush to orig streams.""" 

579 out, err = self.readouterr() 

580 if out: 

581 self.out.writeorg(out) 

582 if err: 

583 self.err.writeorg(err) 

584 return out, err 

585 

586 def suspend_capturing(self, in_: bool = False) -> None: 

587 self._state = "suspended" 

588 if self.out: 

589 self.out.suspend() 

590 if self.err: 

591 self.err.suspend() 

592 if in_ and self.in_: 

593 self.in_.suspend() 

594 self._in_suspended = True 

595 

596 def resume_capturing(self) -> None: 

597 self._state = "started" 

598 if self.out: 

599 self.out.resume() 

600 if self.err: 

601 self.err.resume() 

602 if self._in_suspended: 

603 self.in_.resume() 

604 self._in_suspended = False 

605 

606 def stop_capturing(self) -> None: 

607 """Stop capturing and reset capturing streams.""" 

608 if self._state == "stopped": 

609 raise ValueError("was already stopped") 

610 self._state = "stopped" 

611 if self.out: 

612 self.out.done() 

613 if self.err: 

614 self.err.done() 

615 if self.in_: 

616 self.in_.done() 

617 

618 def is_started(self) -> bool: 

619 """Whether actively capturing -- not suspended or stopped.""" 

620 return self._state == "started" 

621 

622 def readouterr(self) -> CaptureResult[AnyStr]: 

623 out = self.out.snap() if self.out else "" 

624 err = self.err.snap() if self.err else "" 

625 return CaptureResult(out, err) 

626 

627 

628def _get_multicapture(method: "_CaptureMethod") -> MultiCapture[str]: 

629 if method == "fd": 

630 return MultiCapture(in_=FDCapture(0), out=FDCapture(1), err=FDCapture(2)) 

631 elif method == "sys": 

632 return MultiCapture(in_=SysCapture(0), out=SysCapture(1), err=SysCapture(2)) 

633 elif method == "no": 

634 return MultiCapture(in_=None, out=None, err=None) 

635 elif method == "tee-sys": 

636 return MultiCapture( 

637 in_=None, out=SysCapture(1, tee=True), err=SysCapture(2, tee=True) 

638 ) 

639 raise ValueError(f"unknown capturing method: {method!r}") 

640 

641 

642# CaptureManager and CaptureFixture 

643 

644 

645class CaptureManager: 

646 """The capture plugin. 

647 

648 Manages that the appropriate capture method is enabled/disabled during 

649 collection and each test phase (setup, call, teardown). After each of 

650 those points, the captured output is obtained and attached to the 

651 collection/runtest report. 

652 

653 There are two levels of capture: 

654 

655 * global: enabled by default and can be suppressed by the ``-s`` 

656 option. This is always enabled/disabled during collection and each test 

657 phase. 

658 

659 * fixture: when a test function or one of its fixture depend on the 

660 ``capsys`` or ``capfd`` fixtures. In this case special handling is 

661 needed to ensure the fixtures take precedence over the global capture. 

662 """ 

663 

664 def __init__(self, method: "_CaptureMethod") -> None: 

665 self._method = method 

666 self._global_capturing: Optional[MultiCapture[str]] = None 

667 self._capture_fixture: Optional[CaptureFixture[Any]] = None 

668 

669 def __repr__(self) -> str: 

670 return "<CaptureManager _method={!r} _global_capturing={!r} _capture_fixture={!r}>".format( 

671 self._method, self._global_capturing, self._capture_fixture 

672 ) 

673 

674 def is_capturing(self) -> Union[str, bool]: 

675 if self.is_globally_capturing(): 

676 return "global" 

677 if self._capture_fixture: 

678 return "fixture %s" % self._capture_fixture.request.fixturename 

679 return False 

680 

681 # Global capturing control 

682 

683 def is_globally_capturing(self) -> bool: 

684 return self._method != "no" 

685 

686 def start_global_capturing(self) -> None: 

687 assert self._global_capturing is None 

688 self._global_capturing = _get_multicapture(self._method) 

689 self._global_capturing.start_capturing() 

690 

691 def stop_global_capturing(self) -> None: 

692 if self._global_capturing is not None: 

693 self._global_capturing.pop_outerr_to_orig() 

694 self._global_capturing.stop_capturing() 

695 self._global_capturing = None 

696 

697 def resume_global_capture(self) -> None: 

698 # During teardown of the python process, and on rare occasions, capture 

699 # attributes can be `None` while trying to resume global capture. 

700 if self._global_capturing is not None: 

701 self._global_capturing.resume_capturing() 

702 

703 def suspend_global_capture(self, in_: bool = False) -> None: 

704 if self._global_capturing is not None: 

705 self._global_capturing.suspend_capturing(in_=in_) 

706 

707 def suspend(self, in_: bool = False) -> None: 

708 # Need to undo local capsys-et-al if it exists before disabling global capture. 

709 self.suspend_fixture() 

710 self.suspend_global_capture(in_) 

711 

712 def resume(self) -> None: 

713 self.resume_global_capture() 

714 self.resume_fixture() 

715 

716 def read_global_capture(self) -> CaptureResult[str]: 

717 assert self._global_capturing is not None 

718 return self._global_capturing.readouterr() 

719 

720 # Fixture Control 

721 

722 def set_fixture(self, capture_fixture: "CaptureFixture[Any]") -> None: 

723 if self._capture_fixture: 

724 current_fixture = self._capture_fixture.request.fixturename 

725 requested_fixture = capture_fixture.request.fixturename 

726 capture_fixture.request.raiseerror( 

727 "cannot use {} and {} at the same time".format( 

728 requested_fixture, current_fixture 

729 ) 

730 ) 

731 self._capture_fixture = capture_fixture 

732 

733 def unset_fixture(self) -> None: 

734 self._capture_fixture = None 

735 

736 def activate_fixture(self) -> None: 

737 """If the current item is using ``capsys`` or ``capfd``, activate 

738 them so they take precedence over the global capture.""" 

739 if self._capture_fixture: 

740 self._capture_fixture._start() 

741 

742 def deactivate_fixture(self) -> None: 

743 """Deactivate the ``capsys`` or ``capfd`` fixture of this item, if any.""" 

744 if self._capture_fixture: 

745 self._capture_fixture.close() 

746 

747 def suspend_fixture(self) -> None: 

748 if self._capture_fixture: 

749 self._capture_fixture._suspend() 

750 

751 def resume_fixture(self) -> None: 

752 if self._capture_fixture: 

753 self._capture_fixture._resume() 

754 

755 # Helper context managers 

756 

757 @contextlib.contextmanager 

758 def global_and_fixture_disabled(self) -> Generator[None, None, None]: 

759 """Context manager to temporarily disable global and current fixture capturing.""" 

760 do_fixture = self._capture_fixture and self._capture_fixture._is_started() 

761 if do_fixture: 

762 self.suspend_fixture() 

763 do_global = self._global_capturing and self._global_capturing.is_started() 

764 if do_global: 

765 self.suspend_global_capture() 

766 try: 

767 yield 

768 finally: 

769 if do_global: 

770 self.resume_global_capture() 

771 if do_fixture: 

772 self.resume_fixture() 

773 

774 @contextlib.contextmanager 

775 def item_capture(self, when: str, item: Item) -> Generator[None, None, None]: 

776 self.resume_global_capture() 

777 self.activate_fixture() 

778 try: 

779 yield 

780 finally: 

781 self.deactivate_fixture() 

782 self.suspend_global_capture(in_=False) 

783 

784 out, err = self.read_global_capture() 

785 item.add_report_section(when, "stdout", out) 

786 item.add_report_section(when, "stderr", err) 

787 

788 # Hooks 

789 

790 @hookimpl(hookwrapper=True) 

791 def pytest_make_collect_report(self, collector: Collector): 

792 if isinstance(collector, File): 

793 self.resume_global_capture() 

794 outcome = yield 

795 self.suspend_global_capture() 

796 out, err = self.read_global_capture() 

797 rep = outcome.get_result() 

798 if out: 

799 rep.sections.append(("Captured stdout", out)) 

800 if err: 

801 rep.sections.append(("Captured stderr", err)) 

802 else: 

803 yield 

804 

805 @hookimpl(hookwrapper=True) 

806 def pytest_runtest_setup(self, item: Item) -> Generator[None, None, None]: 

807 with self.item_capture("setup", item): 

808 yield 

809 

810 @hookimpl(hookwrapper=True) 

811 def pytest_runtest_call(self, item: Item) -> Generator[None, None, None]: 

812 with self.item_capture("call", item): 

813 yield 

814 

815 @hookimpl(hookwrapper=True) 

816 def pytest_runtest_teardown(self, item: Item) -> Generator[None, None, None]: 

817 with self.item_capture("teardown", item): 

818 yield 

819 

820 @hookimpl(tryfirst=True) 

821 def pytest_keyboard_interrupt(self) -> None: 

822 self.stop_global_capturing() 

823 

824 @hookimpl(tryfirst=True) 

825 def pytest_internalerror(self) -> None: 

826 self.stop_global_capturing() 

827 

828 

829class CaptureFixture(Generic[AnyStr]): 

830 """Object returned by the :fixture:`capsys`, :fixture:`capsysbinary`, 

831 :fixture:`capfd` and :fixture:`capfdbinary` fixtures.""" 

832 

833 def __init__( 

834 self, captureclass, request: SubRequest, *, _ispytest: bool = False 

835 ) -> None: 

836 check_ispytest(_ispytest) 

837 self.captureclass = captureclass 

838 self.request = request 

839 self._capture: Optional[MultiCapture[AnyStr]] = None 

840 self._captured_out = self.captureclass.EMPTY_BUFFER 

841 self._captured_err = self.captureclass.EMPTY_BUFFER 

842 

843 def _start(self) -> None: 

844 if self._capture is None: 

845 self._capture = MultiCapture( 

846 in_=None, 

847 out=self.captureclass(1), 

848 err=self.captureclass(2), 

849 ) 

850 self._capture.start_capturing() 

851 

852 def close(self) -> None: 

853 if self._capture is not None: 

854 out, err = self._capture.pop_outerr_to_orig() 

855 self._captured_out += out 

856 self._captured_err += err 

857 self._capture.stop_capturing() 

858 self._capture = None 

859 

860 def readouterr(self) -> CaptureResult[AnyStr]: 

861 """Read and return the captured output so far, resetting the internal 

862 buffer. 

863 

864 :returns: 

865 The captured content as a namedtuple with ``out`` and ``err`` 

866 string attributes. 

867 """ 

868 captured_out, captured_err = self._captured_out, self._captured_err 

869 if self._capture is not None: 

870 out, err = self._capture.readouterr() 

871 captured_out += out 

872 captured_err += err 

873 self._captured_out = self.captureclass.EMPTY_BUFFER 

874 self._captured_err = self.captureclass.EMPTY_BUFFER 

875 return CaptureResult(captured_out, captured_err) 

876 

877 def _suspend(self) -> None: 

878 """Suspend this fixture's own capturing temporarily.""" 

879 if self._capture is not None: 

880 self._capture.suspend_capturing() 

881 

882 def _resume(self) -> None: 

883 """Resume this fixture's own capturing temporarily.""" 

884 if self._capture is not None: 

885 self._capture.resume_capturing() 

886 

887 def _is_started(self) -> bool: 

888 """Whether actively capturing -- not disabled or closed.""" 

889 if self._capture is not None: 

890 return self._capture.is_started() 

891 return False 

892 

893 @contextlib.contextmanager 

894 def disabled(self) -> Generator[None, None, None]: 

895 """Temporarily disable capturing while inside the ``with`` block.""" 

896 capmanager = self.request.config.pluginmanager.getplugin("capturemanager") 

897 with capmanager.global_and_fixture_disabled(): 

898 yield 

899 

900 

901# The fixtures. 

902 

903 

904@fixture 

905def capsys(request: SubRequest) -> Generator[CaptureFixture[str], None, None]: 

906 r"""Enable text capturing of writes to ``sys.stdout`` and ``sys.stderr``. 

907 

908 The captured output is made available via ``capsys.readouterr()`` method 

909 calls, which return a ``(out, err)`` namedtuple. 

910 ``out`` and ``err`` will be ``text`` objects. 

911 

912 Returns an instance of :class:`CaptureFixture[str] <pytest.CaptureFixture>`. 

913 

914 Example: 

915 

916 .. code-block:: python 

917 

918 def test_output(capsys): 

919 print("hello") 

920 captured = capsys.readouterr() 

921 assert captured.out == "hello\n" 

922 """ 

923 capman = request.config.pluginmanager.getplugin("capturemanager") 

924 capture_fixture = CaptureFixture[str](SysCapture, request, _ispytest=True) 

925 capman.set_fixture(capture_fixture) 

926 capture_fixture._start() 

927 yield capture_fixture 

928 capture_fixture.close() 

929 capman.unset_fixture() 

930 

931 

932@fixture 

933def capsysbinary(request: SubRequest) -> Generator[CaptureFixture[bytes], None, None]: 

934 r"""Enable bytes capturing of writes to ``sys.stdout`` and ``sys.stderr``. 

935 

936 The captured output is made available via ``capsysbinary.readouterr()`` 

937 method calls, which return a ``(out, err)`` namedtuple. 

938 ``out`` and ``err`` will be ``bytes`` objects. 

939 

940 Returns an instance of :class:`CaptureFixture[bytes] <pytest.CaptureFixture>`. 

941 

942 Example: 

943 

944 .. code-block:: python 

945 

946 def test_output(capsysbinary): 

947 print("hello") 

948 captured = capsysbinary.readouterr() 

949 assert captured.out == b"hello\n" 

950 """ 

951 capman = request.config.pluginmanager.getplugin("capturemanager") 

952 capture_fixture = CaptureFixture[bytes](SysCaptureBinary, request, _ispytest=True) 

953 capman.set_fixture(capture_fixture) 

954 capture_fixture._start() 

955 yield capture_fixture 

956 capture_fixture.close() 

957 capman.unset_fixture() 

958 

959 

960@fixture 

961def capfd(request: SubRequest) -> Generator[CaptureFixture[str], None, None]: 

962 r"""Enable text capturing of writes to file descriptors ``1`` and ``2``. 

963 

964 The captured output is made available via ``capfd.readouterr()`` method 

965 calls, which return a ``(out, err)`` namedtuple. 

966 ``out`` and ``err`` will be ``text`` objects. 

967 

968 Returns an instance of :class:`CaptureFixture[str] <pytest.CaptureFixture>`. 

969 

970 Example: 

971 

972 .. code-block:: python 

973 

974 def test_system_echo(capfd): 

975 os.system('echo "hello"') 

976 captured = capfd.readouterr() 

977 assert captured.out == "hello\n" 

978 """ 

979 capman = request.config.pluginmanager.getplugin("capturemanager") 

980 capture_fixture = CaptureFixture[str](FDCapture, request, _ispytest=True) 

981 capman.set_fixture(capture_fixture) 

982 capture_fixture._start() 

983 yield capture_fixture 

984 capture_fixture.close() 

985 capman.unset_fixture() 

986 

987 

988@fixture 

989def capfdbinary(request: SubRequest) -> Generator[CaptureFixture[bytes], None, None]: 

990 r"""Enable bytes capturing of writes to file descriptors ``1`` and ``2``. 

991 

992 The captured output is made available via ``capfd.readouterr()`` method 

993 calls, which return a ``(out, err)`` namedtuple. 

994 ``out`` and ``err`` will be ``byte`` objects. 

995 

996 Returns an instance of :class:`CaptureFixture[bytes] <pytest.CaptureFixture>`. 

997 

998 Example: 

999 

1000 .. code-block:: python 

1001 

1002 def test_system_echo(capfdbinary): 

1003 os.system('echo "hello"') 

1004 captured = capfdbinary.readouterr() 

1005 assert captured.out == b"hello\n" 

1006 

1007 """ 

1008 capman = request.config.pluginmanager.getplugin("capturemanager") 

1009 capture_fixture = CaptureFixture[bytes](FDCaptureBinary, request, _ispytest=True) 

1010 capman.set_fixture(capture_fixture) 

1011 capture_fixture._start() 

1012 yield capture_fixture 

1013 capture_fixture.close() 

1014 capman.unset_fixture()