pytest-devtools 1.1.0__tar.gz → 1.3.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.
@@ -0,0 +1,446 @@
1
+ Metadata-Version: 2.3
2
+ Name: pytest-devtools
3
+ Version: 1.3.0
4
+ Summary: Pytest plugin providing debug fixtures, ANSI-stripped capsys, whitespace-visible assertions, terminal column management, and click/typer CLI runner fixtures.
5
+ Author: Nathaniel Landau
6
+ Author-email: Nathaniel Landau <github@natelandau.com>
7
+ License: MIT License
8
+
9
+ Copyright (c) 2026 natelandau
10
+
11
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
16
+ Classifier: Framework :: Pytest
17
+ Classifier: Intended Audience :: Developers
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Programming Language :: Python :: 3.14
23
+ Classifier: Topic :: Software Development :: Testing
24
+ Requires-Dist: pytest>=7
25
+ Requires-Dist: rich>=15.0.0
26
+ Requires-Dist: click>=8.2 ; extra == 'click'
27
+ Requires-Dist: typer>=0.25 ; extra == 'typer'
28
+ Requires-Python: >=3.10
29
+ Provides-Extra: click
30
+ Provides-Extra: typer
31
+ Description-Content-Type: text/markdown
32
+
33
+ # pytest-devtools
34
+
35
+ [![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)
36
+ [![codecov](https://codecov.io/gh/natelandau/pytest-devtools/graph/badge.svg)](https://codecov.io/gh/natelandau/pytest-devtools)
37
+
38
+ A pytest plugin that smooths over a few common annoyances when writing and debugging tests.
39
+
40
+ ## Features
41
+
42
+ - **Debug fixture**: pretty-prints variables, paths, and data structures with [Rich](https://rich.readthedocs.io/), and only shows the output when a test fails.
43
+ - **Stripped `capsys` output**: removes ANSI escape codes (and optionally the `tmp_path` prefix) from captured stdout/stderr so assertions stay readable.
44
+ - **Visible whitespace in diffs**: replaces tabs, trailing spaces, carriage returns, and newlines with Unicode symbols when an assertion fails.
45
+ - **Terminal column width control**: sets `COLUMNS` for every test so libraries that auto-wrap (Rich, Click, etc.) produce stable output.
46
+ - **CLI runner output post-processing**: `cli_runner` and `typer_runner` fixtures strip ANSI codes, `tmp_path` prefixes, and trailing whitespace from `click`/`typer` `CliRunner` results, the same way `capsys` does for regular captured output.
47
+
48
+ ## Installation
49
+
50
+ ```bash
51
+ # uv
52
+ uv add pytest-devtools
53
+
54
+ # pip
55
+ pip install pytest-devtools
56
+ ```
57
+
58
+ **Requirements:** Python 3.10+ and pytest 7.0+.
59
+
60
+ The plugin registers itself through the `pytest11` entry point, so no `conftest.py` changes are needed.
61
+
62
+ ## Debug Fixture
63
+
64
+ 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`).
65
+
66
+ ### Basic Usage
67
+
68
+ ```python
69
+ def test_user_creation(debug, tmp_path):
70
+ user = {"name": "Alice", "roles": ["admin", "editor"]}
71
+ debug(user)
72
+
73
+ config_path = tmp_path / "config.toml"
74
+ config_path.write_text("[settings]\nverbose = true")
75
+ debug(config_path)
76
+
77
+ assert user["name"] == "Alice"
78
+ ```
79
+
80
+ When the test fails, stderr shows the buffered output between rule separators:
81
+
82
+ ```
83
+ ──────────────────────────── Debug ─────────────────────────────
84
+ {'name': 'Alice', 'roles': ['admin', 'editor']}
85
+ ──────────────────────────── Debug ─────────────────────────────
86
+ ```
87
+
88
+ ### Multiple Values and Titles
89
+
90
+ Pass several arguments in a single call, and use `title` to label the section:
91
+
92
+ ```python
93
+ def test_transform(debug):
94
+ before = [1, 2, 3]
95
+ after = [x * 2 for x in before]
96
+ debug(before, after, title="Transform")
97
+ ```
98
+
99
+ ### Per-Call Options
100
+
101
+ Override any option on a single call:
102
+
103
+ ```python
104
+ def test_deep_structure(debug, tmp_path):
105
+ nested = {"a": {"b": {"c": {"d": "deep"}}}}
106
+
107
+ # Limit nesting depth
108
+ debug(nested, max_depth=2)
109
+
110
+ # Limit collection length
111
+ debug(list(range(100)), max_length=5)
112
+
113
+ # Show type annotations
114
+ debug(nested, show_type=True)
115
+
116
+ # Show directory tree for Path objects
117
+ debug(tmp_path, list_dir_contents=True)
118
+
119
+ # Disable tmp_path prefix stripping
120
+ debug(tmp_path / "output.txt", strip_tmp_path=False)
121
+ ```
122
+
123
+ ### Path Handling
124
+
125
+ When you pass a `pathlib.Path`:
126
+
127
+ - `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`.
128
+ - Directory listing (default: off). When enabled and the path is a directory, a Rich tree shows the directory contents recursively.
129
+
130
+ ### CLI Options
131
+
132
+ | Flag | Description |
133
+ | ------------------------------ | --------------------------------------------------- |
134
+ | `--print-debug` | Always show debug output, even on passing tests |
135
+ | `--debug-strip-tmp-path` | Strip `tmp_path` prefix from Path objects (default) |
136
+ | `--no-debug-strip-tmp-path` | Show full absolute paths |
137
+ | `--debug-list-dir-contents` | Show directory tree for Path directories |
138
+ | `--no-debug-list-dir-contents` | Don't list directory contents (default) |
139
+ | `--debug-max-depth=N` | Limit nesting depth in pretty-printed output |
140
+ | `--debug-max-length=N` | Limit collection length in pretty-printed output |
141
+ | `--debug-show-type` | Show type annotations above each value |
142
+ | `--no-debug-show-type` | Don't show type annotations (default) |
143
+
144
+ ### INI Options
145
+
146
+ Add these to `pyproject.toml` under `[tool.pytest.ini_options]`:
147
+
148
+ ```toml
149
+ [tool.pytest.ini_options]
150
+ print_debug = true
151
+ debug_strip_tmp_path = true
152
+ debug_list_dir_contents = false
153
+ debug_max_depth = 4
154
+ debug_max_length = 20
155
+ debug_show_type = false
156
+ ```
157
+
158
+ ### Option Precedence
159
+
160
+ Per-call arguments win, then CLI flags, then INI settings:
161
+
162
+ ```
163
+ per-call override > CLI flag > INI option > built-in default
164
+ ```
165
+
166
+ ## Stripped `capsys` Output
167
+
168
+ The plugin overrides the built-in `capsys` fixture so that `readouterr()` returns post-processed strings. Two transformations are available:
169
+
170
+ - ANSI escape stripping (default: on)
171
+ - `tmp_path` prefix stripping (default: off, opt-in)
172
+
173
+ Both can be disabled or enabled independently, and they compose when both are active.
174
+
175
+ ### ANSI Escape Stripping
176
+
177
+ 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:
178
+
179
+ ```python
180
+ def test_greeting(capsys):
181
+ print("\x1b[32mHello, world!\x1b[0m")
182
+
183
+ captured = capsys.readouterr()
184
+ assert captured.out == "Hello, world!\n"
185
+ ```
186
+
187
+ To keep the codes for a single test, mark it with `@pytest.mark.keep_ansi`:
188
+
189
+ ```python
190
+ import pytest
191
+
192
+ @pytest.mark.keep_ansi
193
+ def test_color_codes(capsys):
194
+ print("\x1b[32mgreen\x1b[0m")
195
+ captured = capsys.readouterr()
196
+ assert "\x1b[32m" in captured.out
197
+ ```
198
+
199
+ To turn stripping off globally:
200
+
201
+ ```bash
202
+ pytest --no-strip-ansi
203
+ ```
204
+
205
+ ### `tmp_path` Stripping
206
+
207
+ 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:
208
+
209
+ ```python
210
+ def test_writes_log(capsys, tmp_path):
211
+ log = tmp_path / "app.log"
212
+ print(f"wrote {log}")
213
+
214
+ captured = capsys.readouterr()
215
+ assert captured.out == "wrote app.log\n"
216
+ ```
217
+
218
+ Enable it for one run:
219
+
220
+ ```bash
221
+ pytest --capsys-strip-tmp-path
222
+ ```
223
+
224
+ Or globally in `pyproject.toml`:
225
+
226
+ ```toml
227
+ [tool.pytest.ini_options]
228
+ capsys_strip_tmp_path = true
229
+ ```
230
+
231
+ `--no-capsys-strip-tmp-path` overrides the INI setting for a single run. Stripping applies to both `captured.out` and `captured.err`.
232
+
233
+ > [!NOTE]
234
+ > 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.
235
+
236
+ ### INI Options
237
+
238
+ ```toml
239
+ [tool.pytest.ini_options]
240
+ strip_ansi = true # default: true
241
+ capsys_strip_tmp_path = false # default: false
242
+ ```
243
+
244
+ ## CLI Runner Output
245
+
246
+ `click.testing.CliRunner` and `typer.testing.CliRunner` replace `sys.stdout` and `sys.stderr` with their own in-memory buffers while a command runs. Nothing a CLI writes inside `invoke()` reaches pytest's capture machinery, so the `capsys` override above cannot see it and `result.output` arrives unprocessed.
247
+
248
+ The `cli_runner` and `typer_runner` fixtures are drop-in replacements that post-process the runner's own `Result`.
249
+
250
+ ```bash
251
+ # uv
252
+ uv add --dev "pytest-devtools[click]"
253
+ uv add --dev "pytest-devtools[typer]"
254
+
255
+ # pip
256
+ pip install "pytest-devtools[click]"
257
+ pip install "pytest-devtools[typer]"
258
+ ```
259
+
260
+ Neither extra is strictly required. A project testing a CLI already depends on click or typer directly, and the fixtures activate whenever the library is importable. The extras exist to record the supported versions:
261
+
262
+ | Fixture | Requires |
263
+ | -------------- | ------------- |
264
+ | `cli_runner` | `click>=8.2` |
265
+ | `typer_runner` | `typer>=0.25` |
266
+
267
+ Those floors are where the runner exposes stdout and stderr as separate strings. Below them, `result.stderr` raises `ValueError: stderr not separately captured` and `result.output` silently returns the two streams mixed together. Both fixtures check the installed version at setup and fail with an explicit message rather than letting that surface later.
268
+
269
+ ### click
270
+
271
+ ```python
272
+ from myapp.cli import main
273
+
274
+
275
+ def test_greeting(cli_runner):
276
+ result = cli_runner.invoke(main, ["--name", "world"])
277
+
278
+ assert result.exit_code == 0
279
+ assert result.output == "Hello world\n"
280
+ ```
281
+
282
+ ### typer
283
+
284
+ ```python
285
+ from myapp.cli import app
286
+
287
+
288
+ def test_greeting(typer_runner):
289
+ result = typer_runner.invoke(app, ["--name", "world"])
290
+
291
+ assert result.exit_code == 0
292
+ assert result.output == "Hello world\n"
293
+ ```
294
+
295
+ Both fixtures are real subclasses of their framework's runner, so `isolated_filesystem()` and every other runner method work as usual. Only `invoke()` changes.
296
+
297
+ When a transform is active, `invoke()` returns a transparent proxy over the framework's `Result`: `isinstance(result, click.testing.Result)` and a `-> Result` annotation both hold, but `type(result)` reports the proxy, not the framework's class.
298
+
299
+ ### ANSI Escape Stripping
300
+
301
+ On by default, and controlled by the same settings as `capsys`: `--no-strip-ansi`, `strip_ansi = false`, and `@pytest.mark.keep_ansi` for a single test.
302
+
303
+ This matters most for CLIs that print through Rich. Click strips its own `echo()` styling inside the runner already, but a `Console(force_terminal=True)` writes escape codes straight past that.
304
+
305
+ ### `tmp_path` Stripping
306
+
307
+ Off by default. Enable with `--cli-runner-strip-tmp-path` or `cli_runner_strip_tmp_path = true` to collapse paths under `tmp_path` to their relative portion:
308
+
309
+ ```python
310
+ def test_writes_file(cli_runner, tmp_path):
311
+ result = cli_runner.invoke(main, ["--out", str(tmp_path / "sub" / "report.txt")])
312
+
313
+ assert "wrote sub/report.txt" in result.output
314
+ ```
315
+
316
+ ### Trailing Whitespace Stripping
317
+
318
+ Off by default. Enable with `--cli-runner-strip-trailing-whitespace` or `cli_runner_strip_trailing_whitespace = true`.
319
+
320
+ Typer renders help through a Rich panel, which pads every line out to the full terminal width. At a wide `--columns` setting that means lines of mostly blanks, which makes asserting on `--help` output impractical:
321
+
322
+ ```python
323
+ def test_help(typer_runner):
324
+ result = typer_runner.invoke(app, ["--help"])
325
+
326
+ assert "Usage" in result.output
327
+ assert not any(line.endswith(" ") for line in result.output.split("\n"))
328
+ ```
329
+
330
+ ### Terminal Width
331
+
332
+ Click's help formatter caps at 80 columns and ignores the `COLUMNS` environment variable, so the `--columns` setting alone never reached help text. When the column width feature is enabled, both fixtures pass it to `invoke()` as the `terminal_width` context setting. An explicit `terminal_width` argument always wins:
333
+
334
+ ```python
335
+ def test_narrow_help(cli_runner):
336
+ result = cli_runner.invoke(main, ["--help"], terminal_width=40)
337
+ ```
338
+
339
+ ### Runners You Construct Yourself
340
+
341
+ If runners come from your own conftest fixture and you would rather not switch to `cli_runner`, opt into patching instead. With `cli_runner_patch_result = true` or `--cli-runner-patch-result`, the same post-processing applies to every `Result` for the duration of each test, whoever built the runner:
342
+
343
+ ```toml
344
+ [tool.pytest.ini_options]
345
+ cli_runner_patch_result = true
346
+ ```
347
+
348
+ Three caveats:
349
+
350
+ - Terminal width injection does not reach externally built runners. Only the `cli_runner` and `typer_runner` fixtures' own `invoke()` passes `terminal_width` through; a runner you construct yourself keeps whatever width it would normally use.
351
+ - Enabling patch mode changes what `cli_runner.invoke()` and `typer_runner.invoke()` return too: once the `Result` classes are patched, the fixtures' own `invoke()` skips its wrapping and returns the framework's `Result` directly instead of the proxy.
352
+ - The patch is undone by `monkeypatch`, whose finalizer runs after every function-scoped fixture's teardown but before any session- or module-scoped one. A session- or module-scoped fixture that reads a `Result` during its own teardown therefore sees the raw, unprocessed output.
353
+
354
+ ## Visible Whitespace in Assertions
355
+
356
+ 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.
357
+
358
+ ### Symbol Reference
359
+
360
+ | Character | Symbol | Name |
361
+ | ---------------------- | ------ | ---------------- |
362
+ | Trailing space | `·` | Middle dot |
363
+ | Tab (`\t`) | `→` | Rightwards arrow |
364
+ | Carriage return (`\r`) | `←` | Leftwards arrow |
365
+ | Newline (`\n`) | `↵` | Return symbol |
366
+
367
+ ### Example Output
368
+
369
+ For a test like:
370
+
371
+ ```python
372
+ def test_output():
373
+ assert "hello " == "hello"
374
+ ```
375
+
376
+ The failure message shows:
377
+
378
+ ```
379
+ AssertionError: 'hello·' == 'hello'
380
+
381
+ Whitespace-visible comparison:
382
+ Left: 'hello·'
383
+ Right: 'hello'
384
+ ```
385
+
386
+ ### Disabling
387
+
388
+ Use the `--no-show-whitespace` CLI flag, or set the INI option:
389
+
390
+ ```toml
391
+ [tool.pytest.ini_options]
392
+ show_whitespace = false
393
+ ```
394
+
395
+ > [!NOTE]
396
+ > 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.
397
+
398
+ ## Terminal Column Width
399
+
400
+ 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.
401
+
402
+ The feature is **disabled by default**. Enable it with the `--columns` CLI flag or via INI options.
403
+
404
+ ### CLI Option
405
+
406
+ Set `COLUMNS` for a single run:
407
+
408
+ ```bash
409
+ pytest --columns=180
410
+ ```
411
+
412
+ ### INI Options
413
+
414
+ Enable it permanently in `pyproject.toml`:
415
+
416
+ ```toml
417
+ [tool.pytest.ini_options]
418
+ set_columns = true # turn the feature on
419
+ columns = 180 # value to set when enabled
420
+ ```
421
+
422
+ The `--columns` CLI flag overrides the INI `columns` value when both are present.
423
+
424
+ ## Configuration Summary
425
+
426
+ Every feature is configurable through CLI flags and `pyproject.toml` INI options. The debug fixture additionally supports per-call arguments.
427
+
428
+ | Feature | Default | Toggle with |
429
+ | ---------------------- | --------------------- | ----------------------------------------------------------------- |
430
+ | Debug fixture | Output on failure | Always available; `--print-debug` to also show on success |
431
+ | ANSI stripping | On | `--no-strip-ansi` or `strip_ansi = false` |
432
+ | `tmp_path` in `capsys` | Off | `--capsys-strip-tmp-path` or `capsys_strip_tmp_path = true` |
433
+ | Visible whitespace | On | `--no-show-whitespace` or `show_whitespace = false` |
434
+ | Column width | Off | `--columns=N` or `set_columns = true` |
435
+ | CLI runner ANSI | On | Shares `--no-strip-ansi` / `strip_ansi = false` |
436
+ | `tmp_path` in runner | Off | `--cli-runner-strip-tmp-path` or `cli_runner_strip_tmp_path = true` |
437
+ | Runner trailing space | Off | `--cli-runner-strip-trailing-whitespace` or `cli_runner_strip_trailing_whitespace = true` |
438
+ | Patch external runners | Off | `--cli-runner-patch-result` or `cli_runner_patch_result = true` |
439
+
440
+ ## AI Policy
441
+
442
+ All AI generated content is and always will be meticulously reviewed and approved by the author.
443
+
444
+ ## License
445
+
446
+ MIT