Repository files navigation

dokimi-assert

Test assertions for Python, defined by a language-neutral standard and held to it on every run.

CIPyPIPythonLicence

pip install dokimi-assert

The distribution is dokimi-assert; the package you import is dokimi_assert. Python 3.11 and up. No runtime dependencies.

Getting started

Installing the package registers a pytest plugin, so the seat fixture is available with no conftest and nothing to import.

fromdokimi_assertimportcheckdeftest_get(seat):
item=store.get("widget")
check.is_not_none(seat, item, "Get returns the stored item")
check.equal(seat, item.name, "widget", "and the item is the one stored")

Every assertion takes the seat first and a message last. The message states the contract under test and is the first line of the failure:

AssertionError: and the item is the one stored: want 'widget', got 'gadget'

What a seat is

The seat is where a failure goes. Assertions never call pytest and never raise on their own; they report to whatever seat they are handed. That is what lets one assertion serve a real test, a benchmark, and a test that checks the assertion itself.

You will normally use the fixture and not think about it. Three seats exist, and the fixture hands you the first:

Seatcheck doesexpect does
Collector, from the seat fixturestops the testrecords, reported when the body ends
Standardstops the teststops the test
Recorder, from the recorder fixturerecordsrecords

Use Standard() outside pytest, where nothing owns the end of a test. Use Recorder to read back what an assertion reported instead of suffering it.

Two surfaces

check stops at the first failure. expect records and carries on, so one run shows every property that failed.

fromdokimi_assertimportcheck, expectdeftest_reply(seat):
reply=client.fetch(url)
check.equal(seat, reply.status, 200, "the request succeeds")
expect.has_prefix(seat, reply.body, "{", "the body is JSON")
expect.length(seat, reply.items, 3, "every item comes back")
expect.contains(seat, reply.headers, "etag", "the reply is cacheable")

If two of the three expect calls fail, both are reported together:

AssertionError: 2 failures:
1. every item comes back: expected length 3, got 2
2. the reply is cacheable: {'etag': ...} does not contain 'etag'

Use check when nothing after it makes sense, and expect when each line states an independent property. Both carry the same assertions under the same names.

The assertions

Thirty-four in the root namespace on both surfaces, plus three for golden files and five on the benchmark contract.

Every assertion takes the seat first and the message last. check and expect carry the same names and the same signatures; only what happens on a failure differs.

Equality — Structural, and strict about types.

check.equal(seat: Seat, got: Any, want: Any, msg: str, *options: Option)
check.not_equal(seat: Seat, got: Any, want: Any, msg: str, *options: Option)

Truth and absence — The two-value cases.

check.is_true(seat: Seat, condition: bool, msg: str)
check.is_false(seat: Seat, condition: bool, msg: str)
check.is_none(seat: Seat, got: Any, msg: str)
check.is_not_none(seat: Seat, got: Any, msg: str)

Size — Anything with a length.

check.length(seat: Seat, got: Any, want: int, msg: str)
check.is_empty(seat: Seat, got: Any, msg: str)
check.is_not_empty(seat: Seat, got: Any, msg: str)

Containment — What holding means follows the haystack.

check.contains(seat: Seat, haystack: Any, needle: Any, msg: str, *options: Option)
check.not_contains(seat: Seat, haystack: Any, needle: Any, msg: str, *options: Option)
check.contains_in_order(seat: Seat, got: Any, needles: Sequence[str], msg: str)

Text — str and bytes.

check.has_prefix(seat: Seat, got: Any, prefix: str, msg: str)
check.has_suffix(seat: Seat, got: Any, suffix: str, msg: str)
check.matches(seat: Seat, got: Any, pattern: str, msg: str)

Numbers — Where exact equality is the wrong question.

check.close_to(seat: Seat, got: Any, want: float, tolerance: float, msg: str)
check.in_range(seat: Seat, got: Any, low: float, high: float, msg: str)

Ordering — Sorted, unique, and anything else that holds between neighbours.

check.pairwise(seat: Seat, items: Sequence[Any], predicate: Callable[[Any, Any], bool], msg: str)

Errors — For code that hands an error back rather than raising it.

check.no_error(seat: Seat, exc: BaseException|None, msg: str)
check.has_error(seat: Seat, exc: BaseException|None, msg: str)
check.error_is(seat: Seat, exc: BaseException|None, target: BaseException, msg: str)
check.error_is_not(seat: Seat, exc: BaseException|None, target: BaseException, msg: str)
check.error_as(seat: Seat, exc: BaseException|None, want: type[_E], msg: str) ->_E|None

Raising — For code that raises.

check.raises(seat: Seat, fn: Callable[[], Any], msg: str) ->BaseException|Nonecheck.does_not_raise(seat: Seat, fn: Callable[[], Any], msg: str)

Cancellation — asyncio is Python's cancellation model. These run the loop themselves, so your test stays a plain def. That is also the limit: the two that drive a coroutine cannot be called from a test already running a loop, and say so naming the assertion when they are.

check.honours_cancellation(seat: Seat, fn: Callable[[], Awaitable[Any]], msg: str)
check.honours_deadline(seat: Seat, fn: Callable[[], Awaitable[Any]], msg: str)
check.completes_within(seat: Seat, within: float, fn: Callable[[], Any], msg: str)
check.none_handle_safe(seat: Seat, fn: Callable[[Any], Any], msg: str)

Retrying — For a condition something outside the test makes true. Both spend real time.

check.eventually(seat: Seat, timeout: float, interval: float, body: Callable[[Any], None], msg: str)
check.eventually_true(seat: Seat, timeout: float, predicate: Callable[[], bool], msg: str)

Concurrency — Call what it returns where the scope ends.

check.no_task_leaks(seat: Seat, msg: str) ->Callable[[], None]

Purity — What observe returns defines what nothing means.

check.is_pure(seat: Seat, observe: Callable[[], Any], fn: Callable[[], Any], msg: str, *options: Option)

Testing an assertion — On check only: expect cannot drive a check to failure, because it does not stop.

check.rejects(seat: Seat, msg: str, body: Callable[[Recorder], None]) ->str

Golden files — recorded output, compared and rewritable.

golden.match(seat: Seat, name: str, got: str, update: bool, *scrubbers: Scrubber)
golden.match_at(seat: Seat, path: str|Path, got: str, update: bool, *scrubbers: Scrubber)
golden.match_json_field(seat: Seat, path: str|Path, field: str, got: str, update: bool, *scrubbers: Scrubber)
golden.should_update() ->boolgolden.scrub_timestamps() ->Scrubbergolden.scrub_hashes() ->Scrubbergolden.scrub_run_ids() ->Scrubbergolden.scrub_json_fields(*fields: str) ->Scrubber

Benchmark ceilings — chained onto one contract.

Contract.loop(iterations: int) ->Iterator[int]
Contract.max_latency(seconds: float) ->ContractContract.max_mean(seconds: float) ->ContractContract.max_bytes(count: int) ->ContractContract.excluding(setup: Callable[[], _T]) ->_T

Each one carries a full docstring: what it states, what every argument means, the edge cases it decides, and a worked call. Read them with help(check.close_to) or in your editor.

A few in use

fromdokimi_assertimportcheckdeftest_shapes(seat):
err=check.raises(seat, lambda: parse("{"), "a truncated body is refused")
check.contains(seat, str(err), "unexpected end", "and it says where")
check.pairwise(seat, timestamps, lambdaa, b: a<=b, "the log is ordered")
check.close_to(seat, elapsed, 1.0, 0.05, "the retry waited about a second")
check.matches(seat, request_id, r"^req_[0-9a-f]{16}$", "the id is well formed")
deftest_cancellation(seat):
# The subject is a coroutine function; the test is not. The# assertion drives the event loop itself, so no async plugin.check.honours_cancellation(seat, worker.run, "the worker stops when told")

Equality

Python's == does not answer what the standard asks:

ExpressionPythonHere
0 == FalseTruenot equal
1 == 1.0Truenot equal
[] == NoneFalsenot equal

bool subclasses int, so 0 == False is true, and numeric types compare across themselves. The standard says values of different types never compare, so this enforces it: type(got) is type(want), not isinstance.

An absent collection does not equal an empty one. Where that difference does not matter, relax the comparison for one call:

fromdokimi_assert.optionimportequate_empty, equate_nanscheck.equal(seat, reply.items, [], "no items came back", equate_empty())

An option applies to the call it is passed to and nothing else. There is no global setting, because a rule changed in one place and read in another is how two tests come to mean different things.

Golden files

fromdokimi_assertimportgoldendeftest_render(seat):
golden.match_at(
seat,
"testdata/report.txt",
render(report),
golden.should_update(),
golden.scrub_timestamps(),
)

Set DOKIMI_ASSERT_UPDATE_GOLDEN=1 to rewrite the files. Read the diff before you do. Scrubbers cover timestamps, hex digests, run ids and named JSON fields, so a value that changes every run does not fail the comparison.

Testing your own assertions

rejects states that a check fails, which is the one thing an assertion library has to be able to say about itself:

deftest_the_validator_refuses_an_empty_name(seat):
check.rejects(
seat,
"an empty name is refused",
lambdainner: check.is_none(inner, validate(""), "it passes"),
)

The recorder fixture is the lower-level version: drive an assertion with it, then read failed and message.

The standard

The assertions are defined in assert-spec, language-neutral, and implemented in several languages. This library vendors the definition and holds itself to it:

  • 87 corpus cases state what each assertion must report, run against both surfaces. They are the same cases every other implementation runs.
  • A completeness gate checks every assertion is present under the name the naming table gives it.
  • An overlay records any assertion this library cannot supply. It is empty: all 41 are implemented.

Where Python differs

Go states cancellation with context.Context, which appears in every signature. Python has no such convention, so honours_cancellation, honours_deadline and no_task_leaks are built on asyncio, whose CancelledError, timeouts and tasks are the real analogue. They take a coroutine function rather than a callable.

completes_within measures rather than interrupts: it reports whether a subject finished in time, and a slow subject runs to completion first.

Allocation ceilings use tracemalloc, which counts what Python itself allocated and carries real overhead. Set those ceilings from a traced run; latency ceilings need no such care.

Development

make install # create the environment
make check # the full pre-merge gate
make test# tests
make fmt # format and autofix
make build # build the sdist and wheel
make spec-sync # refresh the vendored definition

make check runs ruff, a formatting check, mypy strict, basedpyright and the tests with a coverage floor. CI runs it on 3.11 through 3.14. The design is recorded in docs/rfc/0001-the-python-implementation.md.

Pushing a v* tag builds, re-runs the gate, checks the tag agrees with pyproject.toml, and publishes to PyPI through Trusted Publishing.

Licence

MIT. See LICENSE.

About

Test assertions for Python, defined by a language-neutral standard and held to it on every run

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

