Metadata-Version: 2.5
Name: assertpy2
Version: 2.21.0
Summary: Fluent assertion library for Python with composable matchers, structural matching, and full type safety
Project-URL: Homepage, https://github.com/Solganis/assertpy2
Project-URL: Repository, https://github.com/Solganis/assertpy2
Project-URL: Issues, https://github.com/Solganis/assertpy2/issues
Author-email: Justin Shacklette <justin@saturnboy.com>
Maintainer-email: Solganis <solganis.dev@gmail.com>
License-Expression: BSD-3-Clause
License-File: LICENSE
Keywords: assert,assert_that,assertion,assertpy,assertthat,fluent,matchers,pytest,test,testing,type-safe,unittest
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: BSD License
Classifier: Natural Language :: English
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Programming Language :: Python :: 3.15
Classifier: Topic :: Software Development
Classifier: Topic :: Software Development :: Testing
Requires-Python: >=3.10
Requires-Dist: typing-extensions>=4.0; python_version < '3.11'
Provides-Extra: allure
Requires-Dist: allure-pytest>=2.13; extra == 'allure'
Provides-Extra: behave
Requires-Dist: behave>=1.2.6; extra == 'behave'
Provides-Extra: data
Requires-Dist: numpy>=1.26; extra == 'data'
Requires-Dist: pandas>=2.0; extra == 'data'
Requires-Dist: polars>=1.0; extra == 'data'
Provides-Extra: inline
Requires-Dist: asttokens>=2.0; extra == 'inline'
Requires-Dist: executing>=2.2; extra == 'inline'
Provides-Extra: json
Requires-Dist: jsonpath-ng>=1.8; extra == 'json'
Requires-Dist: jsonschema>=4.18; extra == 'json'
Requires-Dist: referencing>=0.28.4; extra == 'json'
Provides-Extra: numpy
Requires-Dist: numpy>=1.26; extra == 'numpy'
Provides-Extra: pandas
Requires-Dist: pandas>=2.0; extra == 'pandas'
Provides-Extra: polars
Requires-Dist: polars>=1.0; extra == 'polars'
Description-Content-Type: text/markdown

<p align="center">
  <picture>
    <source media="(prefers-color-scheme: dark)" srcset="docs/logo-dark.svg">
    <source media="(prefers-color-scheme: light)" srcset="docs/logo.svg">
    <img src="docs/logo.svg" alt="assertpy2" width="280">
  </picture>
  <br>
  <b>A fully typed fluent assertion library for Python</b><br>
  A modern, batteries-included fork of <a href="https://github.com/assertpy/assertpy">assertpy</a>
</p>

<p align="center">
  <a href="https://github.com/Solganis/assertpy2/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/Solganis/assertpy2/ci.yml?branch=main&amp;label=CI" alt="CI"></a>
  <a href="https://codecov.io/gh/Solganis/assertpy2"><img src="https://codecov.io/gh/Solganis/assertpy2/graph/badge.svg" alt="Coverage"></a>
  <a href="https://pypi.org/project/assertpy2/"><img src="https://img.shields.io/pypi/v/assertpy2" alt="PyPI version"></a>
  <a href="https://pypi.org/project/assertpy2/"><img src="https://img.shields.io/pypi/pyversions/assertpy2" alt="Python"></a>
  <a href="https://pepy.tech/projects/assertpy2"><img src="https://static.pepy.tech/badge/assertpy2/month" alt="Downloads"></a>
  <br>
  <a href="https://solganis.github.io/assertpy2/concepts/type-safety/"><img src="https://img.shields.io/badge/type--checked-ty%20%7C%20mypy%20%7C%20pyright-2ea043" alt="public overloads type-checked by ty, mypy --strict, and pyright with zero suppressions"></a>
  <a href="https://solganis.github.io/assertpy2/"><img src="https://img.shields.io/badge/docs-online-black" alt="Documentation"></a>
  <a href="https://scorecard.dev/viewer/?uri=github.com/Solganis/assertpy2"><img src="https://img.shields.io/ossf-scorecard/github.com/Solganis/assertpy2?label=OpenSSF%20Scorecard" alt="OpenSSF Scorecard"></a>
