pytest-resources
Lazy, typed, attribute-navigable fixtures for test resource files — JSON / JSONC / YAML /
TOML / INI decoded through e-serde by default,
extensible to any canonical bytes -> object loader, and anything else handed back as
raw bytes.
You point it at one or more directories of sample files; it recursively indexes the tree without reading anything, and you reach, filter and randomise parsed values the way they read:
import pytest_resources as pr
def test_profile(resources: pr.Resources):
user = resources.structured.user # sample dir -> dict (decoded, cached)
body = resources.unstructured.intro # a .md -> str
blob = resources.binary.logo # no codec bound -> raw bytes
jsons = resources.data.select("*.json") # filter a folder (glob / regex / kind)
pick = resources.data.choice("*.json") # random pick, seeded per run
ada = resources.make(User, name="ada") # random model instance [objects]
squad = resources.batch(User, 8) # a typed list of them [objects]
pdf = resources.file("report.pdf") # synthetic file, indexed [files]
Install
uv add --group test "pytest-resources[serde]" # e-serde default codecs
uv add --group test pytest-resources # stdlib-only (json + tomllib)
uv add --group test "pytest-resources[objects]" # resources.make()/batch(): random model objects
uv add --group test "pytest-resources[files]" # resources.file(): synthetic files, 25 formats
uv add --group test "pytest-resources[random]" # both
The resources fixture auto-loads via the pytest11 entry point — no imports in your
conftest.py.
What you get
| Idea | Mechanism |
|---|---|
| Recursive index, zero read | each root is walked once (off the loop); only paths are held |
| Lazy + cached | a file decodes on first access and is memoized for the session |
| Attr / item / iter views | r.a.b, r["a"]["b"], list(r.a) → list of decoded values |
| Extract from a folder | select/first · paths · walk/awalk (sync/async, lazy) · similar (fuzzy) · as_dict, by glob / regex / kind |
| Randomise the pick | r.a.choice(...) (session-seeded) or pass your own rng |
| Synthesize on the fly | make(Model | hint, seed, **pins) / batch(Model | hint, n) for objects, file(kind | name, ...) for real files adopted into the tree — optional [objects] / [files] / [random] extras |
| Typed by suffix | FileType (StrEnum) resolves the extension; the loader table decodes |
| Open-ended fallback | CSV/TSV/PDF/… and any unknown kind stay raw bytes |
| Many roots, one tree | CLI / ini / pytest_resources_roots hook, merged (later wins), auto-created |
| Pluggable codecs | override or add any kind through the pytest_resource_loaders hook |
| e-serde default, optional | native Rust/C config codecs when installed; stdlib otherwise |
| Friendly misses | EntryNotFoundError / KeyError carry a "did you mean" hint |
Configuring the roots
By default the plugin indexes <rootdir>/tests/resources (and creates it if missing).
Point it at one or several directories:
# pyproject.toml
[tool.pytest.ini_options]
resources_root = ["fixtures", "fixtures/shared"] # default: <rootdir>/tests/resources
Or per run (--resources-root is repeatable), or additively from your conftest.py:
from pathlib import Path
def pytest_resources_roots(roots):
roots.append(Path("tests/fixtures/shared"))
CLI wins over ini, which wins over the default. Every resolved root is merged into one navigable tree; on a top-level clash the later root wins.
Custom loaders (the extension seam)
The plugin never assumes e-serde. Bind any parser declaratively in your conftest.py:
from pytest_resources import FileType
import yaml, csv, io
def pytest_resource_loaders(register):
register({FileType.CSV: lambda b: list(csv.DictReader(io.StringIO(b.decode())))})
register({FileType.YAML: yaml.safe_load})
register merges into the session's table, so you can override a default kind as easily
as add an unknown one. Kinds left unbound hand back raw bytes.
Programmatic use
Outside pytest, build a tree yourself:
from pathlib import Path
import pytest_resources as pr
tree = pr.build_resources(Path("fixtures")) # sync, one root
tree = pr.build_resources([Path("a"), Path("b")]) # merged roots, later wins
tree = await pr.abuild_resources(Path("fixtures")) # off the event loop
table = pr.default_loaders() # best available codecs
Development
make install # uv sync + git hooks
make check # ruff + ty + tach + validate + tests
make ci # everything the pipeline runs (adds coverage gate + docs + zizmor)
License
MIT
Release files for pytest-resources 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| pytest_resources-0.2.0.tar.gz | 28.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pytest_resources-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 46.4 kB
Release files / pytest_resources-0.2.0.tar.gz
| Download URL | pytest_resources-0.2.0.tar.gz |
|---|---|
| Size | 28.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
191a996353186a3fa38668d0e74a2ad6eff29d4c819a8538e342924767830a7a
|
|
BLAKE2b-256 checksum How to use checksums |
ebaf4ba9c1c36e38c161cbfe1c66b0a8351ac2ec8ea5d12190a69ab1b8b4f29c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Release files / pytest_resources-0.2.0-py3-none-any.whl
| Download URL | pytest_resources-0.2.0-py3-none-any.whl |
|---|---|
| Size | 18.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
dc7c7d62b775342bc5b383a154dd9c2c5e285f4d8b267edc2278df7e68f0c17b
|
|
BLAKE2b-256 checksum How to use checksums |
4c5f2948d3862741d3984f8218d3f47b5e37b373c5891df3d2fee706790ab369
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|