dokimi-assert

Test assertions for Python, defined by a language-neutral standard and held to it on every run.

CIPyPIPythonLicence

pip install dokimi-assert

The distribution is dokimi-assert; the package you import is dokimi_assert. Python 3.11 and up. No runtime dependencies.

Getting started

Installing the package registers a pytest plugin, so the seat fixture is available with no conftest and nothing to import.

fromdokimi_assertimportcheckdeftest_get(seat):
item=store.get("widget")
check.is_not_none(seat, item, "Get returns the stored item")
check.equal(seat, item.name, "widget", "and the item is the one stored")

Every assertion takes the seat first and a message last. The message states the contract under test and is the first line of the failure:

AssertionError: and the item is the one stored: want 'widget', got 'gadget'

What a seat is

The seat is where a failure goes. Assertions never call pytest and never raise on their own; they report to whatever seat they are handed. That is what lets one assertion serve a real test, a benchmark, and a test that checks the assertion itself.

You will normally use the fixture and not think about it. Three seats exist, and the fixture hands you the first:

Seatcheck doesexpect does
Collector, from the seat fixturestops the testrecords, reported when the body ends
Standardstops the teststops the test
Recorder, from the recorder fixturerecordsrecords

Use Standard() outside pytest, where nothing owns the end of a test. Use Recorder to read back what an assertion reported instead of suffering it.

Two surfaces

check stops at the first failure. expect records and carries on, so one run shows every property that failed.

fromdokimi_assertimportcheck, expectdeftest_reply(seat):
reply=client.fetch(url)
check.equal(seat, reply.status, 200, "the request succeeds")
expect.has_prefix(seat, reply.body, "{", "the body is JSON")
expect.length(seat, reply.items, 3, "every item comes back")
expect.contains(seat, reply.headers, "etag", "the reply is cacheable")

If two of the three expect calls fail, both are reported together:

AssertionError: 2 failures:
1. every item comes back: expected length 3, got 2
2. the reply is cacheable: {'etag': ...} does not contain 'etag'

Use check when nothing after it makes sense, and expect when each line states an independent property. Both carry the same assertions under the same names.

The assertions

Thirty-four in the root namespace on both surfaces, plus three for golden files and five on the benchmark contract.

Every assertion takes the seat first and the message last. check and expect carry the same names and the same signatures; only what happens on a failure differs.

Equality — Structural, and strict about types.

check.equal(seat: Seat, got: Any, want: Any, msg: str, *options: Option)
check.not_equal(seat: Seat, got: Any, want: Any, msg: str, *options: Option)

Truth and absence — The two-value cases.

check.is_true(seat: Seat, condition: bool, msg: str)
check.is_false(seat: Seat, condition: bool, msg: str)
check.is_none(seat: Seat, got: Any, msg: str)
check.is_not_none(seat: Seat, got: Any, msg: str)

Size — Anything with a length.

check.length(seat: Seat, got: Any, want: int, msg: str)
check.is_empty(seat: Seat, got: Any, msg: str)
check.is_not_empty(seat: Seat, got: Any, msg: str)

Containment — What holding means follows the haystack.

check.contains(seat: Seat, haystack: Any, needle: Any, msg: str, *options: Option)
check.not_contains(seat: Seat, haystack: Any, needle: Any, msg: str, *options: Option)
check.contains_in_order(seat: Seat, got: Any, needles: Sequence[str], msg: str)

Text — str and bytes.

check.has_prefix(seat: Seat, got: Any, prefix: str, msg: str)
check.has_suffix(seat: Seat, got: Any, suffix: str, msg: str)
check.matches(seat: Seat, got: Any, pattern: str, msg: str)

Numbers — Where exact equality is the wrong question.

check.close_to(seat: Seat, got: Any, want: float, tolerance: float, msg: str)
check.in_range(seat: Seat, got: Any, low: float, high: float, msg: str)

Ordering — Sorted, unique, and anything else that holds between neighbours.

check.pairwise(seat: Seat, items: Sequence[Any], predicate: Callable[[Any, Any], bool], msg: str)

Errors — For code that hands an error back rather than raising it.

check.no_error(seat: Seat, exc: BaseException|None, msg: str)
check.has_error(seat: Seat, exc: BaseException|None, msg: str)
check.error_is(seat: Seat, exc: BaseException|None, target: BaseException, msg: str)
check.error_is_not(seat: Seat, exc: BaseException|None, target: BaseException, msg: str)
check.error_as(seat: Seat, exc: BaseException|None, want: type[_E], msg: str) ->_E|None

Raising — For code that raises.

check.raises(seat: Seat, fn: Callable[[], Any], msg: str) ->BaseException|Nonecheck.does_not_raise(seat: Seat, fn: Callable[[], Any], msg: str)

Cancellation — asyncio is Python's cancellation model. These run the loop themselves, so your test stays a plain def. That is also the limit: the two that drive a coroutine cannot be called from a test already running a loop, and say so naming the assertion when they are.

check.honours_cancellation(seat: Seat, fn: Callable[[], Awaitable[Any]], msg: str)
check.honours_deadline(seat: Seat, fn: Callable[[], Awaitable[Any]], msg: str)
check.completes_within(seat: Seat, within: float, fn: Callable[[], Any], msg: str)
check.none_handle_safe(seat: Seat, fn: Callable[[Any], Any], msg: str)

Retrying — For a condition something outside the test makes true. Both spend real time.

check.eventually(seat: Seat, timeout: float, interval: float, body: Callable[[Any], None], msg: str)
check.eventually_true(seat: Seat, timeout: float, predicate: Callable[[], bool], msg: str)

Concurrency — Call what it returns where the scope ends.

check.no_task_leaks(seat: Seat, msg: str) ->Callable[[], None]

Purity — What observe returns defines what nothing means.

check.is_pure(seat: Seat, observe: Callable[[], Any], fn: Callable[[], Any], msg: str, *options: Option)

Testing an assertion — On check only: expect cannot drive a check to failure, because it does not stop.

check.rejects(seat: Seat, msg: str, body: Callable[[Recorder], None]) ->str

Golden files — recorded output, compared and rewritable.

golden.match(seat: Seat, name: str, got: str, update: bool, *scrubbers: Scrubber)
golden.match_at(seat: Seat, path: str|Path, got: str, update: bool, *scrubbers: Scrubber)
golden.match_json_field(seat: Seat, path: str|Path, field: str, got: str, update: bool, *scrubbers: Scrubber)
golden.should_update() ->boolgolden.scrub_timestamps() ->Scrubbergolden.scrub_hashes() ->Scrubbergolden.scrub_run_ids() ->Scrubbergolden.scrub_json_fields(*fields: str) ->Scrubber

Benchmark ceilings — chained onto one contract.

Contract.loop(iterations: int) ->Iterator[int]
Contract.max_latency(seconds: float) ->ContractContract.max_mean(seconds: float) ->ContractContract.max_bytes(count: int) ->ContractContract.excluding(setup: Callable[[], _T]) ->_T

Each one carries a full docstring: what it states, what every argument means, the edge cases it decides, and a worked call. Read them with help(check.close_to) or in your editor.

A few in use

fromdokimi_assertimportcheckdeftest_shapes(seat):
err=check.raises(seat, lambda: parse("{"), "a truncated body is refused")
check.contains(seat, str(err), "unexpected end", "and it says where")
check.pairwise(seat, timestamps, lambdaa, b: a<=b, "the log is ordered")
check.close_to(seat, elapsed, 1.0, 0.05, "the retry waited about a second")
check.matches(seat, request_id, r"^req_[0-9a-f]{16}$", "the id is well formed")
deftest_cancellation(seat):
# The subject is a coroutine function; the test is not. The# assertion drives the event loop itself, so no async plugin.check.honours_cancellation(seat, worker.run, "the worker stops when told")

Equality

Python's == does not answer what the standard asks:

ExpressionPythonHere
0 == FalseTruenot equal
1 == 1.0Truenot equal
[] == NoneFalsenot equal

bool subclasses int, so 0 == False is true, and numeric types compare across themselves. The standard says values of different types never compare, so this enforces it: type(got) is type(want), not isinstance.

An absent collection does not equal an empty one. Where that difference does not matter, relax the comparison for one call:

fromdokimi_assert.optionimportequate_empty, equate_nanscheck.equal(seat, reply.items, [], "no items came back", equate_empty())

An option applies to the call it is passed to and nothing else. There is no global setting, because a rule changed in one place and read in another is how two tests come to mean different things.

Golden files

fromdokimi_assertimportgoldendeftest_render(seat):
golden.match_at(
seat,
"testdata/report.txt",
render(report),
golden.should_update(),
golden.scrub_timestamps(),
)

Set DOKIMI_ASSERT_UPDATE_GOLDEN=1 to rewrite the files. Read the diff before you do. Scrubbers cover timestamps, hex digests, run ids and named JSON fields, so a value that changes every run does not fail the comparison.

Testing your own assertions

rejects states that a check fails, which is the one thing an assertion library has to be able to say about itself:

deftest_the_validator_refuses_an_empty_name(seat):
check.rejects(
seat,
"an empty name is refused",
lambdainner: check.is_none(inner, validate(""), "it passes"),
)

The recorder fixture is the lower-level version: drive an assertion with it, then read failed and message.

The standard

The assertions are defined in assert-spec, language-neutral, and implemented in several languages. This library vendors the definition and holds itself to it:

  • 87 corpus cases state what each assertion must report, run against both surfaces. They are the same cases every other implementation runs.
  • A completeness gate checks every assertion is present under the name the naming table gives it.
  • An overlay records any assertion this library cannot supply. It is empty: all 41 are implemented.

Where Python differs

Go states cancellation with context.Context, which appears in every signature. Python has no such convention, so honours_cancellation, honours_deadline and no_task_leaks are built on asyncio, whose CancelledError, timeouts and tasks are the real analogue. They take a coroutine function rather than a callable.

completes_within measures rather than interrupts: it reports whether a subject finished in time, and a slow subject runs to completion first.

Allocation ceilings use tracemalloc, which counts what Python itself allocated and carries real overhead. Set those ceilings from a traced run; latency ceilings need no such care.

Development

make install # create the environment
make check # the full pre-merge gate
make test# tests
make fmt # format and autofix
make build # build the sdist and wheel
make spec-sync # refresh the vendored definition

make check runs ruff, a formatting check, mypy strict, basedpyright and the tests with a coverage floor. CI runs it on 3.11 through 3.14. The design is recorded in docs/rfc/0001-the-python-implementation.md.

Pushing a v* tag builds, re-runs the gate, checks the tag agrees with pyproject.toml, and publishes to PyPI through Trusted Publishing.

Licence

MIT. See LICENSE.

About

Test assertions for Python, defined by a language-neutral standard and held to it on every run

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

