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.
- {pytest_devtools-1.1.0 → pytest_devtools-1.2.0}/PKG-INFO +86 -45
- pytest_devtools-1.2.0/README.md +299 -0
- {pytest_devtools-1.1.0 → pytest_devtools-1.2.0}/pyproject.toml +33 -6
- pytest_devtools-1.2.0/src/devtools/_options.py +38 -0
- pytest_devtools-1.2.0/src/devtools/capsys_strip.py +196 -0
- {pytest_devtools-1.1.0 → pytest_devtools-1.2.0}/src/devtools/debug_fixture.py +7 -32
- pytest_devtools-1.1.0/README.md +0 -258
- pytest_devtools-1.1.0/src/devtools/capsys_strip.py +0 -138
- {pytest_devtools-1.1.0 → pytest_devtools-1.2.0}/LICENSE +0 -0
- {pytest_devtools-1.1.0 → pytest_devtools-1.2.0}/src/devtools/__init__.py +0 -0
- {pytest_devtools-1.1.0 → pytest_devtools-1.2.0}/src/devtools/columns.py +0 -0
- {pytest_devtools-1.1.0 → pytest_devtools-1.2.0}/src/devtools/plugin.py +0 -0
- {pytest_devtools-1.1.0 → pytest_devtools-1.2.0}/src/devtools/whitespace.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: pytest-devtools
|
|
3
|
-
Version: 1.
|
|
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>=
|
|
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
|
-
|
|
31
|
+
[](https://github.com/natelandau/pytest-devtools/actions/workflows/automated-tests.yml)
|
|
32
|
+
[](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
|
-
-
|
|
36
|
-
-
|
|
37
|
-
-
|
|
38
|
-
-
|
|
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
|
-
#
|
|
46
|
+
# uv
|
|
44
47
|
uv add pytest-devtools
|
|
45
48
|
|
|
46
|
-
#
|
|
49
|
+
# pip
|
|
47
50
|
pip install pytest-devtools
|
|
48
51
|
```
|
|
49
52
|
|
|
50
|
-
**Requirements:** Python 3.
|
|
53
|
+
**Requirements:** Python 3.10+ and pytest 7.0+.
|
|
51
54
|
|
|
52
|
-
The plugin
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
120
|
+
When you pass a `pathlib.Path`:
|
|
118
121
|
|
|
119
|
-
-
|
|
120
|
-
-
|
|
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
|
|
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
|
-
##
|
|
161
|
+
## Stripped `capsys` Output
|
|
159
162
|
|
|
160
|
-
|
|
163
|
+
The plugin overrides the built-in `capsys` fixture so that `readouterr()` returns post-processed strings. Two transformations are available:
|
|
161
164
|
|
|
162
|
-
|
|
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"
|
|
179
|
+
assert captured.out == "Hello, world!\n"
|
|
171
180
|
```
|
|
172
181
|
|
|
173
|
-
|
|
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
|
-
|
|
194
|
+
To turn stripping off globally:
|
|
188
195
|
|
|
189
196
|
```bash
|
|
190
197
|
pytest --no-strip-ansi
|
|
191
198
|
```
|
|
192
199
|
|
|
193
|
-
###
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
-
>
|
|
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
|
|
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
|
-
|
|
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 #
|
|
264
|
-
columns = 180 #
|
|
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
|
-
|
|
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
|
|
274
|
-
|
|
|
275
|
-
| Debug fixture
|
|
276
|
-
| ANSI stripping
|
|
277
|
-
|
|
|
278
|
-
|
|
|
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
|
+
[](https://github.com/natelandau/pytest-devtools/actions/workflows/automated-tests.yml)
|
|
4
|
+
[](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>=
|
|
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.
|
|
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
|
|
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 =
|
|
180
|
-
resolved_list_dir =
|
|
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 =
|
|
184
|
-
resolved_length_raw =
|
|
185
|
-
resolved_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
|
pytest_devtools-1.1.0/README.md
DELETED
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|