SKILL.rst#

Claude Code Skill — punit#

This document describes the punit skill for Claude Code and embeds the full SKILL.md snippet. Copy the embedded SKILL.md content (see below) to .claude/skills/punit/SKILL.md in any project that consumes or develops pUnit tests — Claude Code loads it automatically when the user mentions “punit” or discusses unit testing.

Source code: https://github.com/wilson0x4d/punit Docs: https://punit.readthedocs.io/ Install: python3 -m pip install punit

Overview#

pUnit is a modernized xUnit-style unit-testing framework for Python. It provides two core test types: Facts (invariant state tests) and Theories (parameterized tests via @inlinedata). Tests are plain functions/methods — no base classes, no __init__.py required.

Embedded Skill Markdown#

The full SKILL.md content follows verbatim. It is also maintained as a separate file at [docs/SKILL.md](SKILL.md) for standalone use.

docs/SKILL.md#
  1---
  2name: punit
  3description: Provides additional unit-testing guidance for the pUnit framework. MUST use when user mentions "punit" or discusses unit testing. MUST use when creating, refactoring, or reading files inside the `tests/` directory. MUST use when creating or modifying files matching `*test*.py`.
  4user-invocable: true
  5disable-model-invocation: false
  6---
  7
  8`punit` requires Python 3.11+. It exposes `@fact`, `@theory`, `@inlinedata`, `@trait`, and `@fails` decorators to organize code into unit tests. Tests are plain functions/methods — no inheritance required, no `__init__.py` needed. By default pUnit discovers tests under the `tests/` directory.
  9
 10- **Documentation:** https://punit.readthedocs.io/
 11- **Install:** `python3 -m pip install punit`
 12- **Source:** https://github.com/wilson0x4d/punit
 13
 14## CLI Invocation
 15
 16Always prefix with the project's virtual-environment interpreter and always supply a filter:
 17
 18```bash
 19.venv/bin/python -m punit --filter '*'
 20```
 21
 22Multiple filters narrow results (logical AND):
 23
 24```bash
 25.venv/bin/python -m punit --filter '*Widget*' --filter '*Cache*'
 26```
 27
 28## CLI Flags Reference
 29
 30| Flag | Meaning | Example |
 31| :--- | :--- | :--- |
 32| `-q, --quiet` | Suppress normal output | `--quiet` |
 33| `-v, --verbose` | Exhaustive discovery & result detail | `--verbose` |
 34| `-z, --failfast` | Stop on first failure or error | `--failfast` |
 35| `-p, --test-package NAME` | Override test package (default `tests`) | `--test-package foo` |
 36| `-i, --include PATTERN` | Include pattern for file discovery | `--include '*widget*'` |
 37| `-e, --exclude PATTERN` | Exclude pattern (overrides include) | `--exclude '*hardcoded*'` |
 38| `-f, --filter PATTERN\|@FILE` | Restrict tests by qualified name/path | `--filter 'MyClass.fact'` |
 39| `-t, --trait [!]NAME[=VALUE]` | Include/exclude by trait (`!` to exclude) | `--trait '!integration'` |
 40| `--no-pathfix` | Rely on PYTHONPATH instead of adding `src/` | `--no-pathfix` |
 41| `-r, --report {html\|json}` | Generate a report to stdout | `--report json` |
 42| `-o, --output FILENAME` | Write report to file (not stdout) | `--output result.json` |
 43
 44**Wildcard syntax** for include/exclude/filter:
 45- `*` — match one or more characters
 46- `?` — match any single character
 47
 48## Filters File
 49
 50Use `--filter '@path/to/file.txt'` to load patterns from a plaintext file (one per line; lines starting with `#` are comments). Prefix individual filter entries with `!` to exclude them. Order matters: the first matching rule wins.
 51
 52Piped input via stdin is also supported:
 53
 54```bash
 55cat tests/filters-file.txt | .venv/bin/python -m punit --filter '@stdin'
 56```
 57
 58## Writing Tests
 59
 60### Facts — single-case assertions
 61
 62```python
 63from punit import fact
 64
 65@fact
 66async def when_initialized_touch_must_return_true():
 67    mylib = MyLibrary()
 68    mylib.initialize()
 69    await asyncio.sleep(1)
 70    assert mylib.touch(), 'Expected touch() == True after initialize().'
 71```
 72
 73### Theories — parametrized assertions
 74
 75Each `@inlinedata(...)` call produces one test instance; values are passed positionally:
 76
 77```python
 78from punit import theory, inlinedata
 79
 80@theory
 81@inlinedata(2, 2, 4, 'two plus two equals four')
 82@inlinedata(1, 1, 2, 'one plus one equals two')
 83def add_theory(x: int, y: int, z: int, message: str):
 84    assert x + y == z, message
 85```
 86
 87### Traits — categorize tests for CI filtering
 88
 89```python
 90from punit import theory, inlinedata, trait
 91
 92@theory
 93@inlinedata(0, 1, 1)
 94@trait('integration')
 95@trait('redis')
 96def api_query_theory(a: int, b: int, c: int):
 97    assert a + b == c
 98```
 99