dokimi-assert

Test assertions for Python, defined by a language-neutral standard and held to it on every run.

CIPyPIPythonLicence

pip install dokimi-assert

The distribution is dokimi-assert; the package you import is dokimi_assert. Python 3.11 and up. No runtime dependencies.

Getting started

Installing the package registers a pytest plugin, so the seat fixture is available with no conftest and nothing to import.

fromdokimi_assertimportcheckdeftest_get(seat):
item=store.get("widget")
check.is_not_none(seat, item, "Get returns the stored item")
check.equal(seat, item.name, "widget", "and the item is the one stored")

Every assertion takes the seat first and a message last. The message states the contract under test and is the first line of the failure:

AssertionError: and the item is the one stored: want 'widget', got 'gadget'

What a seat is

The seat is where a failure goes. Assertions never call pytest and never raise on their own; they report to whatever seat they are handed. That is what lets one assertion serve a real test, a benchmark, and a test that checks the assertion itself.

You will normally use the fixture and not think about it. Three seats exist, and the fixture hands you the first:

Seatcheck doesexpect does
Collector, from the seat fixturestops the testrecords, reported when the body ends
Standardstops the teststops the test
Recorder, from the recorder fixturerecordsrecords

Use Standard() outside pytest, where nothing owns the end of a test. Use Recorder to read back what an assertion reported instead of suffering it.

Two surfaces

check stops at the first failure. expect records and carries on, so one run shows every property that failed.

fromdokimi_assertimportcheck, expectdeftest_reply(seat):
reply=client.fetch(url)
check.equal(seat, reply.status, 200, "the request succeeds")
expect.has_prefix(seat, reply.body, "{", "the body is JSON")
expect.length(seat, reply.items, 3, "every item comes back")
expect.contains(seat, reply.headers, "etag", "the reply is cacheable")

If two of the three expect calls fail, both are reported together:

AssertionError: 2 failures:
1. every item comes back: expected length 3, got 2
2. the reply is cacheable: {'etag': ...} does not contain 'etag'

Use check when nothing after it makes sense, and expect when each line states an independent property. Both carry the same assertions under the same names.

The assertions

Thirty-four in the root namespace on both surfaces, plus three for golden files and five on the benchmark contract.

Every assertion takes the seat first and the message last. check and expect carry the same names and the same signatures; only what happens on a failure differs.

Equality — Structural, and strict about types.

check.equal(seat: Seat, got: Any, want: Any, msg: str, *options: Option)
check.not_equal(seat: Seat, got: Any, want: Any, msg: str, *options: Option)

Truth and absence — The two-value cases.

check.is_true(seat: Seat, condition: bool, msg: str)
check.is_false(seat: Seat, condition: bool, msg: str)
check.is_none(seat: Seat, got: Any, msg: str)
check.is_not_none(seat: Seat, got: Any, msg: str)

Size — Anything with a length.

check.length(seat: Seat, got: Any, want: int, msg: str)
check.is_empty(seat: Seat, got: Any, msg: str)
check.is_not_empty(seat: Seat, got: Any, msg: str)

Containment — What holding means follows the haystack.

check.contains(seat: Seat, haystack: Any, needle: Any, msg: str, *options: Option)
check.not_contains(seat: Seat, haystack: Any, needle: Any, msg: str, *options: Option)
check.contains_in_order(seat: Seat, got: Any, needles: Sequence[str], msg: str)

Text — str and bytes.

check.has_prefix(seat: Seat, got: Any, prefix: str, msg: str)
check.has_suffix(seat: Seat, got: Any, suffix: str, msg: str)
check.matches(seat: Seat, got: Any, pattern: str, msg: str)

Numbers — Where exact equality is the wrong question.

check.close_to(seat: Seat, got: Any, want: float, tolerance: float, msg: str)
check.in_range(seat: Seat, got: Any, low: float, high: float, msg: str)

Ordering — Sorted, unique, and anything else that holds between neighbours.

check.pairwise(seat: Seat, items: Sequence[Any], predicate: Callable[[Any, Any], bool], msg: str)

Errors — For code that hands an error back rather than raising it.

check.no_error(seat: Seat, exc: BaseException|None, msg: str)
check.has_error(seat: Seat, exc: BaseException|None, msg: str)
check.error_is(seat: Seat, exc: BaseException|None, target: BaseException, msg: str)
check.error_is_not(seat: Seat, exc: BaseException|None, target: BaseException, msg: str)
check.error_as(seat: Seat, exc: BaseException|None, want: type[_E], msg: str) ->_E|None

Raising — For code that raises.

check.raises(seat: Seat, fn: Callable[[], Any], msg: str) ->BaseException|Nonecheck.does_not_raise(seat: Seat, fn: Callable[[], Any], msg: str)

Cancellation — asyncio is Python's cancellation model. These run the loop themselves, so your test stays a plain def. That is also the limit: the two that drive a coroutine cannot be called from a test already running a loop, and say so naming the assertion when they are.

check.honours_cancellation(seat: Seat, fn: Callable[[], Awaitable[Any]], msg: str)
check.honours_deadline(seat: Seat, fn: Callable[[], Awaitable[Any]], msg: str)
check.completes_within(seat: Seat, within: float, fn: Callable[[], Any], msg: str)
check.none_handle_safe(seat: Seat, fn: Callable[[Any], Any], msg: str)

Retrying — For a condition something outside the test makes true. Both spend real time.

check.eventually(seat: Seat, timeout: float, interval: float, body: Callable[[Any], None], msg: str)
check.eventually_true(seat: Seat, timeout: float, predicate: Callable[[], bool], msg: str)

Concurrency — Call what it returns where the scope ends.

check.no_task_leaks(seat: Seat, msg: str) ->Callable[[], None]

Purity — What observe returns defines what nothing means.

check.is_pure(seat: Seat, observe: Callable[[], Any], fn: Callable[[], Any], msg: str, *options: Option)

Testing an assertion — On check only: expect cannot drive a check to failure, because it does not stop.

check.rejects(seat: Seat, msg: str, body: Callable[[Recorder], None]) ->str

Golden files — recorded output, compared and rewritable.

golden.match(seat: Seat, name: str, got: str, update: bool, *scrubbers: Scrubber)
golden.match_at(seat: Seat, path: str|Path, got: str, update: bool, *scrubbers: Scrubber)
golden.match_json_field(seat: Seat, path: str|Path, field: str, got: str, update: bool, *scrubbers: Scrubber)
golden.should_update() ->boolgolden.scrub_timestamps() ->Scrubbergolden.scrub_hashes() ->Scrubbergolden.scrub_run_ids() ->Scrubbergolden.scrub_json_fields(*fields: str) ->Scrubber

Benchmark ceilings — chained onto one contract.

Contract.loop(iterations: int) ->Iterator[int]
Contract.max_latency(seconds: float) ->ContractContract.max_mean(seconds: float) ->ContractContract.max_bytes(count: int) ->ContractContract.excluding(setup: Callable[[], _T]) ->_T

Each one carries a full docstring: what it states, what every argument means, the edge cases it decides, and a worked call. Read them with help(check.close_to) or in your editor.

A few in use

fromdokimi_assertimportcheckdeftest_shapes(seat):
err=check.raises(seat, lambda: parse("{"), "a truncated body is refused")
check.contains(seat, str(err), "unexpected end", "and it says where")
check.pairwise(seat, timestamps, lambdaa, b: a<=b, "the log is ordered")
check.close_to(seat, elapsed, 1.0, 0.05, "the retry waited about a second")
check.matches(seat, request_id, r"^req_[0-9a-f]{16}$", "the id is well formed")
deftest_cancellation(seat):
# The subject is a coroutine function; the test is not. The# assertion drives the event loop itself, so no async plugin.check.honours_cancellation(seat, worker.run, "the worker stops when told")

Equality

Python's == does not answer what the standard asks:

ExpressionPythonHere
0 == FalseTruenot equal
1 == 1.0Truenot equal
[] == NoneFalsenot equal

bool subclasses int, so 0 == False is true, and numeric types compare across themselves. The standard says values of different types never compare, so this enforces it: type(got) is type(want), not isinstance.

An absent collection does not equal an empty one. Where that difference does not matter, relax the comparison for one call:

fromdokimi_assert.optionimportequate_empty, equate_nanscheck.equal(seat, reply.items, [], "no items came back", equate_empty())

An option applies to the call it is passed to and nothing else. There is no global setting, because a rule changed in one place and read in another is how two tests come to mean different things.

Golden files

fromdokimi_assertimportgoldendeftest_render(seat):
golden.match_at(
seat,
"testdata/report.txt",
render(report),
golden.should_update(),
golden.scrub_timestamps(),
)

Set DOKIMI_ASSERT_UPDATE_GOLDEN=1 to rewrite the files. Read the diff before you do. Scrubbers cover timestamps, hex digests, run ids and named JSON fields, so a value that changes every run does not fail the comparison.

Testing your own assertions

rejects states that a check fails, which is the one thing an assertion library has to be able to say about itself:

deftest_the_validator_refuses_an_empty_name(seat):
check.rejects(
seat,
"an empty name is refused",
lambdainner: check.is_none(inner, validate(""), "it passes"),
)

The recorder fixture is the lower-level version: drive an assertion with it, then read failed and message.

The standard

The assertions are defined in assert-spec, language-neutral, and implemented in several languages. This library vendors the definition and holds itself to it:

  • 87 corpus cases state what each assertion must report, run against both surfaces. They are the same cases every other implementation runs.
  • A completeness gate checks every assertion is present under the name the naming table gives it.
  • An overlay records any assertion this library cannot supply. It is empty: all 41 are implemented.

Where Python differs

Go states cancellation with context.Context, which appears in every signature. Python has no such convention, so honours_cancellation, honours_deadline and no_task_leaks are built on asyncio, whose CancelledError, timeouts and tasks are the real analogue. They take a coroutine function rather than a callable.

completes_within measures rather than interrupts: it reports whether a subject finished in time, and a slow subject runs to completion first.

Allocation ceilings use tracemalloc, which counts what Python itself allocated and carries real overhead. Set those ceilings from a traced run; latency ceilings need no such care.

Development

make install # create the environment
make check # the full pre-merge gate
make test# tests
make fmt # format and autofix
make build # build the sdist and wheel
make spec-sync # refresh the vendored definition

make check runs ruff, a formatting check, mypy strict, basedpyright and the tests with a coverage floor. CI runs it on 3.11 through 3.14. The design is recorded in docs/rfc/0001-the-python-implementation.md.

Pushing a v* tag builds, re-runs the gate, checks the tag agrees with pyproject.toml, and publishes to PyPI through Trusted Publishing.

Licence

MIT. See LICENSE.

About

Test assertions for Python, defined by a language-neutral standard and held to it on every run

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

