Hegel for Python
Property-based testing for Python, built on the
Hegel engine (libhegel). The engine
generates inputs, shrinks failures, keeps an example database and prints the
failing test case; this package binds it to Python through a
pyo3 extension module and provides the test-writing API.
The API is the Hegel API, shared in shape with the Rust, Go and TypeScript
bindings: tests take a TestCase and draw values from generators. How it
compares with Hypothesis is set out in
docs/comparison-with-hypothesis.md; engine behaviour that differs from
Python's re or from Hypothesis is recorded in docs/engine-issues.md.
Installation
Hegel for Python is the hegeltest package, imported as hegel. It needs
Python 3.11 or later and, until wheels are published, a Rust toolchain:
pip install git+https://github.com/hegeldev/hegel-python
Quickstart
import hegel
from hegel import generators as gs
@hegel.test
def test_sorting_is_idempotent(tc):
xs = tc.draw(gs.lists(gs.integers()))
assert sorted(sorted(xs)) == sorted(xs)
The test runs under pytest (or by calling it). When it fails, Hegel shrinks the input and reports the minimal failing example, named after the variables the draws were assigned to:
Falsifying example:
xs = [0, 0]
Draw names
A draw assigned to a plain variable (xs = tc.draw(...)) is printed under that
name. A draw made in a loop, or assigned to a name that is assigned more than once
in the test, is numbered (x_1, x_2, ...), and a draw that is not assigned to a
variable is printed as draw_1, draw_2, and so on, as in the Rust library:
@hegel.test
def test_totals(tc):
n = tc.draw(gs.integers(0, 10))
for _ in range(n):
x = tc.draw(gs.integers())
total = tc.draw(gs.integers()) + tc.draw(gs.integers())
...
Falsifying example:
n = 2
x_1 = 0
x_2 = 0
draw_1 = 0
draw_2 = 0
tc.draw(generator, name="...") gives a draw an explicit (always numbered) name,
and tc.draw_silent(generator) draws without printing. Draws made inside a
composite generator are not printed separately.
Generators
hegel.generators provides generators for integers, floats, booleans, text
(by codec, category or alphabet, and from regular expressions), bytes, dates and
times, UUIDs, IP addresses, emails, URLs and domains, lists, sets, dicts, tuples,
permutations and samples, and combinators: map, filter, flat_map,
one_of (or |), optional, just, sampled_from, recursive, deferred,
composite, builds and from_type.
@hegel.composite
def sorted_pairs(tc, limit):
a = tc.draw(gs.integers(0, limit))
b = tc.draw(gs.integers(a, limit))
return (a, b)
Test case methods
Besides draw, a test case has assume(condition) and reject() to discard the
current input, note(message) to add a line to the failure report,
target(score, label) to steer generation toward higher scores,
event(label) and event_value(label, value) for the run's statistics, and
repeat(body), which runs body an engine-chosen number of times and then ends
the test case.
Configuration
@hegel.test(test_cases=1000, seed=42) sets options for one test. Settings cover
the number of test cases, verbosity, seeding and derandomization, the example
database, phases, health checks, multiple-failure reporting and the random backend.
Settings start from a profile. The engine provides development (the default
locally), ci (chosen automatically on a CI server) and workload (under
Antithesis); a project can pick one in a hegel.toml file or with the
HEGEL_DEFAULT_PROFILE environment variable. Your own profiles are registered
in Python:
hegel.Settings.register_profile("thorough", test_cases=10_000)
hegel.Settings.set_default_profile("thorough")
Failing examples are saved in .hegel/examples and replayed first on the next run.
database="path" keeps them elsewhere, and database="" turns the database off.
Under pytest, --hegel-profile, --hegel-seed, --hegel-verbosity,
--hegel-derandomize and --hegel-database adjust every Hegel test in the run.
Explicit test cases and reproduction
@hegel.explicit_test_case(xs=[1, 1])
@hegel.test
def test_no_duplicates(tc):
xs = tc.draw(gs.lists(gs.integers()))
...
A failure report ends with a @hegel.reproduce_failure("...") line; add that
decorator below @hegel.test to replay exactly that failure.
Stateful testing
class Counter(hegel.StateMachine):
def __init__(self):
self.count = 0
@hegel.rule()
def increment(self, tc):
n = tc.draw(gs.integers(1, 10))
self.count += n
@hegel.invariant()
def non_negative(self, tc):
assert self.count >= 0
test_counter = hegel.state_machine_test(Counter)
Rules and invariants take the test case and draw their own values. The machine's constructor takes either no arguments or the test case.
hegel.Pool holds values that rules produce for later rules to use, with
generators that draw a value and leave it in the pool or take it out:
class Files(hegel.StateMachine):
def __init__(self, tc):
self.handles = hegel.Pool(tc)
@hegel.rule()
def open(self, tc):
name = tc.draw(gs.text(min_size=1))
self.handles.add(open_file(name))
@hegel.rule()
def close(self, tc):
handle = tc.draw(self.handles.values_consumed())
handle.close()
Setting max_concurrency above 1 on the class runs the rules from several threads
at once, in rounds grouped by the rules' group, with Hegel choosing the
concurrency level and attributing the output to each worker; hegel.ConcurrentPool
is the pool to share between them.
Coming from Hypothesis
A Hypothesis test maps onto a Hegel one draw by draw:
# Hypothesis # Hegel
@given(st.lists(st.integers())) @hegel.test
def test_reversing_twice(xs): def test_reversing_twice(tc):
... xs = tc.draw(gs.lists(gs.integers()))
...
docs/comparison-with-hypothesis.md lists the correspondences and the
differences. Hypothesis's own test suite, ported to the Hegel API, runs under
tests/hypothesis_port.
Development
just setup # create the virtualenv and build the extension
just check # formatting, lint, types, tests with coverage, the ported suite
Metadata
Release files for hegeltest 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| hegeltest-0.1.0.tar.gz | 266.4 kB | Without provenance | Details |
Built distributions (wheels)
| File | Reset | |||
|---|---|---|---|---|
| hegeltest-0.1.0-cp314-cp314-macosx_11_0_arm64.whl | CPython 3.14 | CPython 3.14 | macOS 11.0+ ARM64 | Details |
| hegeltest-0.1.0-cp313-cp313-macosx_11_0_arm64.whl | CPython 3.13 | CPython 3.13 | macOS 11.0+ ARM64 | Details |
| hegeltest-0.1.0-cp312-cp312-macosx_11_0_arm64.whl | CPython 3.12 | CPython 3.12 | macOS 11.0+ ARM64 | Details |
| hegeltest-0.1.0-cp311-cp311-macosx_11_0_arm64.whl | CPython 3.11 | CPython 3.11 | macOS 11.0+ ARM64 | Details |
Total release size: 7.9 MB
Release files / hegeltest-0.1.0.tar.gz
| Download URL | hegeltest-0.1.0.tar.gz |
|---|---|
| Size | 266.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
8ada34759e87797fd03adc4ebba2e3df721a6869a0ac802876bb041bd73f05de
|
|
BLAKE2b-256 checksum How to use checksums |
492f3830ce572b3c2c0966c4f99e75ec913780ff6fa7ca70e3b0ab1e684e524d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.3 {"installer":{"name":"uv","version":"0.12.3","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / hegeltest-0.1.0-cp314-cp314-macosx_11_0_arm64.whl
| Download URL | hegeltest-0.1.0-cp314-cp314-macosx_11_0_arm64.whl |
|---|---|
| Size | 1.9 MB |
| Tags | CPython 3.14 macOS 11.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
9377ab15c6c120bf93e6fefcceab0ea0bf9c947d68d084deb8913cb09ba902b3
|
|
BLAKE2b-256 checksum How to use checksums |
719db2d99c4bf121aecc7bec5c29fb963adb23edc2faad8e918b9637d6c59ba7
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.3 {"installer":{"name":"uv","version":"0.12.3","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / hegeltest-0.1.0-cp313-cp313-macosx_11_0_arm64.whl
| Download URL | hegeltest-0.1.0-cp313-cp313-macosx_11_0_arm64.whl |
|---|---|
| Size | 1.9 MB |
| Tags | CPython 3.13 macOS 11.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
59c2340e3bb4cde1886b7685abbbf418e086ef59fff12633c623efb363ad73c6
|
|
BLAKE2b-256 checksum How to use checksums |
7a1ed5b979c14f96d758c21d1ebc2115c9cede953e629977f3de6f685f6a9bf1
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.3 {"installer":{"name":"uv","version":"0.12.3","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / hegeltest-0.1.0-cp312-cp312-macosx_11_0_arm64.whl
| Download URL | hegeltest-0.1.0-cp312-cp312-macosx_11_0_arm64.whl |
|---|---|
| Size | 1.9 MB |
| Tags | CPython 3.12 macOS 11.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
871aae98a108cde0257d26f806693b7dc28c84ed764bb0722ea542d3f45172b9
|
|
BLAKE2b-256 checksum How to use checksums |
49fb0370847db4682ec9ada45a2901dcdfd4b801e23bb09893bd9f5aa958287e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.3 {"installer":{"name":"uv","version":"0.12.3","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / hegeltest-0.1.0-cp311-cp311-macosx_11_0_arm64.whl
| Download URL | hegeltest-0.1.0-cp311-cp311-macosx_11_0_arm64.whl |
|---|---|
| Size | 1.9 MB |
| Tags | CPython 3.11 macOS 11.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
2683ca93381f3afcba802799f3ec25c1f69ee010c5193fe96c1ea5b002cc5f74
|
|
BLAKE2b-256 checksum How to use checksums |
d0225828afc826d1c31af0575d89e54e9500dd590a422f7149d2650d32adb383
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.3 {"installer":{"name":"uv","version":"0.12.3","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Security for hegeltest 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.