Metadata-Version: 2.3
Name: pytest-devtools
Version: 1.2.0
Summary: Pytest plugin providing debug fixtures, ANSI-stripped capsys, whitespace-visible assertions, and terminal column management.
Author: Nathaniel Landau
Author-email: Nathaniel Landau <github@natelandau.com>
License: MIT License
         
         Copyright (c) 2026 natelandau
         
         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:
         
         The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
         
         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.
Classifier: Framework :: Pytest
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Software Development :: Testing
Requires-Dist: pytest>=7
Requires-Dist: rich>=15.0.0
Requires-Python: >=3.10
Description-Content-Type: text/markdown

# pytest-devtools

[![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)
[![codecov](https://codecov.io/gh/natelandau/pytest-devtools/graph/badge.svg)](https://codecov.io/gh/natelandau/pytest-devtools)

A pytest plugin that smooths over a few common annoyances when writing and debugging tests.

## Features

- **Debug fixture**: pretty-prints variables, paths, and data structures with [Rich](https://rich.readthedocs.io/), and only shows the output when a test fails.
- **Stripped `capsys` output**: removes ANSI escape codes (and optionally the `tmp_path` prefix) from captured stdout/stderr so assertions stay readable.
- **Visible whitespace in diffs**: replaces tabs, trailing spaces, carriage returns, and newlines with Unicode symbols when an assertion fails.
- **Terminal column width control**: sets `COLUMNS` for every test so libraries that auto-wrap (Rich, Click, etc.) produce stable output.

## Installation

```bash
# uv
uv add pytest-devtools

# pip
pip install pytest-devtools
```

**Requirements:** Python 3.10+ and pytest 7.0+.

The plugin registers itself through the `pytest11` entry point, so no `conftest.py` changes are needed.

## Debug Fixture

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`).

### Basic Usage

```python
def test_user_creation(debug, tmp_path):
    user = {"name": "Alice", "roles": ["admin", "editor"]}
    debug(user)

    config_path = tmp_path / "config.toml"
    config_path.write_text("[settings]\nverbose = true")
    debug(config_path)

    assert user["name"] == "Alice"
```

When the test fails, stderr shows the buffered output between rule separators:

```
──────────────────────────── Debug ─────────────────────────────
{'name': 'Alice', 'roles': ['admin', 'editor']}
──────────────────────────── Debug ─────────────────────────────
```

### Multiple Values and Titles

Pass several arguments in a single call, and use `title` to label the section:

```python
def test_transform(debug):
    before = [1, 2, 3]
    after = [x * 2 for x in before]
    debug(before, after, title="Transform")
```

### Per-Call Options

Override any option on a single call:

```python
def test_deep_structure(debug, tmp_path):
    nested = {"a": {"b": {"c": {"d": "deep"}}}}

    # Limit nesting depth
    debug(nested, max_depth=2)

    # Limit collection length
    debug(list(range(100)), max_length=5)

    # Show type annotations
    debug(nested, show_type=True)

    # Show directory tree for Path objects
    debug(tmp_path, list_dir_contents=True)

    # Disable tmp_path prefix stripping
    debug(tmp_path / "output.txt", strip_tmp_path=False)
```

### Path Handling

When you pass a `pathlib.Path`:

- `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`.
- Directory listing (default: off). When enabled and the path is a directory, a Rich tree shows the directory contents recursively.

### CLI Options

| Flag                           | Description                                         |
| ------------------------------ | --------------------------------------------------- |
| `--print-debug`                | Always show debug output, even on passing tests     |
| `--debug-strip-tmp-path`       | Strip `tmp_path` prefix from Path objects (default) |
| `--no-debug-strip-tmp-path`    | Show full absolute paths                            |
| `--debug-list-dir-contents`    | Show directory tree for Path directories            |
| `--no-debug-list-dir-contents` | Don't list directory contents (default)             |
| `--debug-max-depth=N`          | Limit nesting depth in pretty-printed output        |
| `--debug-max-length=N`         | Limit collection length in pretty-printed output    |
| `--debug-show-type`            | Show type annotations above each value              |
| `--no-debug-show-type`         | Don't show type annotations (default)               |

### INI Options

Add these to `pyproject.toml` under `[tool.pytest.ini_options]`:

```toml
[tool.pytest.ini_options]
print_debug = true
debug_strip_tmp_path = true
debug_list_dir_contents = false
debug_max_depth = 4
debug_max_length = 20
debug_show_type = false
```

### Option Precedence

Per-call arguments win, then CLI flags, then INI settings:

```
per-call override  >  CLI flag  >  INI option  >  built-in default
```

## Stripped `capsys` Output

The plugin overrides the built-in `capsys` fixture so that `readouterr()` returns post-processed strings. Two transformations are available:

- ANSI escape stripping (default: on)
- `tmp_path` prefix stripping (default: off, opt-in)

Both can be disabled or enabled independently, and they compose when both are active.

### ANSI Escape Stripping

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:

```python
def test_greeting(capsys):
    print("\x1b[32mHello, world!\x1b[0m")

    captured = capsys.readouterr()
    assert captured.out == "Hello, world!\n"
```

To keep the codes for a single test, mark it with `@pytest.mark.keep_ansi`:

```python
import pytest

@pytest.mark.keep_ansi
def test_color_codes(capsys):
    print("\x1b[32mgreen\x1b[0m")
    captured = capsys.readouterr()
    assert "\x1b[32m" in captured.out
```

To turn stripping off globally:

```bash
pytest --no-strip-ansi
```

### `tmp_path` Stripping

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:

```python
def test_writes_log(capsys, tmp_path):
    log = tmp_path / "app.log"
    print(f"wrote {log}")

    captured = capsys.readouterr()
    assert captured.out == "wrote app.log\n"
```

Enable it for one run:

```bash
pytest --capsys-strip-tmp-path
```

Or globally in `pyproject.toml`:

```toml
[tool.pytest.ini_options]
capsys_strip_tmp_path = true
```

`--no-capsys-strip-tmp-path` overrides the INI setting for a single run. Stripping applies to both `captured.out` and `captured.err`.

> [!NOTE]
> 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.

### INI Options

```toml
[tool.pytest.ini_options]
strip_ansi = true                # default: true
capsys_strip_tmp_path = false    # default: false
```

## Visible Whitespace in Assertions

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.

### Symbol Reference

| Character              | Symbol | Name             |
| ---------------------- | ------ | ---------------- |
| Trailing space         | `·`    | Middle dot       |
| Tab (`\t`)             | `→`    | Rightwards arrow |
| Carriage return (`\r`) | `←`    | Leftwards arrow  |
| Newline (`\n`)         | `↵`    | Return symbol    |

### Example Output

For a test like:

```python
def test_output():
    assert "hello " == "hello"
```

The failure message shows:

```
AssertionError: 'hello·' == 'hello'

Whitespace-visible comparison:
  Left:  'hello·'
  Right: 'hello'
```

### Disabling

Use the `--no-show-whitespace` CLI flag, or set the INI option:

```toml
[tool.pytest.ini_options]
show_whitespace = false
```

> [!NOTE]
> 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.

## Terminal Column Width

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.

The feature is **disabled by default**. Enable it with the `--columns` CLI flag or via INI options.

### CLI Option

Set `COLUMNS` for a single run:

```bash
pytest --columns=180
```

### INI Options

Enable it permanently in `pyproject.toml`:

```toml
[tool.pytest.ini_options]
set_columns = true   # turn the feature on
columns = 180        # value to set when enabled
```

The `--columns` CLI flag overrides the INI `columns` value when both are present.

## Configuration Summary

Every feature is configurable through CLI flags and `pyproject.toml` INI options. The debug fixture additionally supports per-call arguments.

| Feature                | Default               | Toggle with                                                       |
| ---------------------- | --------------------- | ----------------------------------------------------------------- |
| Debug fixture          | Output on failure     | Always available; `--print-debug` to also show on success         |
| ANSI stripping         | On                    | `--no-strip-ansi` or `strip_ansi = false`                         |
| `tmp_path` in `capsys` | Off                   | `--capsys-strip-tmp-path` or `capsys_strip_tmp_path = true`       |
| Visible whitespace     | On                    | `--no-show-whitespace` or `show_whitespace = false`               |
| Column width           | Off                   | `--columns=N` or `set_columns = true`                             |

## AI Policy

All AI generated content is and always will be meticulously reviewed and approved by the author.

## License

MIT