dokimi-assert

Test assertions for Python, defined by a language-neutral standard and held to it on every run.

CIPyPIPythonLicence

pip install dokimi-assert

The distribution is dokimi-assert; the package you import is dokimi_assert. Python 3.11 and up. No runtime dependencies.

Getting started

Installing the package registers a pytest plugin, so the seat fixture is available with no conftest and nothing to import.

fromdokimi_assertimportcheckdeftest_get(seat):
item=store.get("widget")
check.is_not_none(seat, item, "Get returns the stored item")
check.equal(seat, item.name, "widget", "and the item is the one stored")

Every assertion takes the seat first and a message last. The message states the contract under test and is the first line of the failure:

AssertionError: and the item is the one stored: want 'widget', got 'gadget'

What a seat is

The seat is where a failure goes. Assertions never call pytest and never raise on their own; they report to whatever seat they are handed. That is what lets one assertion serve a real test, a benchmark, and a test that checks the assertion itself.

You will normally use the fixture and not think about it. Three seats exist, and the fixture hands you the first:

Seatcheck doesexpect does
Collector, from the seat fixturestops the testrecords, reported when the body ends
Standardstops the teststops the test
Recorder, from the recorder fixturerecordsrecords

Use Standard() outside pytest, where nothing owns the end of a test. Use Recorder to read back what an assertion reported instead of suffering it.

Two surfaces

check stops at the first failure. expect records and carries on, so one run shows every property that failed.

fromdokimi_assertimportcheck, expectdeftest_reply(seat):
reply=client.fetch(url)
check.equal(seat, reply.status, 200, "the request succeeds")
expect.has_prefix(seat, reply.body, "{", "the body is JSON")
expect.length(seat, reply.items, 3, "every item comes back")
expect.contains(seat, reply.headers, "etag", "the reply is cacheable")

If two of the three expect calls fail, both are reported together:

AssertionError: 2 failures:
1. every item comes back: expected length 3, got 2
2. the reply is cacheable: {'etag': ...} does not contain 'etag'

Use check when nothing after it makes sense, and expect when each line states an independent property. Both carry the same assertions under the same names.

The assertions

Thirty-four in the root namespace on both surfaces, plus three for golden files and five on the benchmark contract.

Every assertion takes the seat first and the message last. check and expect carry the same names and the same signatures; only what happens on a failure differs.

Equality — Structural, and strict about types.

check.equal(seat: Seat, got: Any, want: Any, msg: str, *options: Option)
check.not_equal(seat: Seat, got: Any, want: Any, msg: str, *options: Option)

Truth and absence — The two-value cases.

check.is_true(seat: Seat, condition: bool, msg: str)
check.is_false(seat: Seat, condition: bool, msg: str)
check.is_none(seat: Seat, got: Any, msg: str)
check.is_not_none(seat: Seat, got: Any, msg: str)

Size — Anything with a length.

check.length(seat: Seat, got: Any, want: int, msg: str)
check.is_empty(seat: Seat, got: Any, msg: str)
check.is_not_empty(seat: Seat, got: Any, msg: str)

Containment — What holding means follows the haystack.

check.contains(seat: Seat, haystack: Any, needle: Any, msg: str, *options: Option)
check.not_contains(seat: Seat, haystack: Any, needle: Any, msg: str, *options: Option)
check.contains_in_order(seat: Seat, got: Any, needles: Sequence[str], msg: str)

Text — str and bytes.

check.has_prefix(seat: Seat, got: Any, prefix: str, msg: str)
check.has_suffix(seat: Seat, got: Any, suffix: str, msg: str)
check.matches(seat: Seat, got: Any, pattern: str, msg: str)

Numbers — Where exact equality is the wrong question.

check.close_to(seat: Seat, got: Any, want: float, tolerance: float, msg: str)
check.in_range(seat: Seat, got: Any, low: float, high: float, msg: str)

Ordering — Sorted, unique, and anything else that holds between neighbours.

check.pairwise(seat: Seat, items: Sequence[Any], predicate: Callable[[Any, Any], bool], msg: str)

Errors — For code that hands an error back rather than raising it.

check.no_error(seat: Seat, exc: BaseException|None, msg: str)
check.has_error(seat: Seat, exc: BaseException|None, msg: str)
check.error_is(seat: Seat, exc: BaseException|None, target: BaseException, msg: str)
check.error_is_not(seat: Seat, exc: BaseException|None, target: BaseException, msg: str)
check.error_as(seat: Seat, exc: BaseException|None, want: type[_E], msg: str) ->_E|None

Raising — For code that raises.

check.raises(seat: Seat, fn: Callable[[], Any], msg: str) ->BaseException|Nonecheck.does_not_raise(seat: Seat, fn: Callable[[], Any], msg: str)

Cancellation — asyncio is Python's cancellation model. These run the loop themselves, so your test stays a plain def. That is also the limit: the two that drive a coroutine cannot be called from a test already running a loop, and say so naming the assertion when they are.

check.honours_cancellation(seat: Seat, fn: Callable[[], Awaitable[Any]], msg: str)
check.honours_deadline(seat: Seat, fn: Callable[[], Awaitable[Any]], msg: str)
check.completes_within(seat: Seat, within: float, fn: Callable[[], Any], msg: str)
check.none_handle_safe(seat: Seat, fn: Callable[[Any], Any], msg: str)

Retrying — For a condition something outside the test makes true. Both spend real time.

check.eventually(seat: Seat, timeout: float, interval: float, body: Callable[[Any], None], msg: str)
check.eventually_true(seat: Seat, timeout: float, predicate: Callable[[], bool], msg: str)

Concurrency — Call what it returns where the scope ends.

check.no_task_leaks(seat: Seat, msg: str) ->Callable[[], None]

Purity — What observe returns defines what nothing means.

check.is_pure(seat: Seat, observe: Callable[[], Any], fn: Callable[[], Any], msg: str, *options: Option)

Testing an assertion — On check only: expect cannot drive a check to failure, because it does not stop.

check.rejects(seat: Seat, msg: str, body: Callable[[Recorder], None]) ->str

Golden files — recorded output, compared and rewritable.

golden.match(seat: Seat, name: str, got: str, update: bool, *scrubbers: Scrubber)
golden.match_at(seat: Seat, path: str|Path, got: str, update: bool, *scrubbers: Scrubber)
golden.match_json_field(seat: Seat, path: str|Path, field: str, got: str, update: bool, *scrubbers: Scrubber)
golden.should_update() ->boolgolden.scrub_timestamps() ->Scrubbergolden.scrub_hashes() ->Scrubbergolden.scrub_run_ids() ->Scrubbergolden.scrub_json_fields(*fields: str) ->Scrubber

Benchmark ceilings — chained onto one contract.

Contract.loop(iterations: int) ->Iterator[int]
Contract.max_latency(seconds: float) ->ContractContract.max_mean(seconds: float) ->ContractContract.max_bytes(count: int) ->ContractContract.excluding(setup: Callable[[], _T]) ->_T

Each one carries a full docstring: what it states, what every argument means, the edge cases it decides, and a worked call. Read them with help(check.close_to) or in your editor.

A few in use

fromdokimi_assertimportcheckdeftest_shapes(seat):
err=check.raises(seat, lambda: parse("{"), "a truncated body is refused")
check.contains(seat, str(err), "unexpected end", "and it says where")
check.pairwise(seat, timestamps, lambdaa, b: a<=b, "the log is ordered")
check.close_to(seat, elapsed, 1.0, 0.05, "the retry waited about a second")
check.matches(seat, request_id, r"^req_[0-9a-f]{16}$", "the id is well formed")
deftest_cancellation(seat):
# The subject is a coroutine function; the test is not. The# assertion drives the event loop itself, so no async plugin.check.honours_cancellation(seat, worker.run, "the worker stops when told")

Equality

Python's == does not answer what the standard asks:

ExpressionPythonHere
0 == FalseTruenot equal
1 == 1.0Truenot equal
[] == NoneFalsenot equal

bool subclasses int, so 0 == False is true, and numeric types compare across themselves. The standard says values of different types never compare, so this enforces it: type(got) is type(want), not isinstance.

An absent collection does not equal an empty one. Where that difference does not matter, relax the comparison for one call:

fromdokimi_assert.optionimportequate_empty, equate_nanscheck.equal(seat, reply.items, [], "no items came back", equate_empty())

An option applies to the call it is passed to and nothing else. There is no global setting, because a rule changed in one place and read in another is how two tests come to mean different things.

Golden files

fromdokimi_assertimportgoldendeftest_render(seat):
golden.match_at(
seat,
"testdata/report.txt",
render(report),
golden.should_update(),
golden.scrub_timestamps(),
)

Set DOKIMI_ASSERT_UPDATE_GOLDEN=1 to rewrite the files. Read the diff before you do. Scrubbers cover timestamps, hex digests, run ids and named JSON fields, so a value that changes every run does not fail the comparison.

Testing your own assertions

rejects states that a check fails, which is the one thing an assertion library has to be able to say about itself:

deftest_the_validator_refuses_an_empty_name(seat):
check.rejects(
seat,
"an empty name is refused",
lambdainner: check.is_none(inner, validate(""), "it passes"),
)

The recorder fixture is the lower-level version: drive an assertion with it, then read failed and message.

The standard

The assertions are defined in assert-spec, language-neutral, and implemented in several languages. This library vendors the definition and holds itself to it:

  • 87 corpus cases state what each assertion must report, run against both surfaces. They are the same cases every other implementation runs.
  • A completeness gate checks every assertion is present under the name the naming table gives it.
  • An overlay records any assertion this library cannot supply. It is empty: all 41 are implemented.

Where Python differs

Go states cancellation with context.Context, which appears in every signature. Python has no such convention, so honours_cancellation, honours_deadline and no_task_leaks are built on asyncio, whose CancelledError, timeouts and tasks are the real analogue. They take a coroutine function rather than a callable.

completes_within measures rather than interrupts: it reports whether a subject finished in time, and a slow subject runs to completion first.

Allocation ceilings use tracemalloc, which counts what Python itself allocated and carries real overhead. Set those ceilings from a traced run; latency ceilings need no such care.

Development

make install # create the environment
make check # the full pre-merge gate
make test# tests
make fmt # format and autofix
make build # build the sdist and wheel
make spec-sync # refresh the vendored definition

make check runs ruff, a formatting check, mypy strict, basedpyright and the tests with a coverage floor. CI runs it on 3.11 through 3.14. The design is recorded in docs/rfc/0001-the-python-implementation.md.

Pushing a v* tag builds, re-runs the gate, checks the tag agrees with pyproject.toml, and publishes to PyPI through Trusted Publishing.

Licence

MIT. See LICENSE.

About

Test assertions for Python, defined by a language-neutral standard and held to it on every run

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