</p>

---

<h2 align="center"><a href="https://solganis.github.io/assertpy2/getting-started/quickstart/">Quick start</a></h2>

```bash
pip install assertpy2  # drop-in replacement for assertpy, just change the import
```

```python
from assertpy2 import assert_that

def test_user():
    user = {"name": "Alice", "age": 30, "roles": ["viewer", "editor"]}

    assert_that(user).contains_key("name", "age")
    assert_that(user).contains_entry({"name": "Alice"})
    assert_that(user["age"]).is_between(18, 120)
    assert_that(user["roles"]).contains("viewer").does_not_contain("admin")
```

The [full documentation](https://solganis.github.io/assertpy2/) covers every assertion, matcher, and integration.

<table>
<tr>
<td valign="top" width="50%">
<a href="#why-fluent-assertions"><b>Failures that point at the difference</b></a><br>
A recursive diff names the exact path that differs, in color, instead of dumping both structures.
</td>
<td valign="top" width="50%">
<a href="#type-aware-autocomplete"><b>Type-aware autocomplete</b></a><br>
<code>assert_that()</code> returns a protocol per value type, so your IDE offers the methods that fit.
</td>
</tr>
<tr>
<td valign="top">
<a href="#typed-narrowing"><b>Typed narrowing</b></a><br>
An assertion hands the value back statically narrowed, with no <code>cast</code> and no bare <code>assert</code>.
</td>
<td valign="top">
<a href="#features"><b>Composable matchers</b></a><br>
45 matchers that combine with <code>&amp;</code>, <code>|</code>, <code>~</code> and nest inside the expected structure itself.
</td>
</tr>
<tr>
<td valign="top">
<a href="#features"><b>Built for test suites</b></a><br>
Soft assertions, polling for eventual consistency, snapshots, and expected-exception chains.
</td>
<td valign="top">
<a href="#integrations"><b>Integrations</b></a><br>
Allure, Behave, JSON Path and Schema, pandas, polars, numpy, and OpenAPI response contracts.
</td>
</tr>
</table>

<h2 align="center"><a href="https://solganis.github.io/assertpy2/getting-started/comparison/">Why fluent assertions?</a></h2>

`assert` states a condition well, and pytest reports it well.

What it cannot say is *where* two structures differ. It prints both and leaves the reading to you:

```text
assert response == expected
E   AssertionError: assert {'id': 1, ...} == {'id': 1, ...}
E     Omitting 1 identical items, use -vv to show
E     Differing items:
E     {'user': {'name': 'Alice', 'role': 'superadmin'}} != {'user': {'name': 'Alice', 'role': 'admin'}}
E     {'status': 'active'} != {'status': 'disabled'}
```

assertpy2 names the [exact path](https://solganis.github.io/assertpy2/guides/errors/#rich-pytest-diffs), in color:

<!-- docs-guard: skip -->
```python
assert_that(response).is_equal_to(expected)
```

<p align="center">
  <img src="https://raw.githubusercontent.com/Solganis/assertpy2/main/docs/assets/diff-equal.png" width="300" alt="Structured diff in the terminal: user.role shown with its path, removal in red and addition in green">
</p>

It recurses through nested containers, and matcher predicates get the same treatment.

For dynamic fields like IDs, assert a subset with
[`matches_structure()`](https://solganis.github.io/assertpy2/guides/matchers/#structural-matching).

The chain is the other half: one statement carries the whole intent, and your IDE offers only the
[methods that fit the value](https://solganis.github.io/assertpy2/concepts/type-safety/).

<!-- docs-guard: skip -->
```python
assert_that(items).is_instance_of(list).is_length(3).contains("admin")
```

Matchers are ordinary values that answer `==`, the way `unittest.mock.ANY` does.

Nothing is patched, so a matcher can sit inside the expected structure itself, at any depth:

```python
response = {"id": 7, "user": {"name": "Alice", "age": 30}, "tags": ["a", "b"]}

assert_that(response).is_equal_to(
    {
        "id": match.greater_than(0),
        "user": {"name": "Alice", "age": match.between(18, 120)},
        "tags": ["a", "b"],
    }
)

# or keep the bare `assert`, and pytest's own rewriting reports it
assert response == {
    "id": match.greater_than(0),
    "user": match.ignore(),
    "tags": ["a", "b"],
}
```

The fluent form keeps the path-level diff, the bare form keeps pytest's.

There are [45 matchers](https://solganis.github.io/assertpy2/guides/matchers/), combining with `&`, `|` and `~`.

<p align="center">
  <img src="https://raw.githubusercontent.com/Solganis/assertpy2/main/docs/assets/diff-gallery.png" width="640" alt="Structured diffs in the terminal: dict path, list element, set extra/missing, and structural-matcher predicate diffs, side by side">
</p>

<h2 align="center"><a href="https://solganis.github.io/assertpy2/concepts/type-safety/">Type-aware autocomplete</a></h2>

`assert_that()` uses `@overload` to return type-specific Protocols.<br>
Your IDE shows only methods relevant to the value you're testing, not all 100+:

- `assert_that("hello").` &rarr; string methods: `starts_with`, `matches`, `is_alpha`, ...
- `assert_that(42).` &rarr; numeric methods: `is_positive`, `is_between`, `is_close_to`, ...
- `assert_that(Path("/tmp")).` &rarr; path methods: `exists`, `is_file`, `is_readable`, ...
- `assert_that(my_dict).` &rarr; dict methods: `contains_key`, `contains_entry`, `has_json_path`, ...
- `assert_that(b"\x89PNG").` &rarr; bytes methods: `starts_with_bytes`, `is_valid_utf8`, `decoded_as`, ...

11 type-specific Protocols instead of one `Any`.<br>
Works in PyCharm, VS Code, and any LSP-compatible editor.

<h2 align="center"><a href="https://solganis.github.io/assertpy2/concepts/type-safety/#typed-narrowing-with-value">Typed narrowing</a></h2>

An assertion hands the value back, statically narrowed.

`is_not_none()` strips `None`, `is_instance_of()` narrows to the class, and `.value` returns it:

<!-- docs-guard: skip -->
```python
order = assert_that(repo.find(42)).is_not_none().is_instance_of(PaidOrder).value
order.refund()  # statically PaidOrder - verified by ty, mypy, and pyright
```

For API tests,
[`assert_conforms()`](https://solganis.github.io/assertpy2/concepts/type-safety/#contract-narrowing-with-assert_conforms)
validates a payload against a Pydantic model and narrows to it. `exact=True` catches contract drift:

<!-- docs-guard: skip -->
```python
data = assert_conforms(response.json(), OrderModel).value  # data: OrderModel
```

<h2 align="center"><a href="https://solganis.github.io/assertpy2/guides/errors/#asking-instead-of-asserting">A failure you can read from code</a></h2>

An exception is the right default, and a dead end for anything that wants to read the result.

`check()` runs the next assertion for its verdict instead:

```python
response = {"user": {"name": "Alice", "role": "superadmin"}, "status": "active"}
expected = {"user": {"name": "Alice", "role": "admin"}, "status": "active"}

outcome = assert_that(response).check().is_equal_to(expected)

if not outcome and outcome.diff:
    print(outcome.diff.entries[0].path)  # user.role
```

It is truthy when the assertion held. When it did not, it carries `.message`, `.actual`, `.expected`
and a walkable `.diff`, and so does `AssertionFailure`.

So a reporter reads structure instead of parsing a string. That is how the
[Allure integration](https://solganis.github.io/assertpy2/extending/integrations/#allure) works, and it
is open to anything else you build.

<h2 align="center">Features</h2>

**Fluent API**

- [**Structural matching**](https://solganis.github.io/assertpy2/guides/matchers/#structural-matching): `matches_structure()` for declarative dict/API-response validation.
- [**Recursive field assertions**](https://solganis.github.io/assertpy2/guides/assertions/#recursive-field-assertions): `all_fields_satisfy()` / `has_no_none_fields()` apply a predicate to every leaf of an object graph.
- [**Vacuous-assertion guard**](https://solganis.github.io/assertpy2/guides/assertions/#assertions-that-checked-nothing): `--assertpy2-vacuous` warns when a universal assertion passes over an empty collection, having checked nothing.
- [**Universal negation**](https://solganis.github.io/assertpy2/guides/fluent/#universal-negation): `.not_` inverts any assertion, no dedicated `is_not_*` methods.
- [**Collection pipeline**](https://solganis.github.io/assertpy2/guides/fluent/#collection-pipeline): `filtered_on()`, `mapped()`, `flat_mapped()`, `first()`, `last()`, `element()`, `single()`.
- [**Positional & pairwise checks**](https://solganis.github.io/assertpy2/guides/assertions/#lists): `satisfies_exactly()`, `zip_satisfies()`, `contains_only_once()`, `has_same_size_as()`, plus `*_in_any_order` variants.

**Type safety**

- [**Refinement predicates**](https://solganis.github.io/assertpy2/concepts/type-safety/#refinement-narrowing-with-a-typeis-predicate-advanced): `satisfies()` takes a `TypeIs` predicate, so a domain check narrows the chain too.
- [**Contract testing**](https://solganis.github.io/assertpy2/concepts/type-safety/#contract-narrowing-with-assert_conforms): `assert_conforms()` validates a raw payload against a Pydantic model and narrows to it. [`exact=True`](https://solganis.github.io/assertpy2/concepts/type-safety/#contract-drift-with-exacttrue) catches contract drift, `each=True` validates list endpoints.

**Built-in types**

- [Strings](https://solganis.github.io/assertpy2/guides/assertions/#strings), [numbers](https://solganis.github.io/assertpy2/guides/assertions/#numbers), [lists](https://solganis.github.io/assertpy2/guides/assertions/#lists), [tuples](https://solganis.github.io/assertpy2/guides/assertions/#tuples), [sets](https://solganis.github.io/assertpy2/guides/assertions/#sets), [dicts](https://solganis.github.io/assertpy2/guides/assertions/#dicts), [dates](https://solganis.github.io/assertpy2/guides/assertions/#dates), [booleans](https://solganis.github.io/assertpy2/guides/assertions/#booleans), [objects](https://solganis.github.io/assertpy2/guides/assertions/#objects), [bytes](https://solganis.github.io/assertpy2/guides/assertions/#bytes--bytearray), [files](https://solganis.github.io/assertpy2/guides/assertions/#files), [exceptions](https://solganis.github.io/assertpy2/guides/errors/#expected-exceptions).
- [**Bytes assertions**](https://solganis.github.io/assertpy2/guides/assertions/#bytes--bytearray): `is_valid_utf8()`, `starts_with_bytes()`, `is_hex_equal_to()`, `decoded_as()` for `bytes`/`bytearray`.
- [**Dynamic assertions**](https://solganis.github.io/assertpy2/guides/assertions/#dynamic-assertions-on-objects): `has_<name>()` for any attribute, property, or zero-argument method.
- [**Dict comparison**](https://solganis.github.io/assertpy2/guides/assertions/#selective-comparison-ignore--include): `is_equal_to(ignore=..., include=...)` for selective key/field matching by name, regex, or type.
- [**Recursive comparison**](https://solganis.github.io/assertpy2/guides/assertions/#recursive-comparison-tolerance--custom-comparators): `is_equal_to()` with `tolerance`, `comparators`, or `ignore_null` for nested structures.
- [**Extracting**](https://solganis.github.io/assertpy2/guides/assertions/#extracting-attributes-from-objects): flatten collections on attributes with `filter` and `sort` support.

**Testing**

- [**Soft assertions**](https://solganis.github.io/assertpy2/guides/testing/#soft-assertions): thread-safe and async-safe via `contextvars`, each failure reported with its `file:line`. Group with `sa.group()` or `assert_all()`.
- [**Polling assertions**](https://solganis.github.io/assertpy2/guides/testing/#async-assertions): `eventually()` (async) / `eventually_sync()` (blocking) retry for eventual consistency, with a convergence trace on timeout.
- [**Expected exceptions**](https://solganis.github.io/assertpy2/guides/errors/#expected-exceptions): `raises().when_called_with()`, walk the cause chain (`caused_by()`, `has_root_cause()`), search an `ExceptionGroup` (`contains_error()`, `errors()`, `error_of()`), or pivot to the object (`raised()`).
- [**HTTP responses**](https://solganis.github.io/assertpy2/recipes/#test-an-http-api-response): assert on the response itself and every failure names the request it came from, with `decoded_as_json()` to step into the body. No client library is a dependency.
- [**Structured errors**](https://solganis.github.io/assertpy2/guides/errors/#structured-errors): `AssertionFailure` carries `.actual`, `.expected` and `.diff`, and the diff renders into the message, so it shows off pytest too.
- [**Assertions as values**](https://solganis.github.io/assertpy2/guides/errors/#asking-instead-of-asserting): `check()` runs the next assertion for its verdict instead of raising, handing back an `AssertionOutcome`.
- [**Rich pytest diffs**](https://solganis.github.io/assertpy2/guides/errors/#rich-pytest-diffs): recursive diffs across containers, dataclasses, attrs and Pydantic models, with intra-line carets for strings.
- [**Snapshot testing**](https://solganis.github.io/assertpy2/guides/testing/#snapshot-testing): an external JSON file, an [inline](https://solganis.github.io/assertpy2/guides/testing/#inline-snapshots) value recorded into the test source, or a [value-tolerant contract](https://solganis.github.io/assertpy2/guides/testing/#contract-snapshots), all updated with `--assertpy2-snapshot-update`.
- [**OpenAPI response contracts**](https://solganis.github.io/assertpy2/reference/json/#assertpy2.json_mixin.JsonMixin.conforms_to_openapi): `conforms_to_openapi()` checks a JSON body against an operation's response schema, reporting every violation with its JSON path.

**Extensibility**

- [**Custom matchers**](https://solganis.github.io/assertpy2/guides/matchers/#custom-matchers): `register_matcher()` composes existing ones, `BaseMatcher` carries its own predicate. Both compose with `&`, `|`, `~`.
- [**Custom assertions**](https://solganis.github.io/assertpy2/extending/custom-assertions/): `add_extension()` adds a method to the builder.
- [**Regex group extraction**](https://solganis.github.io/assertpy2/guides/data/#regex-group-extraction): `extracting_group()` and `matches_with_groups()` for regex captures.

<h2 align="center"><a href="https://solganis.github.io/assertpy2/extending/integrations/">Integrations</a></h2>

- [**Allure**](https://solganis.github.io/assertpy2/extending/integrations/#allure) (`pip install assertpy2[allure]`): the pytest plugin auto-attaches structured diff and actual/expected data to Allure reports, in three configurable modes.
- [**Behave**](https://solganis.github.io/assertpy2/extending/integrations/#behave) (`pip install assertpy2[behave]`): ready-made parameter types (`PositiveInt`, `NonEmptyString`, ...) for step definitions like `{age:PositiveInt}`.
- [**JSON**](https://solganis.github.io/assertpy2/guides/data/#json-path--schema) (`pip install assertpy2[json]`): JSONPath navigation (`at_json_path()`, `has_json_path()`) and JSON Schema validation (`matches_json_schema()`).
- [**Data frames**](https://solganis.github.io/assertpy2/extending/integrations/#data-frames-and-arrays) (`pip install assertpy2[pandas]` / `[polars]` / `[numpy]`): fluent equality for pandas/polars frames and numpy arrays, carrying each library's own diff.

---

<p align="center">
  <a href="https://github.com/Solganis/assertpy2/blob/main/LICENSE">BSD 3-Clause License</a>
</p>
