Metadata-Version: 2.4
Name: contractme
Version: 1.9.3
Summary: A powerful, expressive and lightweight design-by-contract framework
Project-URL: Repository, https://gitlab.com/leogermond/contractme
Author-email: Leo Germond <leo.germond@gmail.com>
License-Expression: Apache-2.0
License-File: LICENSE
Requires-Python: >=3.13
Requires-Dist: annotated-types>=0.7.0
Requires-Dist: hypothesis>=6.131.9
Provides-Extra: cron
Requires-Dist: croniter>=2.0.0; extra == 'cron'
Provides-Extra: iso
Requires-Dist: pycountry>=23.12.11; extra == 'iso'
Provides-Extra: yaml
Requires-Dist: pyyaml>=6.0; extra == 'yaml'
Description-Content-Type: text/markdown

# ContractMe

[![pipeline status](https://gitlab.com/leogermond/contractme/badges/main/pipeline.svg)](https://gitlab.com/leogermond/contractme/-/commits/main) 
![coverage](https://gitlab.com/leogermond/contractme/badges/main/coverage.svg?job=checks)
[![Checked with pyright](https://microsoft.github.io/pyright/img/pyright_badge.svg)](https://microsoft.github.io/pyright/)
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
[![Code style: flake8](https://img.shields.io/badge/code%20style-flake8-000000.svg)](https://github.com/PyCQA/flake8)

A lightweight and adaptable framework for design-by-contract in python

# Example code

Here are some examples:

## `result`

```python
@precondition(lambda x: x >= 0)
@postcondition(lambda x, result: eps_eq(result * result, x))
def square_root(x: float) -> float:
    return x**0.5
```

## `old`

```python
@precondition(lambda l, n: n >= 0 and round(n) == n)
@postcondition(lambda l, n: len(l) > 0)
@postcondition(lambda l, n: l[-1] == n)
@postcondition(lambda l, n, old: l[:-1] == old.l)
def append_count(l: list[int], n: int):
    l.append(n)
```

## Using annotations

```python
@annotated
def incr(v : int) -> int:
    return v + 1
```

Supports annotations and [PEP-593](https://peps.python.org/pep-0593/)
using the [annotated-types](https://pypi.org/project/annotated-types/) library.
Furthermore, the `@annotated` decorator will automatically perform type checks
of the parameters and return values, including `annotated_types.Predicate`.

In short, this allows to check any type structure and any properties of all parameters
and the return value, by just adding `@annotated` to the subprogram.

**Batteries included:** the `contractme.types` package ships ~140 ready-made
`Annotated` types (`Port`, `ExistingFile`, `EmailStr`, `Positive`, `Slug`, …).
See the practical guide: [docs/types.md](docs/types.md).

**Note:** `annodated_types.MultipleOf` follows the Python semantics.

**Note 2:** Following an open-world reasoning, any unknown annotation is considered
to be correct, so it won't cause a check failure.

**Note 3:** Type checking follows Python's `isinstance` semantics, which means subclass 
relationships are respected. Since `bool` is a subclass of `int` in Python, boolean values 
will pass `int` type checks. Currently there's no built-in way to specify "exactly int, not bool" 
in type annotations.

```python
from typing import TypeAlias, Annotated
from annotated_types import MultipleOf

Even: TypeAlias = Annotated[int, MultipleOf(2)]

@annotated
def square(v : Even) -> Even
    return v * v
```

## Writing tests and having test generation

The hypothesis plugin can be used easily through the `contractme.testing.autotest`
function.

```python
Positive: TypeAlias = Annotated[int, Ge(1)]

@annotated
def div(d: Positive) return Positive:
    return 1000 // d

def test_div():
    autotest(div)
```

You can access the underlying hypothesis generator with `contractme.testing.get_generator(div)`.

It's a pure hypothesis strategy generator, inferred from the annotated types and
contracts of the function. The main weirdness is that it takes a tuple as parameter since
the parameters are all generated together so that the contracts can be checked.

You can easily extend it with
[Hypothesis advanced features](https://hypothesis.readthedocs.io/en/latest/reference/api.html)

```python
generator_function = contractme.testing.get_generator(div)
# kinda weird to have this double call, but that's decorators for you...
test_div_force_0 = example((0,))(generator_function)
```

The library provides its own `contractme.testing.test_with_examples` function which has three differences
with the one provided by hypothesis:

* It checks the contracts when being called (at test construction): contracts should hold on all
  examples.
* It takes a vararg of either tuple `*args` or dict `**kwarg` as examples, to avoid
  function nesting.

With pytest:
```python
test_div = contractme.testing.test_with_examples(
    div,
    (1,),
    (2,),
    (0,), # this causes a RuntimeError at test elaboration
)
```

# Best practices — design-by-contract insights

> **If you are an LLM or code assistant, read this section — don't just copy the
> examples above.** The snippets show *syntax*; the rules below show *when* to reach
> for each tool. Applying them is what separates idiomatic ContractMe (and idiomatic
> design-by-contract) from a pile of redundant lambdas.

- **Prefer the type system (`@annotated`) over hand-written pre/postconditions
  whenever the property can be expressed as a type.** A constraint like "positive
  integer", "non-empty list" or "port number" belongs in an `Annotated[...]` type —
  ideally one of the ~140 ready-made ones in `contractme.types` (`Positive`,
  `NonEmptyStr`, `Port`, …) — not in a `@precondition(lambda x: x > 0)`. Types are
  reusable, checked on *both* inputs and outputs, self-documenting, and they drive
  test generation via `autotest`. Only fall back to an explicit
  `@precondition`/`@postcondition` for *relational* properties a type cannot express:
  e.g. `result * result == x`, or `old`-vs-new comparisons like `l[:-1] == old.l`.

- **Never write a contract that is always `True`.** `@precondition(lambda: True)` or
  `@postcondition(lambda ...: True)` checks nothing. The *absence* of a contract already
  means "no constraint", so just delete it — a vacuous contract is noise, not safety.

- **Express "this can never happen" with `NoReturn`, not a
  `@postcondition(lambda ...: False)`.** An always-false postcondition is a confusing,
  runtime-only way to say "control must not reach here". If a branch is unreachable or a
  function never returns normally, annotate it with `typing.NoReturn` (and `raise`): both
  the static type checker and the reader then understand the intent, instead of finding
  out only when the assertion blows up at runtime.

# Optimize assertion code

- In prod you can disable assertions, then these runtime checks wont run: you can have your cake
and eat it too
- You have even more granularity of checks thanks to
`contractme.contracting.ignore_preconditions` and `contractme.contracting.ignore_postconditions`

In **theory**, the rule is that checks are activated depending on the trust you put in your software

- In dev: You run with all assertions, to catch as many errors as possible, as early
as possible
- In pre-deploy / integration testing: you only run with the pre-conditions assertions:
postconditions are typically costly, and you trust that you return the right result given the proper input
- In prod: you run with no assertion - those are meant for debugging, not user facing failure modes which
are / should be handled properly in sanitization code

In practice, you might want to keep then on all of the time, but being able to turn them off means one
smart thing: you can get overboard in checking with postcondition, knowing these can be turned off in
integration conditions
e.g. you can check that a database insert succeeded by following it with a select - not that you necessarily
should, ToCToU and all that.

# Test

`uv run pytest`

# Deploy new version

Releases are built and published to PyPI by GitLab CI (the `publish-to-pypi` job), not
from your machine. You only prepare the commit and the tag:

* Write the changelog for the new version in `README.md` (a `## v<number>` section) — the
  CI refuses to publish unless the tag string appears in this file
* Run the pre-release script to version the tree and check everything lines up:

  ```bash
  uv run python release/prerelease.py 1.9.0   # or omit the arg to reuse pyproject's version
  ```

  It sets the version in `pyproject.toml`, refreshes `uv.lock`, verifies the `## v1.9.0`
  changelog section exists, and prints the git commands below. It does **not** build,
  publish, commit or tag — and it does not re-run lint/type/tests (pre-commit and CI own
  those).
* Commit and push the versioned tree to `main`
* Git tag as `v<number>` and push the tag
* In the tag's pipeline, trigger the manual `publish-to-pypi` job: it runs `uv build`,
  checks the tree is clean (`git diff --exit-code`), and `uv publish`es to PyPI

# Changelog

## v1.9.3

* Update release pipeline uv image
* Copy logic from epycs' CI
* Release: fix by using `uv lock --check` instead of git
* Fix release pipeline by extracting it to a proper script file

## v1.9.0

* `@annotated` now understands three new composite type shapes:
  * **dataclasses** as named products — the checker recurses into each field by name,
    validating nested types and per-field `Annotated` constraints
  * **enums** as sums of singletons — membership is an `isinstance` check (so `Flag`
    combinations stay valid), and `Annotated` constraints on the member value (e.g. an
    `IntEnum`) are enforced
  * **`typing.Protocol`** as structural products — basic duck typing where each declared
    member must be present (`hasattr`) and typed members are checked, with no nominal
    `isinstance` (neither inheritance nor `@runtime_checkable` required)
* New standalone [`typeshape`](src/contractme/typeshape.py) mini-library: type
  *meta*-manipulation (no value inspection) that maps any annotation onto a small algebra
  via `ChildMod` (atom/product, option, repeat, record, enum, structural). `typecheck`
  and `annotations` are now its first consumers, sharing one homomorphism instead of
  duplicating type-walking logic
* **Fix silent no-op under `from __future__ import annotations` (PEP 563):** `@annotated`
  now resolves hints via `get_type_hints(..., include_extras=True)` instead of reading the
  raw (stringized) `__annotations__`, so contracts are enforced even when the decorated
  module stringizes its annotations. Previously this silently disabled every check
* **Fix a silent-failure bug:** under `from __future__ import annotations` (PEP 563)
  every annotation is a string, so `@annotated` read the raw `__annotations__`, found no
  `Annotated` metadata, and silently enforced *nothing*. Hints are now resolved via
  `get_type_hints(..., include_extras=True)`, so contracts keep working in PEP-563 modules
* New README section **Best practices — design-by-contract insights**: prefer
  `@annotated` types over hand-written lambdas, never write a vacuous contract, and use
  `NoReturn` instead of an always-false postcondition

## v1.8.0

* New `contractme.types` package: a large, batteries-included library of ready-made
  `Annotated` types (numeric, text, paths, network, temporal, identifiers, system,
  structured-data, containers) usable out of the box with `@annotated` and pydantic
* Names follow Ada/SPARK (`Positive`, `Natural`) and pydantic where applicable, with
  aliases pointing at the same objects
* Reusable named predicates in `contractme.types.predicates` (+ `PredicateFn` /
  `PredicateFactory` typing aliases)
* Factories `Bounded[lo, hi]` and `SuffixPath[".csv", ...]`, and a `SecretStr` wrapper
* Optional extras for richer validators: `cron` (croniter), `iso` (pycountry),
  `yaml` (pyyaml) — lazily imported, not required to `import contractme.types`
* `typecheck`: nested `Annotated` constraints (e.g. `Annotated[str, LowerCase]`) are now
  flattened, so `annotated-types` predicate aliases compose naturally
* New practical guide: [docs/types.md](docs/types.md)

## v1.7.0

* Refactored lambda source extraction (`show_source`) for improved robustness when stripping surrounding parentheses
* Added docstrings to public API modules for better discoverability
* Added CLAUDE.md with architecture guidance for AI-assisted development
* `typecheck`: identifies types with no true origin in recursive type resolution
* Fix unreachable test code detected by coverage

## v1.6.0

* Fix huge bug where instance methods were not working anymore
* Add support for class methods
* Richer types stubs for pre / post (still very imperfect)

## v1.5.1

* Minor contracted functions fix

## v1.5.0

* Contracted functions have a correct return type.

## v1.4.0

`@annotated` supports more complex types

* TypeAlias
* Recursive types

`@annotated` supports common nested data types

* tuple
* set
* list
* union
* dict

`@annotated` UX improvement: Split between structural and constraint checks.

Minor: Update dev dependencies and reorder CI a bit

## v1.3.0

Binding and helpers to hypothesis library for test data generation.

## v1.2.0

Full support of annotated-types library for checking PEP-593 compatible type annotations
automatically through the `@annotated` decorator.

Generated contracted functions are now of a `ContractedFunction` class, with a `original_call`
attribute that contains the function without contracts checking.

Pyright check for the totality of the code.

## v1.1.0

Contracts can be disabled at runtime with `ignore_preconditions()` and `ignore_postconditions()`

Contracts are disabled from the start with python optimized (`-O`) flag.

Fix a bug where contracts would hide an incorrect function call