dokimi-assert

Test assertions for Python, defined by a language-neutral standard and held to it on every run.

CIPyPIPythonLicence

pip install dokimi-assert

The distribution is dokimi-assert; the package you import is dokimi_assert. Python 3.11 and up. No runtime dependencies.

Getting started

Installing the package registers a pytest plugin, so the seat fixture is available with no conftest and nothing to import.

fromdokimi_assertimportcheckdeftest_get(seat):
item=store.get("widget")
check.is_not_none(seat, item, "Get returns the stored item")
check.equal(seat, item.name, "widget", "and the item is the one stored")

Every assertion takes the seat first and a message last. The message states the contract under test and is the first line of the failure:

AssertionError: and the item is the one stored: want 'widget', got 'gadget'

What a seat is

The seat is where a failure goes. Assertions never call pytest and never raise on their own; they report to whatever seat they are handed. That is what lets one assertion serve a real test, a benchmark, and a test that checks the assertion itself.

You will normally use the fixture and not think about it. Three seats exist, and the fixture hands you the first:

Seatcheck doesexpect does
Collector, from the seat fixturestops the testrecords, reported when the body ends
Standardstops the teststops the test
Recorder, from the recorder fixturerecordsrecords

Use Standard() outside pytest, where nothing owns the end of a test. Use Recorder to read back what an assertion reported instead of suffering it.

Two surfaces

check stops at the first failure. expect records and carries on, so one run shows every property that failed.

fromdokimi_assertimportcheck, expectdeftest_reply(seat):
reply=client.fetch(url)
check.equal(seat, reply.status, 200, "the request succeeds")
expect.has_prefix(seat, reply.body, "{", "the body is JSON")
expect.length(seat, reply.items, 3, "every item comes back")
expect.contains(seat, reply.headers, "etag", "the reply is cacheable")

If two of the three expect calls fail, both are reported together:

AssertionError: 2 failures:
1. every item comes back: expected length 3, got 2
2. the reply is cacheable: {'etag': ...} does not contain 'etag'

Use check when nothing after it makes sense, and expect when each line states an independent property. Both carry the same assertions under the same names.

The assertions

Thirty-four in the root namespace on both surfaces, plus three for golden files and five on the benchmark contract.

Every assertion takes the seat first and the message last. check and expect carry the same names and the same signatures; only what happens on a failure differs.

Equality — Structural, and strict about types.

check.equal(seat: Seat, got: Any, want: Any, msg: str, *options: Option)
check.not_equal(seat: Seat, got: Any, want: Any, msg: str, *options: Option)

Truth and absence — The two-value cases.

check.is_true(seat: Seat, condition: bool, msg: str)
check.is_false(seat: Seat, condition: bool, msg: str)
check.is_none(seat: Seat, got: Any, msg: str)
check.is_not_none(seat: Seat, got: Any, msg: str)

Size — Anything with a length.

check.length(seat: Seat, got: Any, want: int, msg: str)
check.is_empty(seat: Seat, got: Any, msg: str)
check.is_not_empty(seat: Seat, got: Any, msg: str)

Containment — What holding means follows the haystack.

check.contains(seat: Seat, haystack: Any, needle: Any, msg: str, *options: Option)
check.not_contains(seat: Seat, haystack: Any, needle: Any, msg: str, *options: Option)
check.contains_in_order(seat: Seat, got: Any, needles: Sequence[str], msg: str)

Text — str and bytes.

check.has_prefix(seat: Seat, got: Any, prefix: str, msg: str)
check.has_suffix(seat: Seat, got: Any, suffix: str, msg: str)
check.matches(seat: Seat, got: Any, pattern: str, msg: str)

Numbers — Where exact equality is the wrong question.

check.close_to(seat: Seat, got: Any, want: float, tolerance: float, msg: str)
check.in_range(seat: Seat, got: Any, low: float, high: float, msg: str)

Ordering — Sorted, unique, and anything else that holds between neighbours.

check.pairwise(seat: Seat, items: Sequence[Any], predicate: Callable[[Any, Any], bool], msg: str)

Errors — For code that hands an error back rather than raising it.

check.no_error(seat: Seat, exc: BaseException|None, msg: str)
check.has_error(seat: Seat, exc: BaseException|None, msg: str)
check.error_is(seat: Seat, exc: BaseException|None, target: BaseException, msg: str)
check.error_is_not(seat: Seat, exc: BaseException|None, target: BaseException, msg: str)
check.error_as(seat: Seat, exc: BaseException|None, want: type[_E], msg: str) ->_E|None

Raising — For code that raises.

check.raises(seat: Seat, fn: Callable[[], Any], msg: str) ->BaseException|Nonecheck.does_not_raise(seat: Seat, fn: Callable[[], Any], msg: str)

Cancellation — asyncio is Python's cancellation model. These run the loop themselves, so your test stays a plain def. That is also the limit: the two that drive a coroutine cannot be called from a test already running a loop, and say so naming the assertion when they are.

check.honours_cancellation(seat: Seat, fn: Callable[[], Awaitable[Any]], msg: str)
check.honours_deadline(seat: Seat, fn: Callable[[], Awaitable[Any]], msg: str)
check.completes_within(seat: Seat, within: float, fn: Callable[[], Any], msg: str)
check.none_handle_safe(seat: Seat, fn: Callable[[Any], Any], msg: str)

Retrying — For a condition something outside the test makes true. Both spend real time.

check.eventually(seat: Seat, timeout: float, interval: float, body: Callable[[Any], None], msg: str)
check.eventually_true(seat: Seat, timeout: float, predicate: Callable[[], bool], msg: str)

Concurrency — Call what it returns where the scope ends.

check.no_task_leaks(seat: Seat, msg: str) ->Callable[[], None]

Purity — What observe returns defines what nothing means.

check.is_pure(seat: Seat, observe: Callable[[], Any], fn: Callable[[], Any], msg: str, *options: Option)

Testing an assertion — On check only: expect cannot drive a check to failure, because it does not stop.

check.rejects(seat: Seat, msg: str, body: Callable[[Recorder], None]) ->str

Golden files — recorded output, compared and rewritable.

golden.match(seat: Seat, name: str, got: str, update: bool, *scrubbers: Scrubber)
golden.match_at(seat: Seat, path: str|Path, got: str, update: bool, *scrubbers: Scrubber)
golden.match_json_field(seat: Seat, path: str|Path, field: str, got: str, update: bool, *scrubbers: Scrubber)
golden.should_update() ->boolgolden.scrub_timestamps() ->Scrubbergolden.scrub_hashes() ->Scrubbergolden.scrub_run_ids() ->Scrubbergolden.scrub_json_fields(*fields: str) ->Scrubber

Benchmark ceilings — chained onto one contract.

Contract.loop(iterations: int) ->Iterator[int]
Contract.max_latency(seconds: float) ->ContractContract.max_mean(seconds: float) ->ContractContract.max_bytes(count: int) ->ContractContract.excluding(setup: Callable[[], _T]) ->_T

Each one carries a full docstring: what it states, what every argument means, the edge cases it decides, and a worked call. Read them with help(check.close_to) or in your editor.

A few in use

fromdokimi_assertimportcheckdeftest_shapes(seat):
err=check.raises(seat, lambda: parse("{"), "a truncated body is refused")
check.contains(seat, str(err), "unexpected end", "and it says where")
check.pairwise(seat, timestamps, lambdaa, b: a<=b, "the log is ordered")
check.close_to(seat, elapsed, 1.0, 0.05, "the retry waited about a second")
check.matches(seat, request_id, r"^req_[0-9a-f]{16}$", "the id is well formed")
deftest_cancellation(seat):
# The subject is a coroutine function; the test is not. The# assertion drives the event loop itself, so no async plugin.check.honours_cancellation(seat, worker.run, "the worker stops when told")

Equality

Python's == does not answer what the standard asks:

ExpressionPythonHere
0 == FalseTruenot equal
1 == 1.0Truenot equal
[] == NoneFalsenot equal

bool subclasses int, so 0 == False is true, and numeric types compare across themselves. The standard says values of different types never compare, so this enforces it: type(got) is type(want), not isinstance.

An absent collection does not equal an empty one. Where that difference does not matter, relax the comparison for one call:

fromdokimi_assert.optionimportequate_empty, equate_nanscheck.equal(seat, reply.items, [], "no items came back", equate_empty())

An option applies to the call it is passed to and nothing else. There is no global setting, because a rule changed in one place and read in another is how two tests come to mean different things.

Golden files

fromdokimi_assertimportgoldendeftest_render(seat):
golden.match_at(
seat,
"testdata/report.txt",
render(report),
golden.should_update(),
golden.scrub_timestamps(),
)

Set DOKIMI_ASSERT_UPDATE_GOLDEN=1 to rewrite the files. Read the diff before you do. Scrubbers cover timestamps, hex digests, run ids and named JSON fields, so a value that changes every run does not fail the comparison.

Testing your own assertions

rejects states that a check fails, which is the one thing an assertion library has to be able to say about itself:

deftest_the_validator_refuses_an_empty_name(seat):
check.rejects(
seat,
"an empty name is refused",
lambdainner: check.is_none(inner, validate(""), "it passes"),
)

The recorder fixture is the lower-level version: drive an assertion with it, then read failed and message.

The standard

The assertions are defined in assert-spec, language-neutral, and implemented in several languages. This library vendors the definition and holds itself to it:

  • 87 corpus cases state what each assertion must report, run against both surfaces. They are the same cases every other implementation runs.
  • A completeness gate checks every assertion is present under the name the naming table gives it.
  • An overlay records any assertion this library cannot supply. It is empty: all 41 are implemented.

Where Python differs

Go states cancellation with context.Context, which appears in every signature. Python has no such convention, so honours_cancellation, honours_deadline and no_task_leaks are built on asyncio, whose CancelledError, timeouts and tasks are the real analogue. They take a coroutine function rather than a callable.

completes_within measures rather than interrupts: it reports whether a subject finished in time, and a slow subject runs to completion first.

Allocation ceilings use tracemalloc, which counts what Python itself allocated and carries real overhead. Set those ceilings from a traced run; latency ceilings need no such care.

Development

make install # create the environment
make check # the full pre-merge gate
make test# tests
make fmt # format and autofix
make build # build the sdist and wheel
make spec-sync # refresh the vendored definition

make check runs ruff, a formatting check, mypy strict, basedpyright and the tests with a coverage floor. CI runs it on 3.11 through 3.14. The design is recorded in docs/rfc/0001-the-python-implementation.md.

Pushing a v* tag builds, re-runs the gate, checks the tag agrees with pyproject.toml, and publishes to PyPI through Trusted Publishing.

Licence

MIT. See LICENSE.

About

Test assertions for Python, defined by a language-neutral standard and held to it on every run

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

