Skip to main content

mirk-fixtures

The Python port of @mirk/fixtures: authored documents, loaded through layered sources, patched, validated, and explained.

A fixture is a JSON document somebody wrote by hand. This package finds those documents across several sources, merges them by priority so a local pack can override a shipped one, validates the result against a JSON Schema, and tells you which files contributed to the value you got.

The TypeScript package and this one replay the same conformance corpus, so a Python process and a Node process read the same fixture pack the same way. The loading rules, merge strategies, provenance kinds, and diagnostic shape are described in the @mirk/fixtures README; this page covers what is specific to Python. The Python port ships memory, filesystem, and store sources. It has no package-resource source and no CLI.

Install

uv add mirk-fixtures

The import package is mirk.fixtures, inside the shared mirk namespace. The unrelated mirk distribution on PyPI uses the same top-level name, so do not install it in the same environment.

No third-party runtime dependencies. The install pulls mirk-store, which this package depends on for the store source's structural type and for the JSON number rules a document is read under. The JSON Schema engine is injected, never imported, so this package holds no opinion about which one you use.

Namespace package

mirk is a PEP 420 namespace package. There is no src/mirk/__init__.py in either distribution, so mirk.fixtures installs beside mirk.store the way the npm @mirk/* scope works. Do not add one.

Use

Documents live in a source. A type says which directory holds them and what shape they must have. The loader puts the two together.

import jsonschema
from mirk.fixtures import FixtureLoader, FixtureRegistry
from mirk.fixtures.sources.memory import MemoryFixtureSource


def validator_factory(document):
    engine = jsonschema.Draft202012Validator(document)
    return lambda value: [
        {"message": e.message, "path": list(e.absolute_path)} for e in engine.iter_errors(value)
    ]


registry = FixtureRegistry()
registry.register(
    {
        "type": "theme",
        "directory": "themes",
        "jsonSchema": {"type": "object", "required": ["name"]},
    }
)

source = MemoryFixtureSource("pack", {"themes/dark.json": '{"name":"Dark"}'})
loader = FixtureLoader(registry, [source], json_schema_validator=validator_factory)

loader.load("theme:dark")  # {"name": "Dark"}
loader.list()  # ["theme:dark"]
loader.validate()  # {"ok": True, "diagnostics": []}

A fixture is addressed by a ref, type:id. The id is the file's basename with the matched extension removed.

jsonschema is a suggestion, not a requirement. Any callable that takes a schema document and returns a validator works. mirk.fixtures.conformance exports json_schema_validator_factory if you want a working default rather than the six lines above.

An injected engine owns regex dialect parity. JSON Schema says a pattern is an ECMAScript regular expression, and Python's re is a different dialect that disagrees silently: \w, \W, \d, \D, \b and \B read the Unicode sets where ECMAScript reads the ASCII ones, \s omits U+FEFF, $ also matches before a trailing newline, and . excludes only \n. The pattern compiles either way and simply accepts a different set of strings, so ^\w+$ accepts "é" here and rejects it in a JavaScript host. The six-line factory above has that behavior. json_schema_validator_factory does not: it compiles every pattern and patternProperties key through mirk.fixtures.ecma_regex.translate, and raises UnportablePatternError naming the construct when a pattern uses something the translation cannot express (\p{...}, \cX, \u{...}, a negated shorthand inside a character class, (?<name>...)). Use it, or reuse translate in your own factory, if the same packs are read by both languages.

Method names keep the TypeScript camelCase spelling (loadRaw, referenceGraph) and every structured result is a plain dict with the TypeScript key spelling, so a corpus operation dispatches identically in both languages.

Layers

Sources are layers. A higher priority wins, and a document carrying $patch merges into the layer below it instead of replacing it.

from mirk.fixtures import LayeredSource

shipped = MemoryFixtureSource(
    "shipped",
    {
        "themes/dark.json": '{"name":"Dark","palette":{"bg":"#000"}}',
    },
)
local = MemoryFixtureSource(
    "local",
    {
        "themes/dark.json": '{"$patch":"theme:dark","palette":{"fg":"#fff"}}',
    },
)

layered_registry = FixtureRegistry()
layered_registry.register(
    {"type": "theme", "directory": "themes", "jsonSchema": True, "mergeStrategy": "deep"}
)
loader = FixtureLoader(
    layered_registry,
    [LayeredSource(shipped, "shipped", 0), LayeredSource(local, "local", 10)],
    json_schema_validator=validator_factory,
)

loader.load("theme:dark")
# {"name": "Dark", "palette": {"bg": "#000", "fg": "#fff"}}

loader.loadRaw("theme:dark")["provenance"]["layers"]
# [{... "path": "themes/dark.json", "kind": "base"},
#  {... "path": "themes/dark.json", "kind": "patch"}]

mergeStrategy is replace (the default), deep, or array-replace. There is no deletion marker: under deep and array-replace a patch can add and overwrite keys but never remove one, and under replace the patch body becomes the whole value. Provenance names the documents that contributed and in what order, which is what explain reports.

Sources

Memory holds documents in a dict. Deterministic, no I/O, and what the conformance corpus is built on.

Filesystem walks a directory. Every discovered path is resolved to its real path and must stay inside the root, so a symlink pointing outward is an error rather than a leak. Symlinked directories are not descended into.

from mirk.fixtures.sources.filesystem import FilesystemFixtureSource

source = FilesystemFixtureSource("disk", "./fixtures")

Store reads documents out of a mirk-store collection, so a fixture pack can live in the same SQLite file as everything else.

from mirk.store import SqliteStore
from mirk.fixtures.sources.store import StoreFixtureSource, seed_store_from_fixtures

store = SqliteStore("data.db")
store.put(
    "fixture_docs",
    {
        "id": "dark",
        "content": '{"name":"Dark"}',
        "extension": ".json",
    },
)

source = StoreFixtureSource("db", store, "fixture_docs", path_prefix="themes")
loader = FixtureLoader(registry, [source], json_schema_validator=validator_factory)
loader.load("theme:dark")

A row is {id, content, extension, relativePath?}. The path is <path_prefix>/<id><extension> unless the row carries its own relativePath, which is used as given. The listing is cached until you call source.invalidate(), which the loader never does for you. Refreshing a store-backed loader takes both calls in order: source.invalidate() drops the listing, then loader.invalidate() drops the parsed and materialized values built from it.

seed_store_from_fixtures runs the other way, writing loaded fixtures into a collection:

seed_store_from_fixtures(loader, store, {"theme": "themes"}, include_provenance=True)
store.getById("themes", "dark")
# {"id": "dark", "value": {"name": "Dark"}, "provenance": {...}}

Every fixture is collected and validated before the first write, so a validation failure or a load failure writes nothing. A write that fails partway through the batch is NOT rolled back: the rows already written stay written.

Reports

validate(ref=None) returns {"ok": bool, "diagnostics": [...]} and degrades rather than raising: one broken source contributes a diagnostic and the rest of the pack still loads. load does not degrade, because a caller asking for one fixture wants that fixture or an error.

referenceGraph() returns the nodes and edges of every {"$ref": "type:id"} in the pack, with unresolved targets kept visible rather than dropped.

Ordering

Everything sorts by Unicode code point: registered type names, list() results, and every source's entries. Python's default string comparison is code point order, so this package needs no comparator; the TypeScript package uses an explicit one to reach the same answer.

The contract

The corpus at conformance/ in the repository root is the contract (format: conformance/README.md). Both the TypeScript suite and this package replay every fixtures/ scenario against both backends. A behavior that is not in the corpus is not contractual.

Two things the corpus deliberately does not own. Schema messages: Ajv and jsonschema word every failure differently, so a validation scenario compares the set of failing instance paths and nothing else. Parse errors: the message wraps the host parser's own words, and V8 and CPython disagree, so those are compared with the message dropped from both sides. Everything else, every message this package writes itself, is compared exactly.

The function mergeStrategy and the validateReferences, extractReferences and materialize hooks are code and cannot cross a language boundary. Each language pins those with its own tests.

Tests

uv sync --group dev
uv run pytest -q
uv run pyright
uv run ruff check .
uv run ruff format --check .

python/ is a uv workspace, so these run the same from either member and share one lockfile.

Release files for mirk-fixtures 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for mirk-fixtures 0.1.0
File Size Uploaded
mirk_fixtures-0.1.0.tar.gz 53.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for mirk-fixtures 0.1.0
File Interpreter ABI Platform
mirk_fixtures-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 96.3 kB

Release files / mirk_fixtures-0.1.0.tar.gz

Download URL mirk_fixtures-0.1.0.tar.gz
Size 53.3 kB
Tags Source
SHA-256 checksum
How to use checksums
748551fb7d02fc4aa93559f972181ace24da95657bc4480a57992efa46c146cd
BLAKE2b-256 checksum
How to use checksums
9241d5b30e886c055186a6088da84d8b8dfec0b0c365b26198f7bab22fad9570
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.10.9 {"installer":{"name":"uv","version":"0.10.9","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release files / mirk_fixtures-0.1.0-py3-none-any.whl

Download URL mirk_fixtures-0.1.0-py3-none-any.whl
Size 42.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
5627a78d453720ac681d2088140cb239bd2f06f817b87a4cbdceedeb4b51146a
BLAKE2b-256 checksum
How to use checksums
a4708cf84baa04dd809c1f41ffb6281eda0143e03b3223b433b89f2cc7b02d57
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.10.9 {"installer":{"name":"uv","version":"0.10.9","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page