100### Expected Failures — regression detection for known bugs
101
102The `@fails` decorator marks a test as expected to fail; the runner inverts its result (passing becomes failure). Useful for tracking known issues. Always stack **below** `@fact` or `@theory`:
103
104```python
105from punit import fact, fails
106
107@fact
108@fails(reason='bug #123: pending fix')
109def broken_feature_should_pass():
110    assert False  # reported as passed; a fix that makes this pass = regression
111```
112
113Requires the `reason=` keyword argument (positional not allowed). Raises if stacked below another pUnit decorator or double-stacked.
114
115### Methods & Classes — same decorators, no inheritance required
116
117```python
118class MyTestFixture:
119
120    @fact
121    def verify_calc_error_condition(self):
122        from punit.assertions.exceptions import raises
123        def call_with_none(): self.calc(None)
124        assert raises[Exception](call_with_none)
125        assert not raises[Exception](lambda: self.calc(1))
126```
127
128### Setup & Teardown
129
130```python
131from punit import setup, teardown
132
133@setup  # runs before each test (module-scoped if at module level; class-scoped if a method inside a test class)
134def prepare_data(): ...
135
136@teardown  # runs after each test (same scoping model as @setup)
137def cleanup(): ...
138```
139
140## Assertion Helpers
141
142pUnit uses Python's `assert` directly. Optional helper modules live under `punit.assertions`:
143
144### Collections
145
146Both `punit` top-level re-exports (`from punit import collections`) and submodule paths (`from punit.assertions import collections`) are supported.
147
148```python
149from punit.assertions import collections
150
151assert collections.are_same([1, 2], (1, 2))   # element-wise equality
152assert collections.has_length(lst, min=3)      # length check (keyword args only: min, max)
153assert collections.is_none_or_empty([])        # None or empty?
154```
155
156### Strings
157
158Both `punit` top-level re-exports (`from punit import strings`) and submodule paths (`from punit.assertions import strings`) are supported.
159
160```python
161from punit.assertions import strings
162
163assert strings.are_same('a', 'a')              # string equality
164assert strings.has_length(s, min=3)            # length check (keyword args only: min, max)
165assert strings.is_none_or_empty(None)          # None or empty?
166assert strings.is_none_or_whitespace('  ')     # whitespace-only?
167```
168
169### Exceptions
170
171Both `punit` top-level re-exports (`from punit import raises`) and submodule paths (`from punit.assertions.exceptions import raises`) are supported.
172
173```python
174# Preferred: generic syntax (Python 3.11+)
175assert raises[TypeError](lambda: int("bad"))
176
177# Fallback: function-arg syntax
178assert raises(lambda: int("bad"), expect=TypeError, exact=True)
179```
180
181### Numeric
182
183Both `punit` top-level re-exports (`from punit import approx`) and submodule paths (`from punit.assertions.numeric import approx, isclose, isnan, isinfinite, percentage`) are supported.
184
185The `approx` class supports natural Python comparison syntax with directional (one-sided) tolerance:
186
187    * ``x == approx(expected)``; approximately equal (bidirectional tolerance)
188    * ``x > approx(threshold)``; strictly greater than (tolerance extends above)
189    * ``x >= approx(threshold)``; at least the threshold (tolerance extends below)
190    * ``x < approx(threshold)``; strictly less than (tolerance extends above)
191    * ``x <= approx(threshold)``; at most the threshold (tolerance extends below)
192
193```python
194from punit.assertions.numeric import approx, isclose, isnan, isinfinite, percentage
195
196# Primary: operator overloads (preferred by user)
197assert 0.1 + 0.2 == approx(0.3)                        # approximately equal
198assert value >  approx(5)                              # strictly greater than
199assert value >= approx(5)                              # at least the threshold
200assert value <  approx(10)                             # strictly less than
201assert value <= approx(10)                             # at most the threshold
202
203# Secondary: fluent methods for advanced scenarios
204assert some_value == approx(3.14).greater_than()       # >= 3.14 with one-sided tolerance below
205assert some_value == approx(10.0).less_than()          # <= 10.0 with one-sided tolerance above
206assert some_value == approx(5).at_least()              # same as greater_than()
207assert some_value == approx(10).at_most()              # same as less_than()
208assert some_value == approx(5).in_range(3, 7)          # within [3, 7] with directional tolerance
209assert some_value == approx().zero()                   # approximately zero
210
211# Standalone helpers
212assert isclose(3.141, 3.14, rel_tol=0.01)              # drop-in math.isclose replacement
213assert isnan(float('nan'))                             # NaN check
214assert isinfinite(float('inf'))                        # infinity check
215assert percentage(10, 100) == 90.0                     # percentage difference
216```
217
218## Default CLI Invocation
219
220The default behavior is equivalent to:
221
222```bash
223python3 -m punit \
224    --test-package tests \
225    --include '*.py' \
226    --exclude '/__*__' \
227    --filter '*'
228```
229
230This includes all Python files under `tests/`, excludes dunder names, and runs every test.
231
232## Troubleshooting
233
234- If pUnit fails to execute, prefix with the venv path: `.venv/bin/python -m punit`.
235- Use `--verbose` to debug discovery / filtering issues.
236- Test files do **not** need `__init__.py`.
237- Exit code `119` indicates one or more tests failed.