dokimi-assert

Test assertions for Python, defined by a language-neutral standard and held to it on every run.

CIPyPIPythonLicence

pip install dokimi-assert

The distribution is dokimi-assert; the package you import is dokimi_assert. Python 3.11 and up. No runtime dependencies.

Getting started

Installing the package registers a pytest plugin, so the seat fixture is available with no conftest and nothing to import.

fromdokimi_assertimportcheckdeftest_get(seat):
item=store.get("widget")
check.is_not_none(seat, item, "Get returns the stored item")
check.equal(seat, item.name, "widget", "and the item is the one stored")

Every assertion takes the seat first and a message last. The message states the contract under test and is the first line of the failure:

AssertionError: and the item is the one stored: want 'widget', got 'gadget'

What a seat is

The seat is where a failure goes. Assertions never call pytest and never raise on their own; they report to whatever seat they are handed. That is what lets one assertion serve a real test, a benchmark, and a test that checks the assertion itself.

You will normally use the fixture and not think about it. Three seats exist, and the fixture hands you the first:

Seatcheck doesexpect does
Collector, from the seat fixturestops the testrecords, reported when the body ends
Standardstops the teststops the test
Recorder, from the recorder fixturerecordsrecords

Use Standard() outside pytest, where nothing owns the end of a test. Use Recorder to read back what an assertion reported instead of suffering it.

Two surfaces

check stops at the first failure. expect records and carries on, so one run shows every property that failed.

fromdokimi_assertimportcheck, expectdeftest_reply(seat):
reply=client.fetch(url)
check.equal(seat, reply.status, 200, "the request succeeds")
expect.has_prefix(seat, reply.body, "{", "the body is JSON")
expect.length(seat, reply.items, 3, "every item comes back")
expect.contains(seat, reply.headers, "etag", "the reply is cacheable")

If two of the three expect calls fail, both are reported together:

AssertionError: 2 failures:
1. every item comes back: expected length 3, got 2
2. the reply is cacheable: {'etag': ...} does not contain 'etag'

Use check when nothing after it makes sense, and expect when each line states an independent property. Both carry the same assertions under the same names.

The assertions

Thirty-four in the root namespace on both surfaces, plus three for golden files and five on the benchmark contract.

Every assertion takes the seat first and the message last. check and expect carry the same names and the same signatures; only what happens on a failure differs.

Equality — Structural, and strict about types.

check.equal(seat: Seat, got: Any, want: Any, msg: str, *options: Option)
check.not_equal(seat: Seat, got: Any, want: Any, msg: str, *options: Option)

Truth and absence — The two-value cases.

check.is_true(seat: Seat, condition: bool, msg: str)
check.is_false(seat: Seat, condition: bool, msg: str)
check.is_none(seat: Seat, got: Any, msg: str)
check.is_not_none(seat: Seat, got: Any, msg: str)

Size — Anything with a length.

check.length(seat: Seat, got: Any, want: int, msg: str)
check.is_empty(seat: Seat, got: Any, msg: str)
check.is_not_empty(seat: Seat, got: Any, msg: str)

Containment — What holding means follows the haystack.

check.contains(seat: Seat, haystack: Any, needle: Any, msg: str, *options: Option)
check.not_contains(seat: Seat, haystack: Any, needle: Any, msg: str, *options: Option)
check.contains_in_order(seat: Seat, got: Any, needles: Sequence[str], msg: str)

Text — str and bytes.

check.has_prefix(seat: Seat, got: Any, prefix: str, msg: str)
check.has_suffix(seat: Seat, got: Any, suffix: str, msg: str)
check.matches(seat: Seat, got: Any, pattern: str, msg: str)

Numbers — Where exact equality is the wrong question.

check.close_to(seat: Seat, got: Any, want: float, tolerance: float, msg: str)
check.in_range(seat: Seat, got: Any, low: float, high: float, msg: str)

Ordering — Sorted, unique, and anything else that holds between neighbours.

check.pairwise(seat: Seat, items: Sequence[Any], predicate: Callable[[Any, Any], bool], msg: str)

Errors — For code that hands an error back rather than raising it.

check.no_error(seat: Seat, exc: BaseException|None, msg: str)
check.has_error(seat: Seat, exc: BaseException|None, msg: str)
check.error_is(seat: Seat, exc: BaseException|None, target: BaseException, msg: str)
check.error_is_not(seat: Seat, exc: BaseException|None, target: BaseException, msg: str)
check.error_as(seat: Seat, exc: BaseException|None, want: type[_E], msg: str) ->_E|None

Raising — For code that raises.

check.raises(seat: Seat, fn: Callable[[], Any], msg: str) ->BaseException|Nonecheck.does_not_raise(seat: Seat, fn: Callable[[], Any], msg: str)

Cancellation — asyncio is Python's cancellation model. These run the loop themselves, so your test stays a plain def. That is also the limit: the two that drive a coroutine cannot be called from a test already running a loop, and say so naming the assertion when they are.

check.honours_cancellation(seat: Seat, fn: Callable[[], Awaitable[Any]], msg: str)
check.honours_deadline(seat: Seat, fn: Callable[[], Awaitable[Any]], msg: str)
check.completes_within(seat: Seat, within: float, fn: Callable[[], Any], msg: str)
check.none_handle_safe(seat: Seat, fn: Callable[[Any], Any], msg: str)

Retrying — For a condition something outside the test makes true. Both spend real time.

check.eventually(seat: Seat, timeout: float, interval: float, body: Callable[[Any], None], msg: str)
check.eventually_true(seat: Seat, timeout: float, predicate: Callable[[], bool], msg: str)

Concurrency — Call what it returns where the scope ends.

check.no_task_leaks(seat: Seat, msg: str) ->Callable[[], None]

Purity — What observe returns defines what nothing means.

check.is_pure(seat: Seat, observe: Callable[[], Any], fn: Callable[[], Any], msg: str, *options: Option)

Testing an assertion — On check only: expect cannot drive a check to failure, because it does not stop.

check.rejects(seat: Seat, msg: str, body: Callable[[Recorder], None]) ->str

Golden files — recorded output, compared and rewritable.

golden.match(seat: Seat, name: str, got: str, update: bool, *scrubbers: Scrubber)
golden.match_at(seat: Seat, path: str|Path, got: str, update: bool, *scrubbers: Scrubber)
golden.match_json_field(seat: Seat, path: str|Path, field: str, got: str, update: bool, *scrubbers: Scrubber)
golden.should_update() ->boolgolden.scrub_timestamps() ->Scrubbergolden.scrub_hashes() ->Scrubbergolden.scrub_run_ids() ->Scrubbergolden.scrub_json_fields(*fields: str) ->Scrubber

Benchmark ceilings — chained onto one contract.

Contract.loop(iterations: int) ->Iterator[int]
Contract.max_latency(seconds: float) ->ContractContract.max_mean(seconds: float) ->ContractContract.max_bytes(count: int) ->ContractContract.excluding(setup: Callable[[], _T]) ->_T

Each one carries a full docstring: what it states, what every argument means, the edge cases it decides, and a worked call. Read them with help(check.close_to) or in your editor.

A few in use

fromdokimi_assertimportcheckdeftest_shapes(seat):
err=check.raises(seat, lambda: parse("{"), "a truncated body is refused")
check.contains(seat, str(err), "unexpected end", "and it says where")
check.pairwise(seat, timestamps, lambdaa, b: a<=b, "the log is ordered")
check.close_to(seat, elapsed, 1.0, 0.05, "the retry waited about a second")
check.matches(seat, request_id, r"^req_[0-9a-f]{16}$", "the id is well formed")
deftest_cancellation(seat):
# The subject is a coroutine function; the test is not. The# assertion drives the event loop itself, so no async plugin.check.honours_cancellation(seat, worker.run, "the worker stops when told")

Equality

Python's == does not answer what the standard asks:

ExpressionPythonHere
0 == FalseTruenot equal
1 == 1.0Truenot equal
[] == NoneFalsenot equal

bool subclasses int, so 0 == False is true, and numeric types compare across themselves. The standard says values of different types never compare, so this enforces it: type(got) is type(want), not isinstance.

An absent collection does not equal an empty one. Where that difference does not matter, relax the comparison for one call:

fromdokimi_assert.optionimportequate_empty, equate_nanscheck.equal(seat, reply.items, [], "no items came back", equate_empty())

An option applies to the call it is passed to and nothing else. There is no global setting, because a rule changed in one place and read in another is how two tests come to mean different things.

Golden files

fromdokimi_assertimportgoldendeftest_render(seat):
golden.match_at(
seat,
"testdata/report.txt",
render(report),
golden.should_update(),
golden.scrub_timestamps(),
)

Set DOKIMI_ASSERT_UPDATE_GOLDEN=1 to rewrite the files. Read the diff before you do. Scrubbers cover timestamps, hex digests, run ids and named JSON fields, so a value that changes every run does not fail the comparison.

Testing your own assertions

rejects states that a check fails, which is the one thing an assertion library has to be able to say about itself:

deftest_the_validator_refuses_an_empty_name(seat):
check.rejects(
seat,
"an empty name is refused",
lambdainner: check.is_none(inner, validate(""), "it passes"),
)

The recorder fixture is the lower-level version: drive an assertion with it, then read failed and message.

The standard

The assertions are defined in assert-spec, language-neutral, and implemented in several languages. This library vendors the definition and holds itself to it:

  • 87 corpus cases state what each assertion must report, run against both surfaces. They are the same cases every other implementation runs.
  • A completeness gate checks every assertion is present under the name the naming table gives it.
  • An overlay records any assertion this library cannot supply. It is empty: all 41 are implemented.

Where Python differs

Go states cancellation with context.Context, which appears in every signature. Python has no such convention, so honours_cancellation, honours_deadline and no_task_leaks are built on asyncio, whose CancelledError, timeouts and tasks are the real analogue. They take a coroutine function rather than a callable.

completes_within measures rather than interrupts: it reports whether a subject finished in time, and a slow subject runs to completion first.

Allocation ceilings use tracemalloc, which counts what Python itself allocated and carries real overhead. Set those ceilings from a traced run; latency ceilings need no such care.

Development

make install # create the environment
make check # the full pre-merge gate
make test# tests
make fmt # format and autofix
make build # build the sdist and wheel
make spec-sync # refresh the vendored definition

make check runs ruff, a formatting check, mypy strict, basedpyright and the tests with a coverage floor. CI runs it on 3.11 through 3.14. The design is recorded in docs/rfc/0001-the-python-implementation.md.

Pushing a v* tag builds, re-runs the gate, checks the tag agrees with pyproject.toml, and publishes to PyPI through Trusted Publishing.

Licence

MIT. See LICENSE.

About

Test assertions for Python, defined by a language-neutral standard and held to it on every run

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

