provide-testkit 0.5.2__tar.gz → 0.5.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. {provide_testkit-0.5.2/src/provide_testkit.egg-info → provide_testkit-0.5.3}/PKG-INFO +1 -1
  2. provide_testkit-0.5.3/VERSION +1 -0
  3. provide_testkit-0.5.3/src/provide/testkit/_capture.py +144 -0
  4. {provide_testkit-0.5.2 → provide_testkit-0.5.3/src/provide_testkit.egg-info}/PKG-INFO +1 -1
  5. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/tests/test_capture_stream_swap.py +87 -0
  6. provide_testkit-0.5.2/VERSION +0 -1
  7. provide_testkit-0.5.2/src/provide/testkit/_capture.py +0 -81
  8. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/LICENSE +0 -0
  9. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/README.md +0 -0
  10. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/pyproject.toml +0 -0
  11. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/setup.cfg +0 -0
  12. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/__init__.py +0 -0
  13. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/__init__.py +0 -0
  14. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/__main__.py +0 -0
  15. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/_blocker.py +0 -0
  16. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/_early_init.py +0 -0
  17. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/_install_blocker.py +0 -0
  18. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/_install_pth.py +0 -0
  19. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/_streams.py +0 -0
  20. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/base/__init__.py +0 -0
  21. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/base/foundation.py +0 -0
  22. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/base/harness.py +0 -0
  23. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/base/minimal.py +0 -0
  24. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/chaos/__init__.py +0 -0
  25. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/chaos/concurrency_strategies.py +0 -0
  26. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/chaos/fixtures.py +0 -0
  27. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/chaos/io_strategies.py +0 -0
  28. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/chaos/strategies.py +0 -0
  29. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/chaos/time_strategies.py +0 -0
  30. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/cli/__init__.py +0 -0
  31. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/cli/runner.py +0 -0
  32. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/cli/testing.py +0 -0
  33. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/common/__init__.py +0 -0
  34. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/common/fixtures.py +0 -0
  35. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/conftest.py +0 -0
  36. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/crypto/__init__.py +0 -0
  37. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/crypto/fixtures.py +0 -0
  38. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/file/__init__.py +0 -0
  39. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/file/content_fixtures.py +0 -0
  40. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/file/directory_fixtures.py +0 -0
  41. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/file/fixtures.py +0 -0
  42. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/file/special_fixtures.py +0 -0
  43. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/file/temp.py +0 -0
  44. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/hub/__init__.py +0 -0
  45. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/hub/fixtures.py +0 -0
  46. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/logger/__init__.py +0 -0
  47. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/logger/hooks.py +0 -0
  48. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/logger/mocks.py +0 -0
  49. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/logger/reset.py +0 -0
  50. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/main.py +0 -0
  51. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/mocking/__init__.py +0 -0
  52. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/mocking/fixtures.py +0 -0
  53. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/mocking/time.py +0 -0
  54. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/process/__init__.py +0 -0
  55. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/process/async_fixtures.py +0 -0
  56. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/process/fixtures.py +0 -0
  57. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/process/script_assertions.py +0 -0
  58. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/process/script_fixtures.py +0 -0
  59. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/process/subprocess_fixtures.py +0 -0
  60. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/process/system_fixtures.py +0 -0
  61. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/provide_testkit_init.pth +0 -0
  62. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/py.typed +0 -0
  63. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/pytest_plugin.py +0 -0
  64. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/quality/__init__.py +0 -0
  65. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/quality/artifacts.py +0 -0
  66. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/quality/base.py +0 -0
  67. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/quality/cli.py +0 -0
  68. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/quality/complexity/__init__.py +0 -0
  69. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/quality/complexity/analyzer.py +0 -0
  70. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/quality/complexity/fixture.py +0 -0
  71. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/quality/coverage/__init__.py +0 -0
  72. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/quality/coverage/fixture.py +0 -0
  73. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/quality/coverage/reporter.py +0 -0
  74. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/quality/coverage/tracker.py +0 -0
  75. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/quality/decorators.py +0 -0
  76. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/quality/documentation/__init__.py +0 -0
  77. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/quality/documentation/checker.py +0 -0
  78. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/quality/documentation/fixture.py +0 -0
  79. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/quality/profiling/__init__.py +0 -0
  80. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/quality/profiling/fixture.py +0 -0
  81. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/quality/profiling/profiler.py +0 -0
  82. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/quality/report.py +0 -0
  83. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/quality/runner.py +0 -0
  84. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/quality/security/__init__.py +0 -0
  85. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/quality/security/fixture.py +0 -0
  86. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/quality/security/gitleaks_scanner.py +0 -0
  87. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/quality/security/pip_audit_scanner.py +0 -0
  88. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/quality/security/safety_scanner.py +0 -0
  89. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/quality/security/scanner.py +0 -0
  90. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/quality/security/semgrep_scanner.py +0 -0
  91. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/quality/security/trufflehog_scanner.py +0 -0
  92. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/streams/__init__.py +0 -0
  93. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/streams/testing.py +0 -0
  94. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/threading/__init__.py +0 -0
  95. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/threading/basic_fixtures.py +0 -0
  96. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/threading/data_fixtures.py +0 -0
  97. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/threading/execution_fixtures.py +0 -0
  98. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/threading/fixtures.py +0 -0
  99. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/threading/sync_fixtures.py +0 -0
  100. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/time/__init__.py +0 -0
  101. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/time/classes.py +0 -0
  102. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/time/controlled.py +0 -0
  103. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/time/fixtures.py +0 -0
  104. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/time/freezing.py +0 -0
  105. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/time/measurement.py +0 -0
  106. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/time/mocking.py +0 -0
  107. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/time/rate_limiting.py +0 -0
  108. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/transport/__init__.py +0 -0
  109. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/transport/fixtures.py +0 -0
  110. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/utils/__init__.py +0 -0
  111. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/utils/environment.py +0 -0
  112. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide/testkit/utils/temp_env.py +0 -0
  113. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide_testkit.egg-info/SOURCES.txt +0 -0
  114. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide_testkit.egg-info/dependency_links.txt +0 -0
  115. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide_testkit.egg-info/entry_points.txt +0 -0
  116. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide_testkit.egg-info/requires.txt +0 -0
  117. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/src/provide_testkit.egg-info/top_level.txt +0 -0
  118. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/tests/test_async_fixtures.py +0 -0
  119. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/tests/test_early_init.py +0 -0
  120. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/tests/test_placeholder.py +0 -0
  121. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/tests/test_pth_installation.py +0 -0
  122. {provide_testkit-0.5.2 → provide_testkit-0.5.3}/tests/test_stream_encoding.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: provide-testkit
3
- Version: 0.5.2
3
+ Version: 0.5.3
4
4
  Summary: Testing utilities and fixtures for the provide ecosystem.
5
5
  Author-email: Tim Perkins <code@tim.life>
6
6
  Maintainer-email: "provide.io" <code@provide.io>
@@ -0,0 +1 @@
1
+ 0.5.3
@@ -0,0 +1,144 @@
1
+ #
2
+ # SPDX-FileCopyrightText: Copyright (c) provide.io llc. All rights reserved.
3
+ # SPDX-License-Identifier: Apache-2.0
4
+ #
5
+
6
+ """Capture that gives back the stream it took, not the one it remembers.
7
+
8
+ ``SysCaptureBase.suspend`` hands ``sys.stdout`` back to the stream pytest saved
9
+ before capture started, and ``resume`` reinstates ``tmpfile`` -- the stream
10
+ capture installed at ``start``. Neither looks at what is actually there. A test
11
+ that swaps the stream after capture started owns something pytest has no record
12
+ of, so the swap is discarded on the first suspend and every write after it lands
13
+ in pytest's buffer instead of the caller's.
14
+
15
+ ``log_cli = true`` makes that ordinary: pytest's live-log handler sits on the
16
+ root logger and suspends global capture around each record. One log line emitted
17
+ from inside ``CliRunner.invoke`` -- from any library, at any depth -- leaves
18
+ ``result.output`` empty while the text itself is real and lands in pytest's own
19
+ capture. ``contextlib.redirect_stdout`` and hand-rolled stream fixtures lose
20
+ their swap the same way.
21
+
22
+ The pair below remembers the stream that was in place at suspend and returns it
23
+ at resume, which is what the two methods already claim to do. Teardown is
24
+ untouched: ``done`` still restores the stream capture replaced, so a swap left
25
+ behind by a test cannot outlive it.
26
+
27
+ This belongs in pytest, so the module is built to retire itself rather than to
28
+ be remembered. Installing drives a real capture through swap, suspend and
29
+ resume, and patches only a pytest that loses the swap -- one that keeps it is
30
+ left alone, by whatever route it came to keep it. A pytest that no longer has
31
+ the class costs the fix and nothing else: the import is asked for inside the
32
+ call rather than at module scope, so a reshaped capture module cannot take the
33
+ plugin, and every suite that loads it, down with it.
34
+ """
35
+
36
+ from __future__ import annotations
37
+
38
+ import io
39
+ import sys
40
+ from typing import TYPE_CHECKING, Any
41
+
42
+ if TYPE_CHECKING:
43
+ from _pytest.capture import SysCaptureBase
44
+
45
+ __all__ = ["install_capture_swap_fix"]
46
+
47
+ _INSTALLED_MARKER = "_provide_testkit_swap_safe"
48
+ # The stream is parked on pytest's object, so the name says whose it is.
49
+ _SWAPPED_IN = "_provide_testkit_swapped_in"
50
+ _STDOUT_FD = 1
51
+
52
+
53
+ def _suspend(self: SysCaptureBase[Any]) -> None:
54
+ self._assert_state("suspend", ("started", "suspended"))
55
+ # Suspending while suspended is legal, and the stream sitting there then is
56
+ # the one suspend itself installed -- not a swap worth remembering.
57
+ if self._state == "started":
58
+ setattr(self, _SWAPPED_IN, getattr(sys, self.name))
59
+ setattr(sys, self.name, self._old)
60
+ self._state = "suspended"
61
+
62
+
63
+ def _resume(self: SysCaptureBase[Any]) -> None:
64
+ self._assert_state("resume", ("started", "suspended"))
65
+ if self._state == "started":
66
+ return
67
+ # A capture resumed without ever suspending has nothing recorded, and the
68
+ # stream capture installed is the right answer for it.
69
+ setattr(sys, self.name, getattr(self, _SWAPPED_IN, self.tmpfile))
70
+ self._state = "started"
71
+
72
+
73
+ def _capture_classes() -> tuple[Any, type] | None:
74
+ """The class to patch and the one to probe, or ``None`` from a pytest without them.
75
+
76
+ ``SysCaptureBase`` carries the two methods for every capture that reaches
77
+ ``sys``; ``SysCapture`` is the one that can be driven. Asking for them here
78
+ rather than at import is what keeps a renamed or removed class a lost fix
79
+ instead of an ``ImportError`` raised while the plugin is loading.
80
+ """
81
+ try:
82
+ from _pytest.capture import SysCapture, SysCaptureBase
83
+ except (ImportError, AttributeError):
84
+ return None
85
+ return SysCaptureBase, SysCapture
86
+
87
+
88
+ def _drops_a_swap(probe_class: type) -> bool:
89
+ """Whether this pytest loses a stream installed after capturing started.
90
+
91
+ The question is behavioural, so the answer is measured rather than read off
92
+ a version: start a capture, swap the stream, cycle suspend and resume, and
93
+ see which stream is left standing. A pytest that keeps the swap wants
94
+ nothing from this module, and a version number cannot say which pytest that
95
+ is -- a backport, a fork or a vendored copy all answer here for themselves.
96
+
97
+ A probe that cannot be driven at all -- a capture reshaped past recognition
98
+ -- reports no defect. Patching a class whose behaviour could not be
99
+ established is how a fix becomes the outage.
100
+ """
101
+ saved = sys.stdout
102
+ try:
103
+ probe = probe_class(_STDOUT_FD)
104
+ probe.start()
105
+ try:
106
+ swapped_in = io.StringIO()
107
+ sys.stdout = swapped_in
108
+ probe.suspend()
109
+ probe.resume()
110
+ return sys.stdout is not swapped_in
111
+ finally:
112
+ probe.done()
113
+ except Exception:
114
+ return False
115
+ finally:
116
+ sys.stdout = saved
117
+
118
+
119
+ def install_capture_swap_fix() -> bool:
120
+ """Make pytest's stream capture preserve a swap made after it started.
121
+
122
+ Returns whether this call is the one that installed it. Declining leaves
123
+ pytest exactly as found -- a pytest that already keeps the swap, one whose
124
+ methods belong to another patcher, and one this module cannot recognise are
125
+ all cases where doing nothing is the whole job.
126
+ """
127
+ classes = _capture_classes()
128
+ if classes is None:
129
+ return False
130
+ patch_target, probe_class = classes
131
+
132
+ # Read the marker off the class itself. A subclass inherits it, and asking
133
+ # through inheritance would report a fix that is not on the class in hand.
134
+ if _INSTALLED_MARKER in patch_target.__dict__:
135
+ return False
136
+ if any(getattr(patch_target, name).__module__ != "_pytest.capture" for name in ("suspend", "resume")):
137
+ return False
138
+ if not _drops_a_swap(probe_class):
139
+ return False
140
+
141
+ patch_target.suspend = _suspend
142
+ patch_target.resume = _resume
143
+ setattr(patch_target, _INSTALLED_MARKER, True)
144
+ return True
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: provide-testkit
3
- Version: 0.5.2
3
+ Version: 0.5.3
4
4
  Summary: Testing utilities and fixtures for the provide ecosystem.
5
5
  Author-email: Tim Perkins <code@tim.life>
6
6
  Maintainer-email: "provide.io" <code@provide.io>
@@ -22,6 +22,7 @@ import sys
22
22
  from _pytest.capture import SysCapture
23
23
  import pytest
24
24
 
25
+ from provide.testkit import _capture
25
26
  from provide.testkit._capture import install_capture_swap_fix
26
27
 
27
28
  STDOUT_FD = 1
@@ -140,3 +141,89 @@ def test_the_same_run_loses_its_output_without_this_plugin(pytester: pytest.Pyte
140
141
  result = pytester.runpytest_subprocess("-o", "log_cli=true", "-p", "no:provide_testkit")
141
142
 
142
143
  result.assert_outcomes(failed=1)
144
+
145
+
146
+ class _FixedSysCapture(SysCapture):
147
+ """A pytest whose capture hands back the stream that was in place."""
148
+
149
+ def suspend(self) -> None:
150
+ self._assert_state("suspend", ("started", "suspended"))
151
+ if self._state == "started":
152
+ self._seen = getattr(sys, self.name)
153
+ setattr(sys, self.name, self._old)
154
+ self._state = "suspended"
155
+
156
+ def resume(self) -> None:
157
+ self._assert_state("resume", ("started", "suspended"))
158
+ if self._state == "started":
159
+ return
160
+ setattr(sys, self.name, getattr(self, "_seen", self.tmpfile))
161
+ self._state = "started"
162
+
163
+
164
+ class _UnfixedSysCapture(SysCapture):
165
+ """Pytest as shipped: resume reinstates the stream capture installed."""
166
+
167
+ def suspend(self) -> None:
168
+ self._assert_state("suspend", ("started", "suspended"))
169
+ setattr(sys, self.name, self._old)
170
+ self._state = "suspended"
171
+
172
+ def resume(self) -> None:
173
+ self._assert_state("resume", ("started", "suspended"))
174
+ if self._state == "started":
175
+ return
176
+ setattr(sys, self.name, self.tmpfile)
177
+ self._state = "started"
178
+
179
+
180
+ # Both stand-ins answer for pytest itself, so they must claim the origin the
181
+ # install check reads -- a class whose methods came from anywhere else is a
182
+ # different patcher, and declining that is a separate rule.
183
+ for _stand_in in (_FixedSysCapture, _UnfixedSysCapture):
184
+ _stand_in.suspend.__module__ = "_pytest.capture"
185
+ _stand_in.resume.__module__ = "_pytest.capture"
186
+
187
+
188
+ def test_the_fix_is_declined_when_pytest_already_preserves_the_swap(
189
+ monkeypatch: pytest.MonkeyPatch,
190
+ ) -> None:
191
+ """The exit route: a pytest that fixed this upstream is left alone."""
192
+ monkeypatch.setattr(_capture, "_capture_classes", lambda: (_FixedSysCapture, _FixedSysCapture))
193
+
194
+ installed = install_capture_swap_fix()
195
+
196
+ assert installed is False
197
+ assert _FixedSysCapture.suspend.__qualname__ == "_FixedSysCapture.suspend"
198
+
199
+
200
+ def test_the_fix_is_installed_when_pytest_still_drops_the_swap(
201
+ monkeypatch: pytest.MonkeyPatch,
202
+ ) -> None:
203
+ """The probe answers by behaviour, so it must still say yes to today's pytest."""
204
+ monkeypatch.setattr(_capture, "_capture_classes", lambda: (_UnfixedSysCapture, _UnfixedSysCapture))
205
+
206
+ installed = install_capture_swap_fix()
207
+
208
+ assert installed is True
209
+ assert _UnfixedSysCapture.suspend is _capture._suspend
210
+
211
+
212
+ def test_a_pytest_without_the_class_leaves_the_plugin_importable(
213
+ monkeypatch: pytest.MonkeyPatch,
214
+ ) -> None:
215
+ """A reshaped pytest must cost the fix, not every suite that loads the plugin."""
216
+ monkeypatch.setattr(_capture, "_capture_classes", lambda: None)
217
+
218
+ assert install_capture_swap_fix() is False
219
+
220
+
221
+ @pytest.mark.usefixtures("stdout_restored")
222
+ def test_the_probe_gives_back_the_stream_it_found(monkeypatch: pytest.MonkeyPatch) -> None:
223
+ """Deciding whether to patch runs a real capture; it must leave no trace."""
224
+ monkeypatch.setattr(_capture, "_capture_classes", lambda: (_UnfixedSysCapture, _UnfixedSysCapture))
225
+ before = sys.stdout
226
+
227
+ install_capture_swap_fix()
228
+
229
+ assert sys.stdout is before
@@ -1 +0,0 @@
1
- 0.5.2
@@ -1,81 +0,0 @@
1
- #
2
- # SPDX-FileCopyrightText: Copyright (c) provide.io llc. All rights reserved.
3
- # SPDX-License-Identifier: Apache-2.0
4
- #
5
-
6
- """Capture that gives back the stream it took, not the one it remembers.
7
-
8
- ``SysCaptureBase.suspend`` hands ``sys.stdout`` back to the stream pytest saved
9
- before capture started, and ``resume`` reinstates ``tmpfile`` -- the stream
10
- capture installed at ``start``. Neither looks at what is actually there. A test
11
- that swaps the stream after capture started owns something pytest has no record
12
- of, so the swap is discarded on the first suspend and every write after it lands
13
- in pytest's buffer instead of the caller's.
14
-
15
- ``log_cli = true`` makes that ordinary: pytest's live-log handler sits on the
16
- root logger and suspends global capture around each record. One log line emitted
17
- from inside ``CliRunner.invoke`` -- from any library, at any depth -- leaves
18
- ``result.output`` empty while the text itself is real and lands in pytest's own
19
- capture. ``contextlib.redirect_stdout`` and hand-rolled stream fixtures lose
20
- their swap the same way.
21
-
22
- The pair below remembers the stream that was in place at suspend and returns it
23
- at resume, which is what the two methods already claim to do. Teardown is
24
- untouched: ``done`` still restores the stream capture replaced, so a swap left
25
- behind by a test cannot outlive it.
26
-
27
- This belongs in pytest, and the tests in ``tests/test_capture_stream_swap.py``
28
- drive the real ``SysCapture``, so they fail loudly once pytest changes the
29
- methods underneath -- whether it fixes this or reshapes the class. Drop this
30
- module when the pytest floor carries the fix.
31
- """
32
-
33
- from __future__ import annotations
34
-
35
- import sys
36
- from typing import Any
37
-
38
- from _pytest.capture import SysCaptureBase
39
-
40
- __all__ = ["install_capture_swap_fix"]
41
-
42
- _INSTALLED_MARKER = "_provide_testkit_swap_safe"
43
- # The stream is parked on pytest's object, so the name says whose it is.
44
- _SWAPPED_IN = "_provide_testkit_swapped_in"
45
-
46
-
47
- def _suspend(self: SysCaptureBase[Any]) -> None:
48
- self._assert_state("suspend", ("started", "suspended"))
49
- # Suspending while suspended is legal, and the stream sitting there then is
50
- # the one suspend itself installed -- not a swap worth remembering.
51
- if self._state == "started":
52
- setattr(self, _SWAPPED_IN, getattr(sys, self.name))
53
- setattr(sys, self.name, self._old)
54
- self._state = "suspended"
55
-
56
-
57
- def _resume(self: SysCaptureBase[Any]) -> None:
58
- self._assert_state("resume", ("started", "suspended"))
59
- if self._state == "started":
60
- return
61
- # A capture resumed without ever suspending has nothing recorded, and the
62
- # stream capture installed is the right answer for it.
63
- setattr(sys, self.name, getattr(self, _SWAPPED_IN, self.tmpfile))
64
- self._state = "started"
65
-
66
-
67
- def install_capture_swap_fix() -> bool:
68
- """Make pytest's stream capture preserve a swap made after it started.
69
-
70
- Returns whether this call is the one that installed it. Declining leaves
71
- pytest exactly as found: the fix is worth having, and silently layering it
72
- over a patch someone else installed is not.
73
- """
74
- if getattr(SysCaptureBase, _INSTALLED_MARKER, False):
75
- return False
76
- if any(getattr(SysCaptureBase, name).__module__ != "_pytest.capture" for name in ("suspend", "resume")):
77
- return False
78
- SysCaptureBase.suspend = _suspend # type: ignore[method-assign]
79
- SysCaptureBase.resume = _resume # type: ignore[method-assign]
80
- setattr(SysCaptureBase, _INSTALLED_MARKER, True)
81
- return True
File without changes