pytest-devtools 1.1.0__tar.gz → 1.2.0__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: pytest-devtools
3
- Version: 1.1.0
3
+ Version: 1.2.0
4
4
  Summary: Pytest plugin providing debug fixtures, ANSI-stripped capsys, whitespace-visible assertions, and terminal column management.
5
5
  Author: Nathaniel Landau
6
6
  Author-email: Nathaniel Landau <github@natelandau.com>
@@ -21,39 +21,42 @@ Classifier: Programming Language :: Python :: 3.12
21
21
  Classifier: Programming Language :: Python :: 3.13
22
22
  Classifier: Programming Language :: Python :: 3.14
23
23
  Classifier: Topic :: Software Development :: Testing
24
- Requires-Dist: pytest>=9.0.3
24
+ Requires-Dist: pytest>=7
25
25
  Requires-Dist: rich>=15.0.0
26
26
  Requires-Python: >=3.10
27
27
  Description-Content-Type: text/markdown
28
28
 
29
29
  # pytest-devtools
30
30
 
31
- Small pytest plugin that provides some niceties for writing and debugging tests.
31
+ [![Automated Tests](https://github.com/natelandau/pytest-devtools/actions/workflows/automated-tests.yml/badge.svg)](https://github.com/natelandau/pytest-devtools/actions/workflows/automated-tests.yml)
32
+ [![codecov](https://codecov.io/gh/natelandau/pytest-devtools/graph/badge.svg)](https://codecov.io/gh/natelandau/pytest-devtools)
33
+
34
+ A pytest plugin that smooths over a few common annoyances when writing and debugging tests.
32
35
 
33
36
  ## Features
34
37
 
35
- - **Debug fixture** -- Pretty-print variables, paths, and data structures with Rich. Output appears only when tests fail (or always with `--print-debug`).
36
- - **ANSI-stripped capsys** -- Automatically strip ANSI escape sequences from captured output so assertions don't break on color codes.
37
- - **Visible whitespace** -- Replace invisible whitespace characters (tabs, trailing spaces, carriage returns, newlines) with Unicode symbols in assertion failure diffs.
38
- - **Terminal column width** -- Optionally set the `COLUMNS` environment variable so Rich and other terminal-aware libraries don't introduce unwanted line wraps.
38
+ - **Debug fixture**: pretty-prints variables, paths, and data structures with [Rich](https://rich.readthedocs.io/), and only shows the output when a test fails.
39
+ - **Stripped `capsys` output**: removes ANSI escape codes (and optionally the `tmp_path` prefix) from captured stdout/stderr so assertions stay readable.
40
+ - **Visible whitespace in diffs**: replaces tabs, trailing spaces, carriage returns, and newlines with Unicode symbols when an assertion fails.
41
+ - **Terminal column width control**: sets `COLUMNS` for every test so libraries that auto-wrap (Rich, Click, etc.) produce stable output.
39
42
 
40
43
  ## Installation
41
44
 
42
45
  ```bash
43
- # Using uv
46
+ # uv
44
47
  uv add pytest-devtools
45
48
 
46
- # Using pip
49
+ # pip
47
50
  pip install pytest-devtools
48
51
  ```
49
52
 
50
- **Requirements:** Python 3.11+ and pytest 7.0+
53
+ **Requirements:** Python 3.10+ and pytest 7.0+.
51
54
 
52
- The plugin activates automatically once installed. No `conftest.py` changes are needed.
55
+ The plugin registers itself through the `pytest11` entry point, so no `conftest.py` changes are needed.
53
56
 
54
57
  ## Debug Fixture
55
58
 
56
- The `debug` fixture gives you a callable that pretty-prints any Python object using Rich. Output is collected during the test and flushed to stderr only when the test fails.
59
+ The `debug` fixture is a callable that pretty-prints any Python object using Rich. Output is buffered during the test and written to stderr only if the test fails (or always, with `--print-debug`).
57
60
 
58
61
  ### Basic Usage
59
62
 
@@ -69,7 +72,7 @@ def test_user_creation(debug, tmp_path):
69
72
  assert user["name"] == "Alice"
70
73
  ```
71
74
 
72
- On failure, stderr shows the Rich-formatted output between rule separators:
75
+ When the test fails, stderr shows the buffered output between rule separators:
73
76
 
74
77
  ```
75
78
  ──────────────────────────── Debug ─────────────────────────────
@@ -79,7 +82,7 @@ On failure, stderr shows the Rich-formatted output between rule separators:
79
82
 
80
83
  ### Multiple Values and Titles
81
84
 
82
- Pass multiple arguments in a single call, and use `title` to label sections:
85
+ Pass several arguments in a single call, and use `title` to label the section:
83
86
 
84
87
  ```python
85
88
  def test_transform(debug):
@@ -90,7 +93,7 @@ def test_transform(debug):
90
93
 
91
94
  ### Per-Call Options
92
95
 
93
- Every option can be overridden on a per-call basis:
96
+ Override any option on a single call:
94
97
 
95
98
  ```python
96
99
  def test_deep_structure(debug, tmp_path):
@@ -114,10 +117,10 @@ def test_deep_structure(debug, tmp_path):
114
117
 
115
118
  ### Path Handling
116
119
 
117
- When you pass a `pathlib.Path` to `debug`:
120
+ When you pass a `pathlib.Path`:
118
121
 
119
- - **tmp_path stripping** (default: on) -- If the path is inside `tmp_path`, only the relative portion is shown. A path like `/var/folders/.../pytest-1234/test_foo0/subdir/file.txt` displays as `subdir/file.txt`.
120
- - **Directory listing** (default: off) -- When enabled and the path is a directory, a Rich tree shows the full directory contents recursively.
122
+ - `tmp_path` stripping (default: on). If the path is inside `tmp_path`, only the relative portion is shown. A path like `/var/folders/.../pytest-1234/test_foo0/subdir/file.txt` displays as `subdir/file.txt`.
123
+ - Directory listing (default: off). When enabled and the path is a directory, a Rich tree shows the directory contents recursively.
121
124
 
122
125
  ### CLI Options
123
126
 
@@ -149,30 +152,34 @@ debug_show_type = false
149
152
 
150
153
  ### Option Precedence
151
154
 
152
- Per-call arguments take highest priority, then CLI flags, then INI settings:
155
+ Per-call arguments win, then CLI flags, then INI settings:
153
156
 
154
157
  ```
155
158
  per-call override > CLI flag > INI option > built-in default
156
159
  ```
157
160
 
158
- ## ANSI-Stripped capsys
161
+ ## Stripped `capsys` Output
159
162
 
160
- By default, `capsys.readouterr()` returns output with ANSI escape sequences removed. This makes assertions simpler when testing code that uses colored output (Rich, Click, Colorama, etc.).
163
+ The plugin overrides the built-in `capsys` fixture so that `readouterr()` returns post-processed strings. Two transformations are available:
161
164
 
162
- ### Basic Usage
165
+ - ANSI escape stripping (default: on)
166
+ - `tmp_path` prefix stripping (default: off, opt-in)
167
+
168
+ Both can be disabled or enabled independently, and they compose when both are active.
169
+
170
+ ### ANSI Escape Stripping
171
+
172
+ Tests that exercise code printing colored output (Rich, Click, Colorama, etc.) usually don't care about the escape codes. By default, they're removed before you see the captured string:
163
173
 
164
174
  ```python
165
175
  def test_greeting(capsys):
166
- # Imagine this function uses Rich to print colored output
167
176
  print("\x1b[32mHello, world!\x1b[0m")
168
177
 
169
178
  captured = capsys.readouterr()
170
- assert captured.out == "Hello, world!\n" # No ANSI codes to worry about
179
+ assert captured.out == "Hello, world!\n"
171
180
  ```
172
181
 
173
- ### Keeping ANSI Codes
174
-
175
- For tests that need to verify color output, disable stripping per-test with a marker:
182
+ To keep the codes for a single test, mark it with `@pytest.mark.keep_ansi`:
176
183
 
177
184
  ```python
178
185
  import pytest
@@ -184,22 +191,54 @@ def test_color_codes(capsys):
184
191
  assert "\x1b[32m" in captured.out
185
192
  ```
186
193
 
187
- Or disable stripping globally with a CLI flag:
194
+ To turn stripping off globally:
188
195
 
189
196
  ```bash
190
197
  pytest --no-strip-ansi
191
198
  ```
192
199
 
193
- ### INI Option
200
+ ### `tmp_path` Stripping
201
+
202
+ Code that prints a `tmp_path`-rooted file produces output like `/var/folders/.../pytest-1234/test_foo0/file.txt`, which is awkward to assert on. Opt in to capsys `tmp_path` stripping to collapse those prefixes to their relative portion:
203
+
204
+ ```python
205
+ def test_writes_log(capsys, tmp_path):
206
+ log = tmp_path / "app.log"
207
+ print(f"wrote {log}")
208
+
209
+ captured = capsys.readouterr()
210
+ assert captured.out == "wrote app.log\n"
211
+ ```
212
+
213
+ Enable it for one run:
214
+
215
+ ```bash
216
+ pytest --capsys-strip-tmp-path
217
+ ```
218
+
219
+ Or globally in `pyproject.toml`:
220
+
221
+ ```toml
222
+ [tool.pytest.ini_options]
223
+ capsys_strip_tmp_path = true
224
+ ```
225
+
226
+ `--no-capsys-strip-tmp-path` overrides the INI setting for a single run. Stripping applies to both `captured.out` and `captured.err`.
227
+
228
+ > [!NOTE]
229
+ > When both transformations are active, ANSI codes are stripped first, then `tmp_path` prefixes. The order matters only if your output mixes the two, but the combined result is what you'd expect.
230
+
231
+ ### INI Options
194
232
 
195
233
  ```toml
196
234
  [tool.pytest.ini_options]
197
- strip_ansi = false
235
+ strip_ansi = true # default: true
236
+ capsys_strip_tmp_path = false # default: false
198
237
  ```
199
238
 
200
239
  ## Visible Whitespace in Assertions
201
240
 
202
- When two strings differ only by whitespace, pytest's default diff is hard to read. This plugin replaces invisible characters with visible Unicode symbols in assertion failure output.
241
+ When two strings differ only in whitespace, pytest's default diff is hard to read. This plugin replaces invisible characters with visible Unicode symbols in the assertion failure message.
203
242
 
204
243
  ### Symbol Reference
205
244
 
@@ -231,20 +270,21 @@ Whitespace-visible comparison:
231
270
 
232
271
  ### Disabling
233
272
 
234
- Disable with the `--no-show-whitespace` CLI flag or in INI:
273
+ Use the `--no-show-whitespace` CLI flag, or set the INI option:
235
274
 
236
275
  ```toml
237
276
  [tool.pytest.ini_options]
238
277
  show_whitespace = false
239
278
  ```
240
279
 
241
- > **Note:** Whitespace visibility only activates for `==` comparisons between strings, and only when the replacement actually changes the display. Non-string comparisons and strings without special whitespace are unaffected.
280
+ > [!NOTE]
281
+ > Whitespace visibility activates only for `==` comparisons between strings, and only when the replacement actually changes how the string displays. Non-string comparisons and strings without notable whitespace are unaffected.
242
282
 
243
283
  ## Terminal Column Width
244
284
 
245
- Many terminal-aware libraries (Rich, Click, etc.) detect the terminal width at runtime. In test environments, the detected width is often very small, causing unwanted line wraps in captured output. This plugin can set the `COLUMNS` environment variable for every test to prevent this.
285
+ Many terminal-aware libraries (Rich, Click, etc.) detect terminal width at runtime. In test environments, the detected width is often very small, which causes unwanted line wraps in captured output. This plugin can set the `COLUMNS` environment variable for every test to keep output stable.
246
286
 
247
- This feature is **disabled by default**. Enable it with the `--columns` CLI flag or via INI options.
287
+ The feature is **disabled by default**. Enable it with the `--columns` CLI flag or via INI options.
248
288
 
249
289
  ### CLI Option
250
290
 
@@ -256,26 +296,27 @@ pytest --columns=180
256
296
 
257
297
  ### INI Options
258
298
 
259
- Enable permanently in `pyproject.toml`:
299
+ Enable it permanently in `pyproject.toml`:
260
300
 
261
301
  ```toml
262
302
  [tool.pytest.ini_options]
263
- set_columns = true # Enable the feature
264
- columns = 180 # Value to set (default when enabled)
303
+ set_columns = true # turn the feature on
304
+ columns = 180 # value to set when enabled
265
305
  ```
266
306
 
267
307
  The `--columns` CLI flag overrides the INI `columns` value when both are present.
268
308
 
269
309
  ## Configuration Summary
270
310
 
271
- All features can be configured via CLI flags, `pyproject.toml` INI options, or (for the debug fixture) per-call arguments.
311
+ Every feature is configurable through CLI flags and `pyproject.toml` INI options. The debug fixture additionally supports per-call arguments.
272
312
 
273
- | Feature | Enabled by default | Enable/Disable with |
274
- | ------------------ | ---------------------- | --------------------------------------------------- |
275
- | Debug fixture | Output on failure only | N/A (always available) |
276
- | ANSI stripping | Yes | `--no-strip-ansi` or `strip_ansi = false` |
277
- | Visible whitespace | Yes | `--no-show-whitespace` or `show_whitespace = false` |
278
- | Column width | No | `--columns=N` or `set_columns = true` |
313
+ | Feature | Default | Toggle with |
314
+ | ---------------------- | --------------------- | ----------------------------------------------------------------- |
315
+ | Debug fixture | Output on failure | Always available; `--print-debug` to also show on success |
316
+ | ANSI stripping | On | `--no-strip-ansi` or `strip_ansi = false` |
317
+ | `tmp_path` in `capsys` | Off | `--capsys-strip-tmp-path` or `capsys_strip_tmp_path = true` |
318
+ | Visible whitespace | On | `--no-show-whitespace` or `show_whitespace = false` |
319
+ | Column width | Off | `--columns=N` or `set_columns = true` |
279
320
 
280
321
  ## AI Policy
281
322
 
@@ -0,0 +1,299 @@
1
+ # pytest-devtools
2
+
3
+ [![Automated Tests](https://github.com/natelandau/pytest-devtools/actions/workflows/automated-tests.yml/badge.svg)](https://github.com/natelandau/pytest-devtools/actions/workflows/automated-tests.yml)
4
+ [![codecov](https://codecov.io/gh/natelandau/pytest-devtools/graph/badge.svg)](https://codecov.io/gh/natelandau/pytest-devtools)
5
+
6
+ A pytest plugin that smooths over a few common annoyances when writing and debugging tests.
7
+
8
+ ## Features
9
+
10
+ - **Debug fixture**: pretty-prints variables, paths, and data structures with [Rich](https://rich.readthedocs.io/), and only shows the output when a test fails.
11
+ - **Stripped `capsys` output**: removes ANSI escape codes (and optionally the `tmp_path` prefix) from captured stdout/stderr so assertions stay readable.
12
+ - **Visible whitespace in diffs**: replaces tabs, trailing spaces, carriage returns, and newlines with Unicode symbols when an assertion fails.
13
+ - **Terminal column width control**: sets `COLUMNS` for every test so libraries that auto-wrap (Rich, Click, etc.) produce stable output.
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ # uv
19
+ uv add pytest-devtools
20
+
21
+ # pip
22
+ pip install pytest-devtools
23
+ ```
24
+
25
+ **Requirements:** Python 3.10+ and pytest 7.0+.
26
+
27
+ The plugin registers itself through the `pytest11` entry point, so no `conftest.py` changes are needed.
28
+
29
+ ## Debug Fixture
30
+
31
+ The `debug` fixture is a callable that pretty-prints any Python object using Rich. Output is buffered during the test and written to stderr only if the test fails (or always, with `--print-debug`).
32
+
33
+ ### Basic Usage
34
+
35
+ ```python
36
+ def test_user_creation(debug, tmp_path):
37
+ user = {"name": "Alice", "roles": ["admin", "editor"]}
38
+ debug(user)
39
+
40
+ config_path = tmp_path / "config.toml"
41
+ config_path.write_text("[settings]\nverbose = true")
42
+ debug(config_path)
43
+
44
+ assert user["name"] == "Alice"
45
+ ```
46
+
47
+ When the test fails, stderr shows the buffered output between rule separators:
48
+
49
+ ```
50
+ ──────────────────────────── Debug ─────────────────────────────
51
+ {'name': 'Alice', 'roles': ['admin', 'editor']}
52
+ ──────────────────────────── Debug ─────────────────────────────
53
+ ```
54
+
55
+ ### Multiple Values and Titles
56
+
57
+ Pass several arguments in a single call, and use `title` to label the section:
58
+
59
+ ```python
60
+ def test_transform(debug):
61
+ before = [1, 2, 3]
62
+ after = [x * 2 for x in before]
63
+ debug(before, after, title="Transform")
64
+ ```
65
+
66
+ ### Per-Call Options
67
+
68
+ Override any option on a single call:
69
+
70
+ ```python
71
+ def test_deep_structure(debug, tmp_path):
72
+ nested = {"a": {"b": {"c": {"d": "deep"}}}}
73
+
74
+ # Limit nesting depth
75
+ debug(nested, max_depth=2)
76
+
77
+ # Limit collection length
78
+ debug(list(range(100)), max_length=5)
79
+
80
+ # Show type annotations
81
+ debug(nested, show_type=True)
82
+
83
+ # Show directory tree for Path objects
84
+ debug(tmp_path, list_dir_contents=True)
85
+
86
+ # Disable tmp_path prefix stripping
87
+ debug(tmp_path / "output.txt", strip_tmp_path=False)
88
+ ```
89
+
90
+ ### Path Handling
91
+
92
+ When you pass a `pathlib.Path`:
93
+
94
+ - `tmp_path` stripping (default: on). If the path is inside `tmp_path`, only the relative portion is shown. A path like `/var/folders/.../pytest-1234/test_foo0/subdir/file.txt` displays as `subdir/file.txt`.
95
+ - Directory listing (default: off). When enabled and the path is a directory, a Rich tree shows the directory contents recursively.
96
+
97
+ ### CLI Options
98
+
99
+ | Flag | Description |
100
+ | ------------------------------ | --------------------------------------------------- |
101
+ | `--print-debug` | Always show debug output, even on passing tests |
102
+ | `--debug-strip-tmp-path` | Strip `tmp_path` prefix from Path objects (default) |
103
+ | `--no-debug-strip-tmp-path` | Show full absolute paths |
104
+ | `--debug-list-dir-contents` | Show directory tree for Path directories |
105
+ | `--no-debug-list-dir-contents` | Don't list directory contents (default) |
106
+ | `--debug-max-depth=N` | Limit nesting depth in pretty-printed output |
107
+ | `--debug-max-length=N` | Limit collection length in pretty-printed output |
108
+ | `--debug-show-type` | Show type annotations above each value |
109
+ | `--no-debug-show-type` | Don't show type annotations (default) |
110
+
111
+ ### INI Options
112
+
113
+ Add these to `pyproject.toml` under `[tool.pytest.ini_options]`:
114
+
115
+ ```toml
116
+ [tool.pytest.ini_options]
117
+ print_debug = true
118
+ debug_strip_tmp_path = true
119
+ debug_list_dir_contents = false
120
+ debug_max_depth = 4
121
+ debug_max_length = 20
122
+ debug_show_type = false
123
+ ```
124
+
125
+ ### Option Precedence
126
+
127
+ Per-call arguments win, then CLI flags, then INI settings:
128
+
129
+ ```
130
+ per-call override > CLI flag > INI option > built-in default
131
+ ```
132
+
133
+ ## Stripped `capsys` Output
134
+
135
+ The plugin overrides the built-in `capsys` fixture so that `readouterr()` returns post-processed strings. Two transformations are available:
136
+
137
+ - ANSI escape stripping (default: on)
138
+ - `tmp_path` prefix stripping (default: off, opt-in)
139
+
140
+ Both can be disabled or enabled independently, and they compose when both are active.
141
+
142
+ ### ANSI Escape Stripping
143
+
144
+ Tests that exercise code printing colored output (Rich, Click, Colorama, etc.) usually don't care about the escape codes. By default, they're removed before you see the captured string:
145
+
146
+ ```python
147
+ def test_greeting(capsys):
148
+ print("\x1b[32mHello, world!\x1b[0m")
149
+
150
+ captured = capsys.readouterr()
151
+ assert captured.out == "Hello, world!\n"
152
+ ```
153
+
154
+ To keep the codes for a single test, mark it with `@pytest.mark.keep_ansi`:
155
+
156
+ ```python
157
+ import pytest
158
+
159
+ @pytest.mark.keep_ansi
160
+ def test_color_codes(capsys):
161
+ print("\x1b[32mgreen\x1b[0m")
162
+ captured = capsys.readouterr()
163
+ assert "\x1b[32m" in captured.out
164
+ ```
165
+
166
+ To turn stripping off globally:
167
+
168
+ ```bash
169
+ pytest --no-strip-ansi
170
+ ```
171
+
172
+ ### `tmp_path` Stripping
173
+
174
+ Code that prints a `tmp_path`-rooted file produces output like `/var/folders/.../pytest-1234/test_foo0/file.txt`, which is awkward to assert on. Opt in to capsys `tmp_path` stripping to collapse those prefixes to their relative portion:
175
+
176
+ ```python
177
+ def test_writes_log(capsys, tmp_path):
178
+ log = tmp_path / "app.log"
179
+ print(f"wrote {log}")
180
+
181
+ captured = capsys.readouterr()
182
+ assert captured.out == "wrote app.log\n"
183
+ ```
184
+
185
+ Enable it for one run:
186
+
187
+ ```bash
188
+ pytest --capsys-strip-tmp-path
189
+ ```
190
+
191
+ Or globally in `pyproject.toml`:
192
+
193
+ ```toml
194
+ [tool.pytest.ini_options]
195
+ capsys_strip_tmp_path = true
196
+ ```
197
+
198
+ `--no-capsys-strip-tmp-path` overrides the INI setting for a single run. Stripping applies to both `captured.out` and `captured.err`.
199
+
200
+ > [!NOTE]
201
+ > When both transformations are active, ANSI codes are stripped first, then `tmp_path` prefixes. The order matters only if your output mixes the two, but the combined result is what you'd expect.
202
+
203
+ ### INI Options
204
+
205
+ ```toml
206
+ [tool.pytest.ini_options]
207
+ strip_ansi = true # default: true
208
+ capsys_strip_tmp_path = false # default: false
209
+ ```
210
+
211
+ ## Visible Whitespace in Assertions
212
+
213
+ When two strings differ only in whitespace, pytest's default diff is hard to read. This plugin replaces invisible characters with visible Unicode symbols in the assertion failure message.
214
+
215
+ ### Symbol Reference
216
+
217
+ | Character | Symbol | Name |
218
+ | ---------------------- | ------ | ---------------- |
219
+ | Trailing space | `·` | Middle dot |
220
+ | Tab (`\t`) | `→` | Rightwards arrow |
221
+ | Carriage return (`\r`) | `←` | Leftwards arrow |
222
+ | Newline (`\n`) | `↵` | Return symbol |
223
+
224
+ ### Example Output
225
+
226
+ For a test like:
227
+
228
+ ```python
229
+ def test_output():
230
+ assert "hello " == "hello"
231
+ ```
232
+
233
+ The failure message shows:
234
+
235
+ ```
236
+ AssertionError: 'hello·' == 'hello'
237
+
238
+ Whitespace-visible comparison:
239
+ Left: 'hello·'
240
+ Right: 'hello'
241
+ ```
242
+
243
+ ### Disabling
244
+
245
+ Use the `--no-show-whitespace` CLI flag, or set the INI option:
246
+
247
+ ```toml
248
+ [tool.pytest.ini_options]
249
+ show_whitespace = false
250
+ ```
251
+
252
+ > [!NOTE]
253
+ > Whitespace visibility activates only for `==` comparisons between strings, and only when the replacement actually changes how the string displays. Non-string comparisons and strings without notable whitespace are unaffected.
254
+
255
+ ## Terminal Column Width
256
+
257
+ Many terminal-aware libraries (Rich, Click, etc.) detect terminal width at runtime. In test environments, the detected width is often very small, which causes unwanted line wraps in captured output. This plugin can set the `COLUMNS` environment variable for every test to keep output stable.
258
+
259
+ The feature is **disabled by default**. Enable it with the `--columns` CLI flag or via INI options.
260
+
261
+ ### CLI Option
262
+
263
+ Set `COLUMNS` for a single run:
264
+
265
+ ```bash
266
+ pytest --columns=180
267
+ ```
268
+
269
+ ### INI Options
270
+
271
+ Enable it permanently in `pyproject.toml`:
272
+
273
+ ```toml
274
+ [tool.pytest.ini_options]
275
+ set_columns = true # turn the feature on
276
+ columns = 180 # value to set when enabled
277
+ ```
278
+
279
+ The `--columns` CLI flag overrides the INI `columns` value when both are present.
280
+
281
+ ## Configuration Summary
282
+
283
+ Every feature is configurable through CLI flags and `pyproject.toml` INI options. The debug fixture additionally supports per-call arguments.
284
+
285
+ | Feature | Default | Toggle with |
286
+ | ---------------------- | --------------------- | ----------------------------------------------------------------- |
287
+ | Debug fixture | Output on failure | Always available; `--print-debug` to also show on success |
288
+ | ANSI stripping | On | `--no-strip-ansi` or `strip_ansi = false` |
289
+ | `tmp_path` in `capsys` | Off | `--capsys-strip-tmp-path` or `capsys_strip_tmp_path = true` |
290
+ | Visible whitespace | On | `--no-show-whitespace` or `show_whitespace = false` |
291
+ | Column width | Off | `--columns=N` or `set_columns = true` |
292
+
293
+ ## AI Policy
294
+
295
+ All AI generated content is and always will be meticulously reviewed and approved by the author.
296
+
297
+ ## License
298
+
299
+ MIT
@@ -10,13 +10,13 @@
10
10
  "Programming Language :: Python :: 3.14",
11
11
  "Topic :: Software Development :: Testing",
12
12
  ]
13
- dependencies = ["pytest>=9.0.3", "rich>=15.0.0"]
13
+ dependencies = ["pytest>=7", "rich>=15.0.0"]
14
14
  description = "Pytest plugin providing debug fixtures, ANSI-stripped capsys, whitespace-visible assertions, and terminal column management."
15
15
  license = { file = "LICENSE" }
16
16
  name = "pytest-devtools"
17
17
  readme = "README.md"
18
18
  requires-python = ">=3.10"
19
- version = "1.1.0"
19
+ version = "1.2.0"
20
20
 
21
21
  [project.entry-points.pytest11]
22
22
  pytest-devtools = "devtools.plugin"
@@ -31,18 +31,18 @@
31
31
  [dependency-groups]
32
32
  dev = [
33
33
  "commitizen>=4.15.0",
34
+ "coverage>=7.13.5",
34
35
  "duty>=1.9.0",
35
36
  "prek>=0.3.11",
36
37
  "pytest-clarity>=1.0.1",
38
+ "pytest-cov>=7.1.0",
37
39
  "pytest-mock>=3.15.1",
38
40
  "pytest-repeat>=0.9.4",
39
- "pytest-sugar>=1.1.1",
40
- "pytest-xdist>=3.8.0",
41
41
  "ruff>=0.15.12",
42
42
  "ty>=0.0.34",
43
43
  "typos>=1.46.0",
44
44
  "yamllint>=1.38.0",
45
- ]
45
+ ]
46
46
 
47
47
  [tool.commitizen]
48
48
  bump_message = "bump(release): v$current_version → v$new_version"
@@ -54,13 +54,40 @@
54
54
 
55
55
  [tool.pytest.ini_options]
56
56
 
57
- addopts = "--color=yes --doctest-modules --strict-config --strict-markers -n auto --dist loadfile"
57
+ addopts = "--color=yes --doctest-modules --strict-config --strict-markers"
58
58
  cache_dir = ".cache/pytest"
59
59
  filterwarnings = ['error', 'ignore:.*Pydantic.*:UserWarning', 'ignore::DeprecationWarning:']
60
60
  markers = ["serial"]
61
61
  testpaths = ["tests"]
62
62
  xfail_strict = true
63
63
 
64
+ [tool.coverage.report] # https://coverage.readthedocs.io/en/latest/config.html#report
65
+ exclude_lines = [
66
+ 'assert_never',
67
+ 'def __repr__',
68
+ 'except [\w\s\._]+ as .*:',
69
+ 'if TYPE_CHECKING:',
70
+ 'if __name__ == .__main__.:',
71
+ 'if typing.TYPE_CHECKING:',
72
+ 'pragma: no cover',
73
+ 'raise cappa\.Exit',
74
+ ]
75
+ fail_under = 20
76
+ # omit = ["src/neatfile/constants.py"]
77
+ precision = 1
78
+ show_missing = true
79
+ skip_covered = true
80
+ skip_empty = true
81
+
82
+ [tool.coverage.run]
83
+ branch = true
84
+ command_line = "--module pytest"
85
+ data_file = ".cache/.coverage"
86
+ source = ["src"]
87
+
88
+ [tool.coverage.xml]
89
+ output = ".cache/coverage.xml"
90
+
64
91
  [tool.ruff] # https://github.com/charliermarsh/ruff
65
92
 
66
93
  exclude = [".cache", ".dev", ".git", ".venv", ".worktrees", "_build", "_site", "build", "dist"]
@@ -0,0 +1,38 @@
1
+ """Shared option resolution for pytest-devtools features."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, Any
6
+
7
+ if TYPE_CHECKING:
8
+ import pytest
9
+
10
+
11
+ def resolve_option(
12
+ request: pytest.FixtureRequest,
13
+ name: str,
14
+ per_call: bool | int | None = None, # noqa: FBT001
15
+ ) -> Any:
16
+ """Resolve a config option using per-call > CLI > INI precedence.
17
+
18
+ Per-call takes priority, then CLI flag, then INI option. Use a single
19
+ ``name`` for both the ``getoption`` dest and the INI key so call sites
20
+ only have to spell the option once.
21
+
22
+ Args:
23
+ request: The pytest fixture request.
24
+ name: The option name (matching both getoption dest and ini key).
25
+ per_call: The per-call override value, or None to fall through to
26
+ CLI/INI resolution.
27
+
28
+ Returns:
29
+ The resolved option value.
30
+ """
31
+ if per_call is not None:
32
+ return per_call
33
+
34
+ cli_value = request.config.getoption(name, default=None)
35
+ if cli_value is not None:
36
+ return cli_value
37
+
38
+ return request.config.getini(name)
@@ -0,0 +1,196 @@
1
+ """ANSI escape sequence stripping for capsys captured output.
2
+
3
+ Override pytest's built-in capsys fixture to automatically strip ANSI escape
4
+ sequences from captured stdout and stderr, making assertions against captured
5
+ output simpler and more reliable.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import contextlib
11
+ import re
12
+ from typing import TYPE_CHECKING, Any
13
+
14
+ import pytest
15
+ from _pytest.capture import CaptureFixture, CaptureResult
16
+
17
+ from devtools._options import resolve_option
18
+
19
+ if TYPE_CHECKING:
20
+ from pathlib import Path
21
+
22
+ from _pytest.config import Config
23
+
24
+ ANSI_PATTERN = re.compile(r"\x1b\[[0-9;]*m")
25
+
26
+
27
+ def strip_ansi(text: str) -> str:
28
+ """Remove ANSI escape sequences from a string.
29
+
30
+ Args:
31
+ text: The string potentially containing ANSI escape sequences.
32
+
33
+ Returns:
34
+ The string with all ANSI SGR sequences removed.
35
+ """
36
+ return ANSI_PATTERN.sub("", text)
37
+
38
+
39
+ def strip_tmp_path(text: str, tmp_path: Path) -> str:
40
+ """Remove tmp_path prefix occurrences from a string.
41
+
42
+ Strip both ``str(tmp_path) + "/"`` and bare ``str(tmp_path)`` so that paths
43
+ nested under ``tmp_path`` collapse to their relative portion (matching the
44
+ behavior of the ``debug`` fixture's tmp_path stripping).
45
+
46
+ Args:
47
+ text: The captured output string.
48
+ tmp_path: The pytest ``tmp_path`` fixture value to strip.
49
+
50
+ Returns:
51
+ The string with tmp_path prefixes removed.
52
+ """
53
+ tmp_str = str(tmp_path)
54
+ return text.replace(f"{tmp_str}/", "").replace(tmp_str, "")
55
+
56
+
57
+ def add_options(parser: pytest.Parser) -> None:
58
+ """Register CLI options for ANSI stripping.
59
+
60
+ Args:
61
+ parser: The pytest option parser to add options to.
62
+ """
63
+ group = parser.getgroup("devtools-capsys", "ANSI stripping for capsys")
64
+ group.addoption(
65
+ "--no-strip-ansi",
66
+ action="store_true",
67
+ default=False,
68
+ help="Disable automatic ANSI escape sequence stripping from capsys output",
69
+ )
70
+ group.addoption(
71
+ "--capsys-strip-tmp-path",
72
+ action="store_true",
73
+ default=None,
74
+ dest="capsys_strip_tmp_path",
75
+ help="Strip tmp_path prefix from capsys output (default: false)",
76
+ )
77
+ group.addoption(
78
+ "--no-capsys-strip-tmp-path",
79
+ action="store_false",
80
+ dest="capsys_strip_tmp_path",
81
+ help="Don't strip tmp_path prefix from capsys output",
82
+ )
83
+ parser.addini(
84
+ "strip_ansi",
85
+ type="bool",
86
+ default=True,
87
+ help="Enable/disable ANSI stripping from capsys output (default: true)",
88
+ )
89
+ parser.addini(
90
+ "capsys_strip_tmp_path",
91
+ type="bool",
92
+ default=False,
93
+ help="Strip tmp_path prefix from capsys output (default: false)",
94
+ )
95
+
96
+
97
+ def configure(config: Config) -> None:
98
+ """Register the keep_ansi marker.
99
+
100
+ Args:
101
+ config: The pytest config object.
102
+ """
103
+ config.addinivalue_line(
104
+ "markers",
105
+ "keep_ansi: disable ANSI stripping from capsys for this test",
106
+ )
107
+
108
+
109
+ def _should_strip_ansi(request: pytest.FixtureRequest) -> bool:
110
+ """Determine whether ANSI stripping should be applied for this test.
111
+
112
+ Args:
113
+ request: The pytest fixture request object.
114
+
115
+ Returns:
116
+ True if ANSI codes should be stripped, False otherwise.
117
+ """
118
+ if request.config.getoption("no_strip_ansi", default=False):
119
+ return False
120
+
121
+ if not request.config.getini("strip_ansi"):
122
+ return False
123
+
124
+ return not request.node.get_closest_marker("keep_ansi")
125
+
126
+
127
+ class StrippedCaptureFixture:
128
+ """Wrapper around CaptureFixture that strips noise from readouterr() results.
129
+
130
+ Delegate all attribute access to the underlying CaptureFixture, intercepting
131
+ only readouterr() to optionally strip ANSI escape sequences and/or the
132
+ tmp_path prefix from captured stdout/stderr.
133
+ """
134
+
135
+ def __init__(
136
+ self,
137
+ original: CaptureFixture[str],
138
+ *,
139
+ request: pytest.FixtureRequest,
140
+ ansi: bool,
141
+ tmp_path: bool,
142
+ ) -> None:
143
+ self._original = original
144
+ self._request = request
145
+ self._strip_ansi_enabled = ansi
146
+ self._strip_tmp_path_enabled = tmp_path
147
+
148
+ def readouterr(self) -> CaptureResult[str]:
149
+ """Read captured output, applying configured post-processing."""
150
+ result = self._original.readouterr()
151
+ out, err = result.out, result.err
152
+
153
+ if self._strip_ansi_enabled:
154
+ out = strip_ansi(out)
155
+ err = strip_ansi(err)
156
+
157
+ if self._strip_tmp_path_enabled:
158
+ tmp_path: Path | None = None
159
+ with contextlib.suppress(pytest.FixtureLookupError):
160
+ tmp_path = self._request.getfixturevalue("tmp_path")
161
+ if tmp_path is not None:
162
+ out = strip_tmp_path(out, tmp_path)
163
+ err = strip_tmp_path(err, tmp_path)
164
+
165
+ return CaptureResult(out=out, err=err)
166
+
167
+ def __getattr__(self, name: str) -> Any:
168
+ """Delegate attribute access to the wrapped capsys fixture."""
169
+ return getattr(self._original, name)
170
+
171
+
172
+ @pytest.fixture
173
+ def capsys(
174
+ request: pytest.FixtureRequest,
175
+ capsys: CaptureFixture[str],
176
+ ) -> CaptureFixture[str] | StrippedCaptureFixture:
177
+ """Override built-in capsys to optionally strip ANSI and/or tmp_path.
178
+
179
+ When either ANSI stripping (default on) or tmp_path stripping (default off)
180
+ is enabled, wrap the original capsys fixture so that readouterr() returns
181
+ post-processed output.
182
+
183
+ Args:
184
+ request: The pytest fixture request object.
185
+ capsys: The original pytest capsys fixture.
186
+
187
+ Returns:
188
+ Either the original capsys or a wrapped version that post-processes output.
189
+ """
190
+ ansi = _should_strip_ansi(request)
191
+ tmp_path = bool(resolve_option(request, "capsys_strip_tmp_path"))
192
+
193
+ if not ansi and not tmp_path:
194
+ return capsys
195
+
196
+ return StrippedCaptureFixture(capsys, request=request, ansi=ansi, tmp_path=tmp_path)
@@ -20,6 +20,8 @@ from rich.console import Console
20
20
  from rich.pretty import pretty_repr
21
21
  from rich.tree import Tree
22
22
 
23
+ from devtools._options import resolve_option
24
+
23
25
  phase_report_key = pytest.StashKey[dict[str, pytest.CollectReport]]()
24
26
 
25
27
 
@@ -97,33 +99,6 @@ def add_options(parser: pytest.Parser) -> None:
97
99
  parser.addini("debug_show_type", type="bool", default=False, help="Show type annotations")
98
100
 
99
101
 
100
- def _resolve_option(
101
- request: pytest.FixtureRequest,
102
- name: str,
103
- per_call: bool | int | None, # noqa: FBT001
104
- ) -> Any:
105
- """Resolve a config option with per-call override.
106
-
107
- Per-call value takes precedence, then CLI flag, then ini option.
108
-
109
- Args:
110
- request: The pytest fixture request.
111
- name: The option name (matching both getoption dest and ini key).
112
- per_call: The per-call override value, or None to use global default.
113
-
114
- Returns:
115
- The resolved option value.
116
- """
117
- if per_call is not None:
118
- return per_call
119
-
120
- cli_value = request.config.getoption(name, default=None)
121
- if cli_value is not None:
122
- return cli_value
123
-
124
- return request.config.getini(name)
125
-
126
-
127
102
  def _build_dir_tree(path: Path, base_name: str) -> Tree:
128
103
  """Build a Rich Tree showing directory contents.
129
104
 
@@ -176,13 +151,13 @@ class DebugPrinter:
176
151
  max_length: Override global max_length setting.
177
152
  show_type: Override global show_type setting.
178
153
  """
179
- resolved_strip = _resolve_option(self._request, "debug_strip_tmp_path", strip_tmp_path)
180
- resolved_list_dir = _resolve_option(
154
+ resolved_strip = resolve_option(self._request, "debug_strip_tmp_path", strip_tmp_path)
155
+ resolved_list_dir = resolve_option(
181
156
  self._request, "debug_list_dir_contents", list_dir_contents
182
157
  )
183
- resolved_depth_raw = _resolve_option(self._request, "debug_max_depth", max_depth)
184
- resolved_length_raw = _resolve_option(self._request, "debug_max_length", max_length)
185
- resolved_show_type = _resolve_option(self._request, "debug_show_type", show_type)
158
+ resolved_depth_raw = resolve_option(self._request, "debug_max_depth", max_depth)
159
+ resolved_length_raw = resolve_option(self._request, "debug_max_length", max_length)
160
+ resolved_show_type = resolve_option(self._request, "debug_show_type", show_type)
186
161
 
187
162
  resolved_depth = int(resolved_depth_raw) if resolved_depth_raw else None
188
163
  resolved_length = int(resolved_length_raw) if resolved_length_raw else None
@@ -1,258 +0,0 @@
1
- # pytest-devtools
2
-
3
- Small pytest plugin that provides some niceties for writing and debugging tests.
4
-
5
- ## Features
6
-
7
- - **Debug fixture** -- Pretty-print variables, paths, and data structures with Rich. Output appears only when tests fail (or always with `--print-debug`).
8
- - **ANSI-stripped capsys** -- Automatically strip ANSI escape sequences from captured output so assertions don't break on color codes.
9
- - **Visible whitespace** -- Replace invisible whitespace characters (tabs, trailing spaces, carriage returns, newlines) with Unicode symbols in assertion failure diffs.
10
- - **Terminal column width** -- Optionally set the `COLUMNS` environment variable so Rich and other terminal-aware libraries don't introduce unwanted line wraps.
11
-
12
- ## Installation
13
-
14
- ```bash
15
- # Using uv
16
- uv add pytest-devtools
17
-
18
- # Using pip
19
- pip install pytest-devtools
20
- ```
21
-
22
- **Requirements:** Python 3.11+ and pytest 7.0+
23
-
24
- The plugin activates automatically once installed. No `conftest.py` changes are needed.
25
-
26
- ## Debug Fixture
27
-
28
- The `debug` fixture gives you a callable that pretty-prints any Python object using Rich. Output is collected during the test and flushed to stderr only when the test fails.
29
-
30
- ### Basic Usage
31
-
32
- ```python
33
- def test_user_creation(debug, tmp_path):
34
- user = {"name": "Alice", "roles": ["admin", "editor"]}
35
- debug(user)
36
-
37
- config_path = tmp_path / "config.toml"
38
- config_path.write_text("[settings]\nverbose = true")
39
- debug(config_path)
40
-
41
- assert user["name"] == "Alice"
42
- ```
43
-
44
- On failure, stderr shows the Rich-formatted output between rule separators:
45
-
46
- ```
47
- ──────────────────────────── Debug ─────────────────────────────
48
- {'name': 'Alice', 'roles': ['admin', 'editor']}
49
- ──────────────────────────── Debug ─────────────────────────────
50
- ```
51
-
52
- ### Multiple Values and Titles
53
-
54
- Pass multiple arguments in a single call, and use `title` to label sections:
55
-
56
- ```python
57
- def test_transform(debug):
58
- before = [1, 2, 3]
59
- after = [x * 2 for x in before]
60
- debug(before, after, title="Transform")
61
- ```
62
-
63
- ### Per-Call Options
64
-
65
- Every option can be overridden on a per-call basis:
66
-
67
- ```python
68
- def test_deep_structure(debug, tmp_path):
69
- nested = {"a": {"b": {"c": {"d": "deep"}}}}
70
-
71
- # Limit nesting depth
72
- debug(nested, max_depth=2)
73
-
74
- # Limit collection length
75
- debug(list(range(100)), max_length=5)
76
-
77
- # Show type annotations
78
- debug(nested, show_type=True)
79
-
80
- # Show directory tree for Path objects
81
- debug(tmp_path, list_dir_contents=True)
82
-
83
- # Disable tmp_path prefix stripping
84
- debug(tmp_path / "output.txt", strip_tmp_path=False)
85
- ```
86
-
87
- ### Path Handling
88
-
89
- When you pass a `pathlib.Path` to `debug`:
90
-
91
- - **tmp_path stripping** (default: on) -- If the path is inside `tmp_path`, only the relative portion is shown. A path like `/var/folders/.../pytest-1234/test_foo0/subdir/file.txt` displays as `subdir/file.txt`.
92
- - **Directory listing** (default: off) -- When enabled and the path is a directory, a Rich tree shows the full directory contents recursively.
93
-
94
- ### CLI Options
95
-
96
- | Flag | Description |
97
- | ------------------------------ | --------------------------------------------------- |
98
- | `--print-debug` | Always show debug output, even on passing tests |
99
- | `--debug-strip-tmp-path` | Strip `tmp_path` prefix from Path objects (default) |
100
- | `--no-debug-strip-tmp-path` | Show full absolute paths |
101
- | `--debug-list-dir-contents` | Show directory tree for Path directories |
102
- | `--no-debug-list-dir-contents` | Don't list directory contents (default) |
103
- | `--debug-max-depth=N` | Limit nesting depth in pretty-printed output |
104
- | `--debug-max-length=N` | Limit collection length in pretty-printed output |
105
- | `--debug-show-type` | Show type annotations above each value |
106
- | `--no-debug-show-type` | Don't show type annotations (default) |
107
-
108
- ### INI Options
109
-
110
- Add these to `pyproject.toml` under `[tool.pytest.ini_options]`:
111
-
112
- ```toml
113
- [tool.pytest.ini_options]
114
- print_debug = true
115
- debug_strip_tmp_path = true
116
- debug_list_dir_contents = false
117
- debug_max_depth = 4
118
- debug_max_length = 20
119
- debug_show_type = false
120
- ```
121
-
122
- ### Option Precedence
123
-
124
- Per-call arguments take highest priority, then CLI flags, then INI settings:
125
-
126
- ```
127
- per-call override > CLI flag > INI option > built-in default
128
- ```
129
-
130
- ## ANSI-Stripped capsys
131
-
132
- By default, `capsys.readouterr()` returns output with ANSI escape sequences removed. This makes assertions simpler when testing code that uses colored output (Rich, Click, Colorama, etc.).
133
-
134
- ### Basic Usage
135
-
136
- ```python
137
- def test_greeting(capsys):
138
- # Imagine this function uses Rich to print colored output
139
- print("\x1b[32mHello, world!\x1b[0m")
140
-
141
- captured = capsys.readouterr()
142
- assert captured.out == "Hello, world!\n" # No ANSI codes to worry about
143
- ```
144
-
145
- ### Keeping ANSI Codes
146
-
147
- For tests that need to verify color output, disable stripping per-test with a marker:
148
-
149
- ```python
150
- import pytest
151
-
152
- @pytest.mark.keep_ansi
153
- def test_color_codes(capsys):
154
- print("\x1b[32mgreen\x1b[0m")
155
- captured = capsys.readouterr()
156
- assert "\x1b[32m" in captured.out
157
- ```
158
-
159
- Or disable stripping globally with a CLI flag:
160
-
161
- ```bash
162
- pytest --no-strip-ansi
163
- ```
164
-
165
- ### INI Option
166
-
167
- ```toml
168
- [tool.pytest.ini_options]
169
- strip_ansi = false
170
- ```
171
-
172
- ## Visible Whitespace in Assertions
173
-
174
- When two strings differ only by whitespace, pytest's default diff is hard to read. This plugin replaces invisible characters with visible Unicode symbols in assertion failure output.
175
-
176
- ### Symbol Reference
177
-
178
- | Character | Symbol | Name |
179
- | ---------------------- | ------ | ---------------- |
180
- | Trailing space | `·` | Middle dot |
181
- | Tab (`\t`) | `→` | Rightwards arrow |
182
- | Carriage return (`\r`) | `←` | Leftwards arrow |
183
- | Newline (`\n`) | `↵` | Return symbol |
184
-
185
- ### Example Output
186
-
187
- For a test like:
188
-
189
- ```python
190
- def test_output():
191
- assert "hello " == "hello"
192
- ```
193
-
194
- The failure message shows:
195
-
196
- ```
197
- AssertionError: 'hello·' == 'hello'
198
-
199
- Whitespace-visible comparison:
200
- Left: 'hello·'
201
- Right: 'hello'
202
- ```
203
-
204
- ### Disabling
205
-
206
- Disable with the `--no-show-whitespace` CLI flag or in INI:
207
-
208
- ```toml
209
- [tool.pytest.ini_options]
210
- show_whitespace = false
211
- ```
212
-
213
- > **Note:** Whitespace visibility only activates for `==` comparisons between strings, and only when the replacement actually changes the display. Non-string comparisons and strings without special whitespace are unaffected.
214
-
215
- ## Terminal Column Width
216
-
217
- Many terminal-aware libraries (Rich, Click, etc.) detect the terminal width at runtime. In test environments, the detected width is often very small, causing unwanted line wraps in captured output. This plugin can set the `COLUMNS` environment variable for every test to prevent this.
218
-
219
- This feature is **disabled by default**. Enable it with the `--columns` CLI flag or via INI options.
220
-
221
- ### CLI Option
222
-
223
- Set `COLUMNS` for a single run:
224
-
225
- ```bash
226
- pytest --columns=180
227
- ```
228
-
229
- ### INI Options
230
-
231
- Enable permanently in `pyproject.toml`:
232
-
233
- ```toml
234
- [tool.pytest.ini_options]
235
- set_columns = true # Enable the feature
236
- columns = 180 # Value to set (default when enabled)
237
- ```
238
-
239
- The `--columns` CLI flag overrides the INI `columns` value when both are present.
240
-
241
- ## Configuration Summary
242
-
243
- All features can be configured via CLI flags, `pyproject.toml` INI options, or (for the debug fixture) per-call arguments.
244
-
245
- | Feature | Enabled by default | Enable/Disable with |
246
- | ------------------ | ---------------------- | --------------------------------------------------- |
247
- | Debug fixture | Output on failure only | N/A (always available) |
248
- | ANSI stripping | Yes | `--no-strip-ansi` or `strip_ansi = false` |
249
- | Visible whitespace | Yes | `--no-show-whitespace` or `show_whitespace = false` |
250
- | Column width | No | `--columns=N` or `set_columns = true` |
251
-
252
- ## AI Policy
253
-
254
- All AI generated content is and always will be meticulously reviewed and approved by the author.
255
-
256
- ## License
257
-
258
- MIT
@@ -1,138 +0,0 @@
1
- """ANSI escape sequence stripping for capsys captured output.
2
-
3
- Override pytest's built-in capsys fixture to automatically strip ANSI escape
4
- sequences from captured stdout and stderr, making assertions against captured
5
- output simpler and more reliable.
6
- """
7
-
8
- from __future__ import annotations
9
-
10
- import re
11
- from typing import TYPE_CHECKING, Any
12
-
13
- import pytest
14
- from _pytest.capture import CaptureFixture, CaptureResult
15
-
16
- if TYPE_CHECKING:
17
- from _pytest.config import Config
18
-
19
- ANSI_PATTERN = re.compile(r"\x1b\[[0-9;]*m")
20
-
21
-
22
- def strip_ansi(text: str) -> str:
23
- """Remove ANSI escape sequences from a string.
24
-
25
- Args:
26
- text: The string potentially containing ANSI escape sequences.
27
-
28
- Returns:
29
- The string with all ANSI SGR sequences removed.
30
- """
31
- return ANSI_PATTERN.sub("", text)
32
-
33
-
34
- def add_options(parser: pytest.Parser) -> None:
35
- """Register CLI options for ANSI stripping.
36
-
37
- Args:
38
- parser: The pytest option parser to add options to.
39
- """
40
- group = parser.getgroup("devtools-capsys", "ANSI stripping for capsys")
41
- group.addoption(
42
- "--no-strip-ansi",
43
- action="store_true",
44
- default=False,
45
- help="Disable automatic ANSI escape sequence stripping from capsys output",
46
- )
47
- parser.addini(
48
- "strip_ansi",
49
- type="bool",
50
- default=True,
51
- help="Enable/disable ANSI stripping from capsys output (default: true)",
52
- )
53
-
54
-
55
- def configure(config: Config) -> None:
56
- """Register the keep_ansi marker.
57
-
58
- Args:
59
- config: The pytest config object.
60
- """
61
- config.addinivalue_line(
62
- "markers",
63
- "keep_ansi: disable ANSI stripping from capsys for this test",
64
- )
65
-
66
-
67
- def _should_strip(request: pytest.FixtureRequest) -> bool:
68
- """Determine whether ANSI stripping should be applied for this test.
69
-
70
- Args:
71
- request: The pytest fixture request object.
72
-
73
- Returns:
74
- True if ANSI codes should be stripped, False otherwise.
75
- """
76
- if request.config.getoption("no_strip_ansi", default=False):
77
- return False
78
-
79
- if not request.config.getini("strip_ansi"):
80
- return False
81
-
82
- return not request.node.get_closest_marker("keep_ansi")
83
-
84
-
85
- class StrippedCaptureFixture:
86
- """Wrapper around CaptureFixture that strips ANSI from readouterr() results.
87
-
88
- Delegate all attribute access to the underlying CaptureFixture, intercepting
89
- only readouterr() to strip ANSI escape sequences.
90
- """
91
-
92
- def __init__(self, original: CaptureFixture[str]) -> None:
93
- self._original = original
94
-
95
- def readouterr(self) -> CaptureResult[str]:
96
- """Read and strip ANSI from captured output.
97
-
98
- Returns:
99
- CaptureResult with ANSI sequences removed from both out and err.
100
- """
101
- result = self._original.readouterr()
102
- return CaptureResult(
103
- out=strip_ansi(result.out),
104
- err=strip_ansi(result.err),
105
- )
106
-
107
- def __getattr__(self, name: str) -> Any:
108
- """Delegate all other attribute access to the original fixture.
109
-
110
- Args:
111
- name: The attribute name to look up.
112
-
113
- Returns:
114
- The attribute from the original CaptureFixture.
115
- """
116
- return getattr(self._original, name)
117
-
118
-
119
- @pytest.fixture
120
- def capsys(
121
- request: pytest.FixtureRequest,
122
- capsys: CaptureFixture[str],
123
- ) -> CaptureFixture[str] | StrippedCaptureFixture:
124
- """Override built-in capsys to optionally strip ANSI escape sequences.
125
-
126
- When ANSI stripping is enabled (the default), wrap the original capsys
127
- fixture so that readouterr() returns output with ANSI codes removed.
128
-
129
- Args:
130
- request: The pytest fixture request object.
131
- capsys: The original pytest capsys fixture.
132
-
133
- Returns:
134
- Either the original capsys or a wrapped version that strips ANSI codes.
135
- """
136
- if _should_strip(request):
137
- return StrippedCaptureFixture(capsys)
138
- return capsys
File without changes