dokimi-assert

Test assertions for Python, defined by a language-neutral standard and held to it on every run.

CIPyPIPythonLicence

pip install dokimi-assert

The distribution is dokimi-assert; the package you import is dokimi_assert. Python 3.11 and up. No runtime dependencies.

Getting started

Installing the package registers a pytest plugin, so the seat fixture is available with no conftest and nothing to import.

fromdokimi_assertimportcheckdeftest_get(seat):
item=store.get("widget")
check.is_not_none(seat, item, "Get returns the stored item")
check.equal(seat, item.name, "widget", "and the item is the one stored")

Every assertion takes the seat first and a message last. The message states the contract under test and is the first line of the failure:

AssertionError: and the item is the one stored: want 'widget', got 'gadget'

What a seat is

The seat is where a failure goes. Assertions never call pytest and never raise on their own; they report to whatever seat they are handed. That is what lets one assertion serve a real test, a benchmark, and a test that checks the assertion itself.

You will normally use the fixture and not think about it. Three seats exist, and the fixture hands you the first:

Seatcheck doesexpect does
Collector, from the seat fixturestops the testrecords, reported when the body ends
Standardstops the teststops the test
Recorder, from the recorder fixturerecordsrecords

Use Standard() outside pytest, where nothing owns the end of a test. Use Recorder to read back what an assertion reported instead of suffering it.

Two surfaces

check stops at the first failure. expect records and carries on, so one run shows every property that failed.

fromdokimi_assertimportcheck, expectdeftest_reply(seat):
reply=client.fetch(url)
check.equal(seat, reply.status, 200, "the request succeeds")
expect.has_prefix(seat, reply.body, "{", "the body is JSON")
expect.length(seat, reply.items, 3, "every item comes back")
expect.contains(seat, reply.headers, "etag", "the reply is cacheable")

If two of the three expect calls fail, both are reported together:

AssertionError: 2 failures:
1. every item comes back: expected length 3, got 2
2. the reply is cacheable: {'etag': ...} does not contain 'etag'

Use check when nothing after it makes sense, and expect when each line states an independent property. Both carry the same assertions under the same names.

The assertions

Thirty-four in the root namespace on both surfaces, plus three for golden files and five on the benchmark contract.

Every assertion takes the seat first and the message last. check and expect carry the same names and the same signatures; only what happens on a failure differs.

Equality — Structural, and strict about types.

check.equal(seat: Seat, got: Any, want: Any, msg: str, *options: Option)
check.not_equal(seat: Seat, got: Any, want: Any, msg: str, *options: Option)

Truth and absence — The two-value cases.

check.is_true(seat: Seat, condition: bool, msg: str)
check.is_false(seat: Seat, condition: bool, msg: str)
check.is_none(seat: Seat, got: Any, msg: str)
check.is_not_none(seat: Seat, got: Any, msg: str)

Size — Anything with a length.

check.length(seat: Seat, got: Any, want: int, msg: str)
check.is_empty(seat: Seat, got: Any, msg: str)
check.is_not_empty(seat: Seat, got: Any, msg: str)

Containment — What holding means follows the haystack.

check.contains(seat: Seat, haystack: Any, needle: Any, msg: str, *options: Option)
check.not_contains(seat: Seat, haystack: Any, needle: Any, msg: str, *options: Option)
check.contains_in_order(seat: Seat, got: Any, needles: Sequence[str], msg: str)

Text — str and bytes.

check.has_prefix(seat: Seat, got: Any, prefix: str, msg: str)
check.has_suffix(seat: Seat, got: Any, suffix: str, msg: str)
check.matches(seat: Seat, got: Any, pattern: str, msg: str)

Numbers — Where exact equality is the wrong question.

check.close_to(seat: Seat, got: Any, want: float, tolerance: float, msg: str)
check.in_range(seat: Seat, got: Any, low: float, high: float, msg: str)

Ordering — Sorted, unique, and anything else that holds between neighbours.

check.pairwise(seat: Seat, items: Sequence[Any], predicate: Callable[[Any, Any], bool], msg: str)

Errors — For code that hands an error back rather than raising it.

check.no_error(seat: Seat, exc: BaseException|None, msg: str)
check.has_error(seat: Seat, exc: BaseException|None, msg: str)
check.error_is(seat: Seat, exc: BaseException|None, target: BaseException, msg: str)
check.error_is_not(seat: Seat, exc: BaseException|None, target: BaseException, msg: str)
check.error_as(seat: Seat, exc: BaseException|None, want: type[_E], msg: str) ->_E|None

Raising — For code that raises.

check.raises(seat: Seat, fn: Callable[[], Any], msg: str) ->BaseException|Nonecheck.does_not_raise(seat: Seat, fn: Callable[[], Any], msg: str)

Cancellation — asyncio is Python's cancellation model. These run the loop themselves, so your test stays a plain def. That is also the limit: the two that drive a coroutine cannot be called from a test already running a loop, and say so naming the assertion when they are.

check.honours_cancellation(seat: Seat, fn: Callable[[], Awaitable[Any]], msg: str)
check.honours_deadline(seat: Seat, fn: Callable[[], Awaitable[Any]], msg: str)
check.completes_within(seat: Seat, within: float, fn: Callable[[], Any], msg: str)
check.none_handle_safe(seat: Seat, fn: Callable[[Any], Any], msg: str)

Retrying — For a condition something outside the test makes true. Both spend real time.

check.eventually(seat: Seat, timeout: float, interval: float, body: Callable[[Any], None], msg: str)
check.eventually_true(seat: Seat, timeout: float, predicate: Callable[[], bool], msg: str)

Concurrency — Call what it returns where the scope ends.

check.no_task_leaks(seat: Seat, msg: str) ->Callable[[], None]

Purity — What observe returns defines what nothing means.

check.is_pure(seat: Seat, observe: Callable[[], Any], fn: Callable[[], Any], msg: str, *options: Option)

Testing an assertion — On check only: expect cannot drive a check to failure, because it does not stop.

check.rejects(seat: Seat, msg: str, body: Callable[[Recorder], None]) ->str

Golden files — recorded output, compared and rewritable.

golden.match(seat: Seat, name: str, got: str, update: bool, *scrubbers: Scrubber)
golden.match_at(seat: Seat, path: str|Path, got: str, update: bool, *scrubbers: Scrubber)
golden.match_json_field(seat: Seat, path: str|Path, field: str, got: str, update: bool, *scrubbers: Scrubber)
golden.should_update() ->boolgolden.scrub_timestamps() ->Scrubbergolden.scrub_hashes() ->Scrubbergolden.scrub_run_ids() ->Scrubbergolden.scrub_json_fields(*fields: str) ->Scrubber

Benchmark ceilings — chained onto one contract.

Contract.loop(iterations: int) ->Iterator[int]
Contract.max_latency(seconds: float) ->ContractContract.max_mean(seconds: float) ->ContractContract.max_bytes(count: int) ->ContractContract.excluding(setup: Callable[[], _T]) ->_T

Each one carries a full docstring: what it states, what every argument means, the edge cases it decides, and a worked call. Read them with help(check.close_to) or in your editor.

A few in use

fromdokimi_assertimportcheckdeftest_shapes(seat):
err=check.raises(seat, lambda: parse("{"), "a truncated body is refused")
check.contains(seat, str(err), "unexpected end", "and it says where")
check.pairwise(seat, timestamps, lambdaa, b: a<=b, "the log is ordered")
check.close_to(seat, elapsed, 1.0, 0.05, "the retry waited about a second")
check.matches(seat, request_id, r"^req_[0-9a-f]{16}$", "the id is well formed")
deftest_cancellation(seat):
# The subject is a coroutine function; the test is not. The# assertion drives the event loop itself, so no async plugin.check.honours_cancellation(seat, worker.run, "the worker stops when told")

Equality

Python's == does not answer what the standard asks:

ExpressionPythonHere
0 == FalseTruenot equal
1 == 1.0Truenot equal
[] == NoneFalsenot equal

bool subclasses int, so 0 == False is true, and numeric types compare across themselves. The standard says values of different types never compare, so this enforces it: type(got) is type(want), not isinstance.

An absent collection does not equal an empty one. Where that difference does not matter, relax the comparison for one call:

fromdokimi_assert.optionimportequate_empty, equate_nanscheck.equal(seat, reply.items, [], "no items came back", equate_empty())

An option applies to the call it is passed to and nothing else. There is no global setting, because a rule changed in one place and read in another is how two tests come to mean different things.

Golden files

fromdokimi_assertimportgoldendeftest_render(seat):
golden.match_at(
seat,
"testdata/report.txt",
render(report),
golden.should_update(),
golden.scrub_timestamps(),
)

Set DOKIMI_ASSERT_UPDATE_GOLDEN=1 to rewrite the files. Read the diff before you do. Scrubbers cover timestamps, hex digests, run ids and named JSON fields, so a value that changes every run does not fail the comparison.

Testing your own assertions

rejects states that a check fails, which is the one thing an assertion library has to be able to say about itself:

deftest_the_validator_refuses_an_empty_name(seat):
check.rejects(
seat,
"an empty name is refused",
lambdainner: check.is_none(inner, validate(""), "it passes"),
)

The recorder fixture is the lower-level version: drive an assertion with it, then read failed and message.

The standard

The assertions are defined in assert-spec, language-neutral, and implemented in several languages. This library vendors the definition and holds itself to it:

  • 87 corpus cases state what each assertion must report, run against both surfaces. They are the same cases every other implementation runs.
  • A completeness gate checks every assertion is present under the name the naming table gives it.
  • An overlay records any assertion this library cannot supply. It is empty: all 41 are implemented.

Where Python differs

Go states cancellation with context.Context, which appears in every signature. Python has no such convention, so honours_cancellation, honours_deadline and no_task_leaks are built on asyncio, whose CancelledError, timeouts and tasks are the real analogue. They take a coroutine function rather than a callable.

completes_within measures rather than interrupts: it reports whether a subject finished in time, and a slow subject runs to completion first.

Allocation ceilings use tracemalloc, which counts what Python itself allocated and carries real overhead. Set those ceilings from a traced run; latency ceilings need no such care.

Development

make install # create the environment
make check # the full pre-merge gate
make test# tests
make fmt # format and autofix
make build # build the sdist and wheel
make spec-sync # refresh the vendored definition

make check runs ruff, a formatting check, mypy strict, basedpyright and the tests with a coverage floor. CI runs it on 3.11 through 3.14. The design is recorded in docs/rfc/0001-the-python-implementation.md.

Pushing a v* tag builds, re-runs the gate, checks the tag agrees with pyproject.toml, and publishes to PyPI through Trusted Publishing.

Licence

MIT. See LICENSE.

About

Test assertions for Python, defined by a language-neutral standard and held to it on every run

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

dokimi-assert

Test assertions for Python, defined by a language-neutral standard and held to it on every run.

CIPyPIPythonLicence

pip install dokimi-assert

The distribution is dokimi-assert; the package you import is dokimi_assert. Python 3.11 and up. No runtime dependencies.

Getting started

Installing the package registers a pytest plugin, so the seat fixture is available with no conftest and nothing to import.

fromdokimi_assertimportcheckdeftest_get(seat):
item=store.get("widget")
check.is_not_none(seat, item, "Get returns the stored item")
check.equal(seat, item.name, "widget", "and the item is the one stored")

Every assertion takes the seat first and a message last. The message states the contract under test and is the first line of the failure:

AssertionError: and the item is the one stored: want 'widget', got 'gadget'

What a seat is

The seat is where a failure goes. Assertions never call pytest and never raise on their own; they report to whatever seat they are handed. That is what lets one assertion serve a real test, a benchmark, and a test that checks the assertion itself.

You will normally use the fixture and not think about it. Three seats exist, and the fixture hands you the first:

Seatcheck doesexpect does
Collector, from the seat fixturestops the testrecords, reported when the body ends
Standardstops the teststops the test
Recorder, from the recorder fixturerecordsrecords

Use Standard() outside pytest, where nothing owns the end of a test. Use Recorder to read back what an assertion reported instead of suffering it.

Two surfaces

check stops at the first failure. expect records and carries on, so one run shows every property that failed.

fromdokimi_assertimportcheck, expectdeftest_reply(seat):
reply=client.fetch(url)
check.equal(seat, reply.status, 200, "the request succeeds")
expect.has_prefix(seat, reply.body, "{", "the body is JSON")
expect.length(seat, reply.items, 3, "every item comes back")
expect.contains(seat, reply.headers, "etag", "the reply is cacheable")

If two of the three expect calls fail, both are reported together:

AssertionError: 2 failures:
1. every item comes back: expected length 3, got 2
2. the reply is cacheable: {'etag': ...} does not contain 'etag'

Use check when nothing after it makes sense, and expect when each line states an independent property. Both carry the same assertions under the same names.

The assertions

Thirty-four in the root namespace on both surfaces, plus three for golden files and five on the benchmark contract.

Every assertion takes the seat first and the message last. check and expect carry the same names and the same signatures; only what happens on a failure differs.

Equality — Structural, and strict about types.

check.equal(seat: Seat, got: Any, want: Any, msg: str, *options: Option)
check.not_equal(seat: Seat, got: Any, want: Any, msg: str, *options: Option)

Truth and absence — The two-value cases.

check.is_true(seat: Seat, condition: bool, msg: str)
check.is_false(seat: Seat, condition: bool, msg: str)
check.is_none(seat: Seat, got: Any, msg: str)
check.is_not_none(seat: Seat, got: Any, msg: str)

Size — Anything with a length.

check.length(seat: Seat, got: Any, want: int, msg: str)
check.is_empty(seat: Seat, got: Any, msg: str)
check.is_not_empty(seat: Seat, got: Any, msg: str)

Containment — What holding means follows the haystack.

check.contains(seat: Seat, haystack: Any, needle: Any, msg: str, *options: Option)
check.not_contains(seat: Seat, haystack: Any, needle: Any, msg: str, *options: Option)
check.contains_in_order(seat: Seat, got: Any, needles: Sequence[str], msg: str)

Text — str and bytes.

check.has_prefix(seat: Seat, got: Any, prefix: str, msg: str)
check.has_suffix(seat: Seat, got: Any, suffix: str, msg: str)
check.matches(seat: Seat, got: Any, pattern: str, msg: str)

Numbers — Where exact equality is the wrong question.

check.close_to(seat: Seat, got: Any, want: float, tolerance: float, msg: str)
check.in_range(seat: Seat, got: Any, low: float, high: float, msg: str)

Ordering — Sorted, unique, and anything else that holds between neighbours.

check.pairwise(seat: Seat, items: Sequence[Any], predicate: Callable[[Any, Any], bool], msg: str)

Errors — For code that hands an error back rather than raising it.

check.no_error(seat: Seat, exc: BaseException|None, msg: str)
check.has_error(seat: Seat, exc: BaseException|None, msg: str)
check.error_is(seat: Seat, exc: BaseException|None, target: BaseException, msg: str)
check.error_is_not(seat: Seat, exc: BaseException|None, target: BaseException, msg: str)
check.error_as(seat: Seat, exc: BaseException|None, want: type[_E], msg: str) ->_E|None

Raising — For code that raises.

check.raises(seat: Seat, fn: Callable[[], Any], msg: str) ->BaseException|Nonecheck.does_not_raise(seat: Seat, fn: Callable[[], Any], msg: str)

Cancellation — asyncio is Python's cancellation model. These run the loop themselves, so your test stays a plain def. That is also the limit: the two that drive a coroutine cannot be called from a test already running a loop, and say so naming the assertion when they are.

check.honours_cancellation(seat: Seat, fn: Callable[[], Awaitable[Any]], msg: str)
check.honours_deadline(seat: Seat, fn: Callable[[], Awaitable[Any]], msg: str)
check.completes_within(seat: Seat, within: float, fn: Callable[[], Any], msg: str)
check.none_handle_safe(seat: Seat, fn: Callable[[Any], Any], msg: str)

Retrying — For a condition something outside the test makes true. Both spend real time.

check.eventually(seat: Seat, timeout: float, interval: float, body: Callable[[Any], None], msg: str)
check.eventually_true(seat: Seat, timeout: float, predicate: Callable[[], bool], msg: str)

Concurrency — Call what it returns where the scope ends.

check.no_task_leaks(seat: Seat, msg: str) ->Callable[[], None]

Purity — What observe returns defines what nothing means.

check.is_pure(seat: Seat, observe: Callable[[], Any], fn: Callable[[], Any], msg: str, *options: Option)

Testing an assertion — On check only: expect cannot drive a check to failure, because it does not stop.

check.rejects(seat: Seat, msg: str, body: Callable[[Recorder], None]) ->str

Golden files — recorded output, compared and rewritable.

golden.match(seat: Seat, name: str, got: str, update: bool, *scrubbers: Scrubber)
golden.match_at(seat: Seat, path: str|Path, got: str, update: bool, *scrubbers: Scrubber)
golden.match_json_field(seat: Seat, path: str|Path, field: str, got: str, update: bool, *scrubbers: Scrubber)
golden.should_update() ->boolgolden.scrub_timestamps() ->Scrubbergolden.scrub_hashes() ->Scrubbergolden.scrub_run_ids() ->Scrubbergolden.scrub_json_fields(*fields: str) ->Scrubber

Benchmark ceilings — chained onto one contract.

Contract.loop(iterations: int) ->Iterator[int]
Contract.max_latency(seconds: float) ->ContractContract.max_mean(seconds: float) ->ContractContract.max_bytes(count: int) ->ContractContract.excluding(setup: Callable[[], _T]) ->_T

Each one carries a full docstring: what it states, what every argument means, the edge cases it decides, and a worked call. Read them with help(check.close_to) or in your editor.

A few in use

fromdokimi_assertimportcheckdeftest_shapes(seat):
err=check.raises(seat, lambda: parse("{"), "a truncated body is refused")
check.contains(seat, str(err), "unexpected end", "and it says where")
check.pairwise(seat, timestamps, lambdaa, b: a<=b, "the log is ordered")
check.close_to(seat, elapsed, 1.0, 0.05, "the retry waited about a second")
check.matches(seat, request_id, r"^req_[0-9a-f]{16}$", "the id is well formed")
deftest_cancellation(seat):
# The subject is a coroutine function; the test is not. The# assertion drives the event loop itself, so no async plugin.check.honours_cancellation(seat, worker.run, "the worker stops when told")

Equality

Python's == does not answer what the standard asks:

ExpressionPythonHere
0 == FalseTruenot equal
1 == 1.0Truenot equal
[] == NoneFalsenot equal

bool subclasses int, so 0 == False is true, and numeric types compare across themselves. The standard says values of different types never compare, so this enforces it: type(got) is type(want), not isinstance.

An absent collection does not equal an empty one. Where that difference does not matter, relax the comparison for one call:

fromdokimi_assert.optionimportequate_empty, equate_nanscheck.equal(seat, reply.items, [], "no items came back", equate_empty())

An option applies to the call it is passed to and nothing else. There is no global setting, because a rule changed in one place and read in another is how two tests come to mean different things.

Golden files

fromdokimi_assertimportgoldendeftest_render(seat):
golden.match_at(
seat,
"testdata/report.txt",
render(report),
golden.should_update(),
golden.scrub_timestamps(),
)

Set DOKIMI_ASSERT_UPDATE_GOLDEN=1 to rewrite the files. Read the diff before you do. Scrubbers cover timestamps, hex digests, run ids and named JSON fields, so a value that changes every run does not fail the comparison.

Testing your own assertions

rejects states that a check fails, which is the one thing an assertion library has to be able to say about itself:

deftest_the_validator_refuses_an_empty_name(seat):
check.rejects(
seat,
"an empty name is refused",
lambdainner: check.is_none(inner, validate(""), "it passes"),
)

The recorder fixture is the lower-level version: drive an assertion with it, then read failed and message.

The standard

The assertions are defined in assert-spec, language-neutral, and implemented in several languages. This library vendors the definition and holds itself to it:

  • 87 corpus cases state what each assertion must report, run against both surfaces. They are the same cases every other implementation runs.
  • A completeness gate checks every assertion is present under the name the naming table gives it.
  • An overlay records any assertion this library cannot supply. It is empty: all 41 are implemented.

Where Python differs

Go states cancellation with context.Context, which appears in every signature. Python has no such convention, so honours_cancellation, honours_deadline and no_task_leaks are built on asyncio, whose CancelledError, timeouts and tasks are the real analogue. They take a coroutine function rather than a callable.

completes_within measures rather than interrupts: it reports whether a subject finished in time, and a slow subject runs to completion first.

Allocation ceilings use tracemalloc, which counts what Python itself allocated and carries real overhead. Set those ceilings from a traced run; latency ceilings need no such care.

Development

make install # create the environment
make check # the full pre-merge gate
make test# tests
make fmt # format and autofix
make build # build the sdist and wheel
make spec-sync # refresh the vendored definition

make check runs ruff, a formatting check, mypy strict, basedpyright and the tests with a coverage floor. CI runs it on 3.11 through 3.14. The design is recorded in docs/rfc/0001-the-python-implementation.md.

Pushing a v* tag builds, re-runs the gate, checks the tag agrees with pyproject.toml, and publishes to PyPI through Trusted Publishing.

Licence

MIT. See LICENSE.

About

Test assertions for Python, defined by a language-neutral standard and held to it on every run

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages