assertpy2 2.3.8__tar.gz → 2.5.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- assertpy2-2.5.0/.codecov.yml +10 -0
- {assertpy2-2.3.8 → assertpy2-2.5.0}/.github/workflows/ci.yml +12 -5
- {assertpy2-2.3.8 → assertpy2-2.5.0}/.github/workflows/zizmor.yml +4 -0
- assertpy2-2.5.0/PKG-INFO +221 -0
- assertpy2-2.5.0/README.md +183 -0
- {assertpy2-2.3.8 → assertpy2-2.5.0}/assertpy2/_mixin_base.py +3 -0
- {assertpy2-2.3.8 → assertpy2-2.5.0}/assertpy2/assertpy.py +1 -1
- {assertpy2-2.3.8 → assertpy2-2.5.0}/assertpy2/base.py +192 -30
- {assertpy2-2.3.8 → assertpy2-2.5.0}/assertpy2/helpers.py +83 -16
- {assertpy2-2.3.8 → assertpy2-2.5.0}/assertpy2/numeric.py +4 -4
- {assertpy2-2.3.8 → assertpy2-2.5.0}/assertpy2/pytest_plugin.py +5 -11
- {assertpy2-2.3.8 → assertpy2-2.5.0}/docs/api.md +54 -9
- {assertpy2-2.3.8 → assertpy2-2.5.0}/pyproject.toml +2 -2
- {assertpy2-2.3.8 → assertpy2-2.5.0}/tests/test_class.py +3 -7
- {assertpy2-2.3.8 → assertpy2-2.5.0}/tests/test_collection.py +4 -4
- assertpy2-2.5.0/tests/test_core.py +82 -0
- assertpy2-2.5.0/tests/test_custom_dict.py +209 -0
- {assertpy2-2.3.8 → assertpy2-2.5.0}/tests/test_custom_list.py +9 -9
- {assertpy2-2.3.8 → assertpy2-2.5.0}/tests/test_dict.py +2 -2
- {assertpy2-2.3.8 → assertpy2-2.5.0}/tests/test_dict_compare.py +7 -7
- {assertpy2-2.3.8 → assertpy2-2.5.0}/tests/test_equals.py +2 -2
- {assertpy2-2.3.8 → assertpy2-2.5.0}/tests/test_extensions.py +5 -5
- {assertpy2-2.3.8 → assertpy2-2.5.0}/tests/test_extracting.py +16 -16
- {assertpy2-2.3.8 → assertpy2-2.5.0}/tests/test_list.py +32 -32
- {assertpy2-2.3.8 → assertpy2-2.5.0}/tests/test_matchers.py +63 -63
- {assertpy2-2.3.8 → assertpy2-2.5.0}/tests/test_numbers.py +0 -2
- {assertpy2-2.3.8 → assertpy2-2.5.0}/tests/test_readme.py +26 -27
- assertpy2-2.5.0/tests/test_recursive_compare.py +232 -0
- assertpy2-2.5.0/tests/test_recursive_compare_attrs.py +53 -0
- assertpy2-2.5.0/tests/test_recursive_compare_pydantic.py +55 -0
- assertpy2-2.5.0/tests/test_rich_diff.py +979 -0
- {assertpy2-2.3.8 → assertpy2-2.5.0}/tests/test_snapshots.py +5 -5
- {assertpy2-2.3.8 → assertpy2-2.5.0}/tests/test_string.py +2 -5
- {assertpy2-2.3.8 → assertpy2-2.5.0}/tests/test_structural.py +80 -80
- {assertpy2-2.3.8 → assertpy2-2.5.0}/tests/test_type.py +2 -2
- {assertpy2-2.3.8 → assertpy2-2.5.0}/uv.lock +21 -21
- assertpy2-2.3.8/.codecov.yml +0 -10
- assertpy2-2.3.8/PKG-INFO +0 -633
- assertpy2-2.3.8/README.md +0 -595
- assertpy2-2.3.8/tests/test_core.py +0 -82
- assertpy2-2.3.8/tests/test_custom_dict.py +0 -209
- assertpy2-2.3.8/tests/test_rich_diff.py +0 -427
- {assertpy2-2.3.8 → assertpy2-2.5.0}/.github/dependabot.yml +0 -0
- {assertpy2-2.3.8 → assertpy2-2.5.0}/.github/workflows/codeql.yml +0 -0
- {assertpy2-2.3.8 → assertpy2-2.5.0}/.github/workflows/publish.yml +0 -0
- {assertpy2-2.3.8 → assertpy2-2.5.0}/.github/workflows/scorecard.yml +0 -0
- {assertpy2-2.3.8 → assertpy2-2.5.0}/.gitignore +0 -0
- {assertpy2-2.3.8 → assertpy2-2.5.0}/CONTRIBUTING.md +0 -0
- {assertpy2-2.3.8 → assertpy2-2.5.0}/LICENSE +0 -0
- {assertpy2-2.3.8 → assertpy2-2.5.0}/SECURITY.md +0 -0
- {assertpy2-2.3.8 → assertpy2-2.5.0}/assertpy2/__init__.py +0 -0
- {assertpy2-2.3.8 → assertpy2-2.5.0}/assertpy2/_typing.py +0 -0
- {assertpy2-2.3.8 → assertpy2-2.5.0}/assertpy2/async_assertions.py +0 -0
- {assertpy2-2.3.8 → assertpy2-2.5.0}/assertpy2/behave_matchers.py +0 -0
- {assertpy2-2.3.8 → assertpy2-2.5.0}/assertpy2/bytes_mixin.py +0 -0
- {assertpy2-2.3.8 → assertpy2-2.5.0}/assertpy2/collection.py +0 -0
- {assertpy2-2.3.8 → assertpy2-2.5.0}/assertpy2/contains.py +0 -0
- {assertpy2-2.3.8 → assertpy2-2.5.0}/assertpy2/date.py +0 -0
- {assertpy2-2.3.8 → assertpy2-2.5.0}/assertpy2/dict.py +0 -0
- {assertpy2-2.3.8 → assertpy2-2.5.0}/assertpy2/dynamic.py +0 -0
- {assertpy2-2.3.8 → assertpy2-2.5.0}/assertpy2/errors.py +0 -0
- {assertpy2-2.3.8 → assertpy2-2.5.0}/assertpy2/exception.py +0 -0
- {assertpy2-2.3.8 → assertpy2-2.5.0}/assertpy2/extracting.py +0 -0
- {assertpy2-2.3.8 → assertpy2-2.5.0}/assertpy2/file.py +0 -0
- {assertpy2-2.3.8 → assertpy2-2.5.0}/assertpy2/json_mixin.py +0 -0
- {assertpy2-2.3.8 → assertpy2-2.5.0}/assertpy2/matchers.py +0 -0
- {assertpy2-2.3.8 → assertpy2-2.5.0}/assertpy2/py.typed +0 -0
- {assertpy2-2.3.8 → assertpy2-2.5.0}/assertpy2/snapshot.py +0 -0
- {assertpy2-2.3.8 → assertpy2-2.5.0}/assertpy2/string.py +0 -0
- {assertpy2-2.3.8 → assertpy2-2.5.0}/docs/logo-dark.svg +0 -0
- {assertpy2-2.3.8 → assertpy2-2.5.0}/docs/logo.svg +0 -0
- {assertpy2-2.3.8 → assertpy2-2.5.0}/tests/test_async.py +0 -0
- {assertpy2-2.3.8 → assertpy2-2.5.0}/tests/test_behave_matchers.py +0 -0
- {assertpy2-2.3.8 → assertpy2-2.5.0}/tests/test_bool.py +0 -0
- {assertpy2-2.3.8 → assertpy2-2.5.0}/tests/test_bytes.py +0 -0
- {assertpy2-2.3.8 → assertpy2-2.5.0}/tests/test_datetime.py +0 -0
- {assertpy2-2.3.8 → assertpy2-2.5.0}/tests/test_description.py +0 -0
- {assertpy2-2.3.8 → assertpy2-2.5.0}/tests/test_dyn.py +0 -0
- {assertpy2-2.3.8 → assertpy2-2.5.0}/tests/test_errors.py +0 -0
- {assertpy2-2.3.8 → assertpy2-2.5.0}/tests/test_expected_exception.py +0 -0
- {assertpy2-2.3.8 → assertpy2-2.5.0}/tests/test_fail.py +0 -0
- {assertpy2-2.3.8 → assertpy2-2.5.0}/tests/test_file.py +0 -0
- {assertpy2-2.3.8 → assertpy2-2.5.0}/tests/test_grouped_soft.py +0 -0
- {assertpy2-2.3.8 → assertpy2-2.5.0}/tests/test_in.py +0 -0
- {assertpy2-2.3.8 → assertpy2-2.5.0}/tests/test_json.py +0 -0
- {assertpy2-2.3.8 → assertpy2-2.5.0}/tests/test_matcher_registry.py +0 -0
- {assertpy2-2.3.8 → assertpy2-2.5.0}/tests/test_matchers_phase3.py +0 -0
- {assertpy2-2.3.8 → assertpy2-2.5.0}/tests/test_namedtuple.py +0 -0
- {assertpy2-2.3.8 → assertpy2-2.5.0}/tests/test_none.py +0 -0
- {assertpy2-2.3.8 → assertpy2-2.5.0}/tests/test_not.py +0 -0
- {assertpy2-2.3.8 → assertpy2-2.5.0}/tests/test_overloads.py +0 -0
- {assertpy2-2.3.8 → assertpy2-2.5.0}/tests/test_phase2.py +0 -0
- {assertpy2-2.3.8 → assertpy2-2.5.0}/tests/test_pipeline.py +0 -0
- {assertpy2-2.3.8 → assertpy2-2.5.0}/tests/test_pytest_plugin.py +0 -0
- {assertpy2-2.3.8 → assertpy2-2.5.0}/tests/test_regex_groups.py +0 -0
- {assertpy2-2.3.8 → assertpy2-2.5.0}/tests/test_same_as.py +0 -0
- {assertpy2-2.3.8 → assertpy2-2.5.0}/tests/test_soft.py +0 -0
- {assertpy2-2.3.8 → assertpy2-2.5.0}/tests/test_soft_fail.py +0 -0
- {assertpy2-2.3.8 → assertpy2-2.5.0}/tests/test_traceback.py +0 -0
- {assertpy2-2.3.8 → assertpy2-2.5.0}/tests/test_warn.py +0 -0
|
@@ -6,15 +6,25 @@ on:
|
|
|
6
6
|
pull_request:
|
|
7
7
|
branches: [main]
|
|
8
8
|
|
|
9
|
+
concurrency:
|
|
10
|
+
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
|
11
|
+
cancel-in-progress: true
|
|
12
|
+
|
|
9
13
|
permissions:
|
|
10
14
|
contents: read
|
|
11
15
|
|
|
12
16
|
jobs:
|
|
13
17
|
test:
|
|
14
|
-
runs-on:
|
|
18
|
+
runs-on: ${{ matrix.os }}
|
|
15
19
|
strategy:
|
|
16
20
|
matrix:
|
|
21
|
+
os: [ubuntu-latest]
|
|
17
22
|
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14", "3.15"]
|
|
23
|
+
include:
|
|
24
|
+
- os: windows-latest
|
|
25
|
+
python-version: "3.14"
|
|
26
|
+
- os: macos-latest
|
|
27
|
+
python-version: "3.14"
|
|
18
28
|
steps:
|
|
19
29
|
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
|
20
30
|
with:
|
|
@@ -32,14 +42,11 @@ jobs:
|
|
|
32
42
|
- name: Install dependencies
|
|
33
43
|
run: uv sync --extra json
|
|
34
44
|
|
|
35
|
-
- name: Lint
|
|
36
|
-
run: uv run ruff check .
|
|
37
|
-
|
|
38
45
|
- name: Test with coverage
|
|
39
46
|
run: uv run pytest -v --cov=assertpy2 --cov-report=term-missing --cov-report=xml tests
|
|
40
47
|
|
|
41
48
|
- name: Upload coverage to Codecov
|
|
42
|
-
if: matrix.python-version == '3.14'
|
|
49
|
+
if: matrix.python-version == '3.14' && matrix.os == 'ubuntu-latest'
|
|
43
50
|
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
|
|
44
51
|
with:
|
|
45
52
|
token: ${{ secrets.CODECOV_TOKEN }}
|
assertpy2-2.5.0/PKG-INFO
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: assertpy2
|
|
3
|
+
Version: 2.5.0
|
|
4
|
+
Summary: Fluent assertion library for Python with composable matchers, structural matching, and full type safety
|
|
5
|
+
Project-URL: Homepage, https://github.com/Solganis/assertpy2
|
|
6
|
+
Project-URL: Repository, https://github.com/Solganis/assertpy2
|
|
7
|
+
Project-URL: Issues, https://github.com/Solganis/assertpy2/issues
|
|
8
|
+
Author-email: Justin Shacklette <justin@saturnboy.com>
|
|
9
|
+
Maintainer-email: Solganis <solganis.dev@gmail.com>
|
|
10
|
+
License-Expression: BSD-3-Clause
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: assert,assert_that,assertion,assertthat,pytest,test,testing,unittest
|
|
13
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
16
|
+
Classifier: Natural Language :: English
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python
|
|
19
|
+
Classifier: Programming Language :: Python :: 3
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
26
|
+
Classifier: Topic :: Software Development
|
|
27
|
+
Classifier: Topic :: Software Development :: Testing
|
|
28
|
+
Requires-Python: >=3.10
|
|
29
|
+
Requires-Dist: typing-extensions>=4.0
|
|
30
|
+
Provides-Extra: allure
|
|
31
|
+
Requires-Dist: allure-pytest>=2.13; extra == 'allure'
|
|
32
|
+
Provides-Extra: behave
|
|
33
|
+
Requires-Dist: behave>=1.2.6; extra == 'behave'
|
|
34
|
+
Provides-Extra: json
|
|
35
|
+
Requires-Dist: jsonpath-ng>=1.8; extra == 'json'
|
|
36
|
+
Requires-Dist: jsonschema>=4.0; extra == 'json'
|
|
37
|
+
Description-Content-Type: text/markdown
|
|
38
|
+
|
|
39
|
+
<p align="center">
|
|
40
|
+
<picture>
|
|
41
|
+
<source media="(prefers-color-scheme: dark)" srcset="docs/logo-dark.svg">
|
|
42
|
+
<source media="(prefers-color-scheme: light)" srcset="docs/logo.svg">
|
|
43
|
+
<img src="docs/logo.svg" alt="assertpy2" width="280">
|
|
44
|
+
</picture>
|
|
45
|
+
</p>
|
|
46
|
+
|
|
47
|
+
<p align="center">
|
|
48
|
+
<b>Fluent assertion library for Python with composable matchers, structural matching, and full type safety.</b><br>
|
|
49
|
+
A modern, batteries-included fork of <a href="https://github.com/assertpy/assertpy">assertpy</a>.
|
|
50
|
+
</p>
|
|
51
|
+
|
|
52
|
+
<p align="center">
|
|
53
|
+
<a href="https://github.com/Solganis/assertpy2/actions/workflows/ci.yml"><img src="https://github.com/Solganis/assertpy2/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
|
|
54
|
+
<a href="https://pypi.org/project/assertpy2/"><img src="https://img.shields.io/pypi/v/assertpy2" alt="PyPI version"></a>
|
|
55
|
+
<a href="https://pepy.tech/projects/assertpy2"><img src="https://static.pepy.tech/badge/assertpy2/month" alt="Downloads"></a>
|
|
56
|
+
<a href="https://pypi.org/project/assertpy2/"><img src="https://img.shields.io/pypi/pyversions/assertpy2" alt="Python"></a>
|
|
57
|
+
<a href="https://codecov.io/gh/Solganis/assertpy2"><img src="https://codecov.io/gh/Solganis/assertpy2/graph/badge.svg" alt="Coverage"></a>
|
|
58
|
+
<br>
|
|
59
|
+
<a href="https://github.com/Solganis/assertpy2/blob/main/docs/api.md"><img src="https://img.shields.io/badge/Docs-Read%20The%20Docs-black" alt="Documentation"></a>
|
|
60
|
+
<a href="https://docs.astral.sh/ruff/"><img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json" alt="Ruff"></a>
|
|
61
|
+
<a href="https://github.com/astral-sh/uv"><img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json" alt="uv"></a>
|
|
62
|
+
<a href="https://github.com/astral-sh/ty"><img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ty/main/assets/badge/v0.json" alt="ty"></a>
|
|
63
|
+
<a href="https://scorecard.dev/viewer/?uri=github.com/Solganis/assertpy2"><img src="https://api.scorecard.dev/projects/github.com/Solganis/assertpy2/badge" alt="OpenSSF Scorecard"></a>
|
|
64
|
+
<a href="https://www.bestpractices.dev/projects/12990"><img src="https://www.bestpractices.dev/projects/12990/badge" alt="OpenSSF Best Practices"></a>
|
|
65
|
+
</p>
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Quick start
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
pip install assertpy2 # drop-in replacement for assertpy, just change the import
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
```py
|
|
76
|
+
from assertpy2 import assert_that
|
|
77
|
+
|
|
78
|
+
def test_user():
|
|
79
|
+
user = {"name": "Alice", "age": 30, "roles": ["viewer", "editor"]}
|
|
80
|
+
|
|
81
|
+
assert_that(user).contains_key("name", "age")
|
|
82
|
+
assert_that(user["age"]).is_between(18, 120)
|
|
83
|
+
assert_that(user["roles"]).contains("viewer").does_not_contain("admin")
|
|
84
|
+
assert_that(user).has_name("Alice")
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Why fluent assertions?
|
|
88
|
+
|
|
89
|
+
```py
|
|
90
|
+
# bare assert - passes, but failure message is useless
|
|
91
|
+
assert user["age"] >= 18
|
|
92
|
+
# AssertionError
|
|
93
|
+
|
|
94
|
+
# assertpy2 - same check, clear failure message
|
|
95
|
+
assert_that(user["age"]).is_greater_than_or_equal_to(18)
|
|
96
|
+
# AssertionError: Expected <16> to be greater than or equal to <18>, but was not.
|
|
97
|
+
|
|
98
|
+
# bare assert - three separate statements
|
|
99
|
+
assert isinstance(items, list)
|
|
100
|
+
assert len(items) == 3
|
|
101
|
+
assert "admin" in items
|
|
102
|
+
|
|
103
|
+
# assertpy2 - one fluent chain
|
|
104
|
+
assert_that(items).is_type_of(list).is_length(3).contains("admin")
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Type-aware autocomplete
|
|
108
|
+
|
|
109
|
+
`assert_that()` uses `@overload` to return type-specific Protocols.
|
|
110
|
+
Your IDE shows only methods relevant to the value you're testing, not all 100+:
|
|
111
|
+
|
|
112
|
+
- `assert_that("hello").` → string methods: `starts_with`, `matches`, `is_alpha`, ...
|
|
113
|
+
- `assert_that(42).` → numeric methods: `is_positive`, `is_between`, `is_close_to`, ...
|
|
114
|
+
- `assert_that(Path("/tmp")).` → path methods: `exists`, `is_file`, `is_readable`, ...
|
|
115
|
+
- `assert_that(my_dict).` → dict methods: `contains_key`, `contains_entry`, `has_json_path`, ...
|
|
116
|
+
- `assert_that(b"\x89PNG").` → bytes methods: `starts_with_bytes`, `is_valid_utf8`, `decoded_as`, ...
|
|
117
|
+
|
|
118
|
+
9 type-specific Protocols instead of one `Any`. Works in PyCharm, VS Code, and any LSP-compatible editor.
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Features
|
|
123
|
+
|
|
124
|
+
**Fluent API**
|
|
125
|
+
|
|
126
|
+
- [**Composable matchers**](docs/api.md#composable-matchers): `match.greater_than(5)`, `match.is_uuid()`, combine with `&`, `|`, `~`. Also work with plain `assert ==`.
|
|
127
|
+
- [**Structural matching**](docs/api.md#structural-matching): `matches_structure()` for declarative dict/API response validation.
|
|
128
|
+
- [**Universal negation**](docs/api.md#universal-negation): `.not_` inverts any assertion without dedicated `is_not_*` methods.
|
|
129
|
+
- [**Collection pipeline**](docs/api.md#collection-pipeline): `filtered_on()`, `mapped()`, `flat_mapped()`, `first()`, `last()`, `element()`, `single()`.
|
|
130
|
+
- [**Fluent chaining**](docs/api.md#chaining): write assertions as readable one-liners that chain naturally.
|
|
131
|
+
|
|
132
|
+
**Built-in types**
|
|
133
|
+
|
|
134
|
+
- [Strings](docs/api.md#strings), [numbers](docs/api.md#numbers), [lists](docs/api.md#lists), [tuples](docs/api.md#tuples), [sets](docs/api.md#sets), [dicts](docs/api.md#dicts), [dates](docs/api.md#dates), [booleans](docs/api.md#booleans), [objects](docs/api.md#objects), [bytes](docs/api.md#bytes--bytearray-assertions), [files](docs/api.md#files), [exceptions](docs/api.md#failure).
|
|
135
|
+
- [**Bytes assertions**](docs/api.md#bytes--bytearray-assertions): `is_valid_utf8()`, `starts_with_bytes()`, `is_hex_equal_to()`, `decoded_as()` for `bytes`/`bytearray`.
|
|
136
|
+
- [**JSON assertions**](docs/api.md#json-path--schema-validation): JSONPath navigation and JSON Schema validation. `pip install assertpy2[json]`.
|
|
137
|
+
- [**Dynamic assertions**](docs/api.md#objects): `has_<name>()` for any attribute, property, or zero-argument method.
|
|
138
|
+
- [**Dict comparison**](docs/api.md#dicts): `is_equal_to()` with `ignore` and `include` for selective key/field matching (dicts, dataclasses, namedtuples, Pydantic models, attrs, plain objects).
|
|
139
|
+
- [**Extracting**](docs/api.md#objects): flatten collections on attributes with `filter` and `sort` support.
|
|
140
|
+
|
|
141
|
+
**Testing**
|
|
142
|
+
|
|
143
|
+
- [**Soft assertions**](docs/api.md#soft-assertions): thread-safe, async-safe via `contextvars`. Group errors with `sa.group()`, or use `assert_all()`.
|
|
144
|
+
- [**Async assertions**](docs/api.md#async-assertions): `eventually()` with polling/retry for eventual consistency.
|
|
145
|
+
- [**Structured errors**](docs/api.md#structured-errors): `AssertionFailure` with `.actual`, `.expected`, `.diff` attributes.
|
|
146
|
+
- [**Rich pytest diffs**](docs/api.md#rich-pytest-diffs): recursive structural diffs for lists, sets, strings, dicts, dataclasses, namedtuples, Pydantic models. Circular reference protection.
|
|
147
|
+
- [**Snapshot testing**](docs/api.md#snapshot-testing): store and compare data structures in JSON format.
|
|
148
|
+
|
|
149
|
+
**Type safety**
|
|
150
|
+
|
|
151
|
+
- [**Type-aware autocomplete**](#type-aware-autocomplete): 9 Protocols, IDE shows only relevant methods per type.
|
|
152
|
+
- **py.typed**: `Self` return types, PEP 561 compliant ([PEP 561](https://peps.python.org/pep-0561/)).
|
|
153
|
+
|
|
154
|
+
**Extensibility**
|
|
155
|
+
|
|
156
|
+
- [**Custom matchers**](docs/api.md#custom-matchers---registering-domain-matchers): `register_matcher()` for domain-specific matchers, composable with `&`, `|`, `~`.
|
|
157
|
+
- [**Regex group extraction**](docs/api.md#regex-group-extraction): `extracting_group()` and `matches_with_groups()` for regex captures.
|
|
158
|
+
- [**Extensions**](docs/api.md#extension-system---adding-custom-assertions): `add_extension()` for custom assertion methods.
|
|
159
|
+
|
|
160
|
+
**Integrations**
|
|
161
|
+
|
|
162
|
+
- [**Allure**](docs/api.md#allure-integration): auto-attach structured diff and actual/expected data to reports. `pip install assertpy2[allure]`.
|
|
163
|
+
- [**Behave**](docs/api.md#behave-step-matchers): ready-made parameter types for step definitions. `pip install assertpy2[behave]`.
|
|
164
|
+
|
|
165
|
+
See the [full API reference](docs/api.md) for all assertion methods, examples, and advanced features.
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## Integrations
|
|
170
|
+
|
|
171
|
+
### Allure
|
|
172
|
+
|
|
173
|
+
When `allure-pytest` is installed, the pytest plugin auto-attaches structured failure data to Allure reports as JSON attachments.
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
pip install assertpy2[allure]
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Three modes controlled via `pytest.ini` (or `pyproject.toml`):
|
|
180
|
+
|
|
181
|
+
| Mode | What is attached |
|
|
182
|
+
|---|---|
|
|
183
|
+
| `diff` (default) | Structured Diff JSON (path-level breakdown) |
|
|
184
|
+
| `full` | Structured Diff + actual/expected JSON |
|
|
185
|
+
| `off` | Nothing |
|
|
186
|
+
|
|
187
|
+
```toml
|
|
188
|
+
# pyproject.toml
|
|
189
|
+
[tool.pytest.ini_options]
|
|
190
|
+
assertpy2_allure = "full"
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### Behave
|
|
194
|
+
|
|
195
|
+
Ready-made parameter types for Behave step definitions:
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
pip install assertpy2[behave]
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
```py
|
|
202
|
+
# in environment.py or steps/conftest.py
|
|
203
|
+
from assertpy2.behave_matchers import register_assertpy_types
|
|
204
|
+
register_assertpy_types()
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Then use in step definitions:
|
|
208
|
+
|
|
209
|
+
```py
|
|
210
|
+
@given('a user aged {age:PositiveInt}')
|
|
211
|
+
def step_impl(context, age):
|
|
212
|
+
context.age = age # already validated as int > 0
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Available types: `PositiveInt`, `NonNegativeInt`, `PositiveFloat`, `NonEmptyString`, `BoolLike`.
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
<p align="center">
|
|
220
|
+
<a href="https://github.com/Solganis/assertpy2/blob/main/LICENSE">BSD 3-Clause License</a>
|
|
221
|
+
</p>
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<picture>
|
|
3
|
+
<source media="(prefers-color-scheme: dark)" srcset="docs/logo-dark.svg">
|
|
4
|
+
<source media="(prefers-color-scheme: light)" srcset="docs/logo.svg">
|
|
5
|
+
<img src="docs/logo.svg" alt="assertpy2" width="280">
|
|
6
|
+
</picture>
|
|
7
|
+
</p>
|
|
8
|
+
|
|
9
|
+
<p align="center">
|
|
10
|
+
<b>Fluent assertion library for Python with composable matchers, structural matching, and full type safety.</b><br>
|
|
11
|
+
A modern, batteries-included fork of <a href="https://github.com/assertpy/assertpy">assertpy</a>.
|
|
12
|
+
</p>
|
|
13
|
+
|
|
14
|
+
<p align="center">
|
|
15
|
+
<a href="https://github.com/Solganis/assertpy2/actions/workflows/ci.yml"><img src="https://github.com/Solganis/assertpy2/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
|
|
16
|
+
<a href="https://pypi.org/project/assertpy2/"><img src="https://img.shields.io/pypi/v/assertpy2" alt="PyPI version"></a>
|
|
17
|
+
<a href="https://pepy.tech/projects/assertpy2"><img src="https://static.pepy.tech/badge/assertpy2/month" alt="Downloads"></a>
|
|
18
|
+
<a href="https://pypi.org/project/assertpy2/"><img src="https://img.shields.io/pypi/pyversions/assertpy2" alt="Python"></a>
|
|
19
|
+
<a href="https://codecov.io/gh/Solganis/assertpy2"><img src="https://codecov.io/gh/Solganis/assertpy2/graph/badge.svg" alt="Coverage"></a>
|
|
20
|
+
<br>
|
|
21
|
+
<a href="https://github.com/Solganis/assertpy2/blob/main/docs/api.md"><img src="https://img.shields.io/badge/Docs-Read%20The%20Docs-black" alt="Documentation"></a>
|
|
22
|
+
<a href="https://docs.astral.sh/ruff/"><img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json" alt="Ruff"></a>
|
|
23
|
+
<a href="https://github.com/astral-sh/uv"><img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json" alt="uv"></a>
|
|
24
|
+
<a href="https://github.com/astral-sh/ty"><img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ty/main/assets/badge/v0.json" alt="ty"></a>
|
|
25
|
+
<a href="https://scorecard.dev/viewer/?uri=github.com/Solganis/assertpy2"><img src="https://api.scorecard.dev/projects/github.com/Solganis/assertpy2/badge" alt="OpenSSF Scorecard"></a>
|
|
26
|
+
<a href="https://www.bestpractices.dev/projects/12990"><img src="https://www.bestpractices.dev/projects/12990/badge" alt="OpenSSF Best Practices"></a>
|
|
27
|
+
</p>
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Quick start
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install assertpy2 # drop-in replacement for assertpy, just change the import
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
```py
|
|
38
|
+
from assertpy2 import assert_that
|
|
39
|
+
|
|
40
|
+
def test_user():
|
|
41
|
+
user = {"name": "Alice", "age": 30, "roles": ["viewer", "editor"]}
|
|
42
|
+
|
|
43
|
+
assert_that(user).contains_key("name", "age")
|
|
44
|
+
assert_that(user["age"]).is_between(18, 120)
|
|
45
|
+
assert_that(user["roles"]).contains("viewer").does_not_contain("admin")
|
|
46
|
+
assert_that(user).has_name("Alice")
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Why fluent assertions?
|
|
50
|
+
|
|
51
|
+
```py
|
|
52
|
+
# bare assert - passes, but failure message is useless
|
|
53
|
+
assert user["age"] >= 18
|
|
54
|
+
# AssertionError
|
|
55
|
+
|
|
56
|
+
# assertpy2 - same check, clear failure message
|
|
57
|
+
assert_that(user["age"]).is_greater_than_or_equal_to(18)
|
|
58
|
+
# AssertionError: Expected <16> to be greater than or equal to <18>, but was not.
|
|
59
|
+
|
|
60
|
+
# bare assert - three separate statements
|
|
61
|
+
assert isinstance(items, list)
|
|
62
|
+
assert len(items) == 3
|
|
63
|
+
assert "admin" in items
|
|
64
|
+
|
|
65
|
+
# assertpy2 - one fluent chain
|
|
66
|
+
assert_that(items).is_type_of(list).is_length(3).contains("admin")
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Type-aware autocomplete
|
|
70
|
+
|
|
71
|
+
`assert_that()` uses `@overload` to return type-specific Protocols.
|
|
72
|
+
Your IDE shows only methods relevant to the value you're testing, not all 100+:
|
|
73
|
+
|
|
74
|
+
- `assert_that("hello").` → string methods: `starts_with`, `matches`, `is_alpha`, ...
|
|
75
|
+
- `assert_that(42).` → numeric methods: `is_positive`, `is_between`, `is_close_to`, ...
|
|
76
|
+
- `assert_that(Path("/tmp")).` → path methods: `exists`, `is_file`, `is_readable`, ...
|
|
77
|
+
- `assert_that(my_dict).` → dict methods: `contains_key`, `contains_entry`, `has_json_path`, ...
|
|
78
|
+
- `assert_that(b"\x89PNG").` → bytes methods: `starts_with_bytes`, `is_valid_utf8`, `decoded_as`, ...
|
|
79
|
+
|
|
80
|
+
9 type-specific Protocols instead of one `Any`. Works in PyCharm, VS Code, and any LSP-compatible editor.
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Features
|
|
85
|
+
|
|
86
|
+
**Fluent API**
|
|
87
|
+
|
|
88
|
+
- [**Composable matchers**](docs/api.md#composable-matchers): `match.greater_than(5)`, `match.is_uuid()`, combine with `&`, `|`, `~`. Also work with plain `assert ==`.
|
|
89
|
+
- [**Structural matching**](docs/api.md#structural-matching): `matches_structure()` for declarative dict/API response validation.
|
|
90
|
+
- [**Universal negation**](docs/api.md#universal-negation): `.not_` inverts any assertion without dedicated `is_not_*` methods.
|
|
91
|
+
- [**Collection pipeline**](docs/api.md#collection-pipeline): `filtered_on()`, `mapped()`, `flat_mapped()`, `first()`, `last()`, `element()`, `single()`.
|
|
92
|
+
- [**Fluent chaining**](docs/api.md#chaining): write assertions as readable one-liners that chain naturally.
|
|
93
|
+
|
|
94
|
+
**Built-in types**
|
|
95
|
+
|
|
96
|
+
- [Strings](docs/api.md#strings), [numbers](docs/api.md#numbers), [lists](docs/api.md#lists), [tuples](docs/api.md#tuples), [sets](docs/api.md#sets), [dicts](docs/api.md#dicts), [dates](docs/api.md#dates), [booleans](docs/api.md#booleans), [objects](docs/api.md#objects), [bytes](docs/api.md#bytes--bytearray-assertions), [files](docs/api.md#files), [exceptions](docs/api.md#failure).
|
|
97
|
+
- [**Bytes assertions**](docs/api.md#bytes--bytearray-assertions): `is_valid_utf8()`, `starts_with_bytes()`, `is_hex_equal_to()`, `decoded_as()` for `bytes`/`bytearray`.
|
|
98
|
+
- [**JSON assertions**](docs/api.md#json-path--schema-validation): JSONPath navigation and JSON Schema validation. `pip install assertpy2[json]`.
|
|
99
|
+
- [**Dynamic assertions**](docs/api.md#objects): `has_<name>()` for any attribute, property, or zero-argument method.
|
|
100
|
+
- [**Dict comparison**](docs/api.md#dicts): `is_equal_to()` with `ignore` and `include` for selective key/field matching (dicts, dataclasses, namedtuples, Pydantic models, attrs, plain objects).
|
|
101
|
+
- [**Extracting**](docs/api.md#objects): flatten collections on attributes with `filter` and `sort` support.
|
|
102
|
+
|
|
103
|
+
**Testing**
|
|
104
|
+
|
|
105
|
+
- [**Soft assertions**](docs/api.md#soft-assertions): thread-safe, async-safe via `contextvars`. Group errors with `sa.group()`, or use `assert_all()`.
|
|
106
|
+
- [**Async assertions**](docs/api.md#async-assertions): `eventually()` with polling/retry for eventual consistency.
|
|
107
|
+
- [**Structured errors**](docs/api.md#structured-errors): `AssertionFailure` with `.actual`, `.expected`, `.diff` attributes.
|
|
108
|
+
- [**Rich pytest diffs**](docs/api.md#rich-pytest-diffs): recursive structural diffs for lists, sets, strings, dicts, dataclasses, namedtuples, Pydantic models. Circular reference protection.
|
|
109
|
+
- [**Snapshot testing**](docs/api.md#snapshot-testing): store and compare data structures in JSON format.
|
|
110
|
+
|
|
111
|
+
**Type safety**
|
|
112
|
+
|
|
113
|
+
- [**Type-aware autocomplete**](#type-aware-autocomplete): 9 Protocols, IDE shows only relevant methods per type.
|
|
114
|
+
- **py.typed**: `Self` return types, PEP 561 compliant ([PEP 561](https://peps.python.org/pep-0561/)).
|
|
115
|
+
|
|
116
|
+
**Extensibility**
|
|
117
|
+
|
|
118
|
+
- [**Custom matchers**](docs/api.md#custom-matchers---registering-domain-matchers): `register_matcher()` for domain-specific matchers, composable with `&`, `|`, `~`.
|
|
119
|
+
- [**Regex group extraction**](docs/api.md#regex-group-extraction): `extracting_group()` and `matches_with_groups()` for regex captures.
|
|
120
|
+
- [**Extensions**](docs/api.md#extension-system---adding-custom-assertions): `add_extension()` for custom assertion methods.
|
|
121
|
+
|
|
122
|
+
**Integrations**
|
|
123
|
+
|
|
124
|
+
- [**Allure**](docs/api.md#allure-integration): auto-attach structured diff and actual/expected data to reports. `pip install assertpy2[allure]`.
|
|
125
|
+
- [**Behave**](docs/api.md#behave-step-matchers): ready-made parameter types for step definitions. `pip install assertpy2[behave]`.
|
|
126
|
+
|
|
127
|
+
See the [full API reference](docs/api.md) for all assertion methods, examples, and advanced features.
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## Integrations
|
|
132
|
+
|
|
133
|
+
### Allure
|
|
134
|
+
|
|
135
|
+
When `allure-pytest` is installed, the pytest plugin auto-attaches structured failure data to Allure reports as JSON attachments.
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
pip install assertpy2[allure]
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Three modes controlled via `pytest.ini` (or `pyproject.toml`):
|
|
142
|
+
|
|
143
|
+
| Mode | What is attached |
|
|
144
|
+
|---|---|
|
|
145
|
+
| `diff` (default) | Structured Diff JSON (path-level breakdown) |
|
|
146
|
+
| `full` | Structured Diff + actual/expected JSON |
|
|
147
|
+
| `off` | Nothing |
|
|
148
|
+
|
|
149
|
+
```toml
|
|
150
|
+
# pyproject.toml
|
|
151
|
+
[tool.pytest.ini_options]
|
|
152
|
+
assertpy2_allure = "full"
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Behave
|
|
156
|
+
|
|
157
|
+
Ready-made parameter types for Behave step definitions:
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
pip install assertpy2[behave]
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
```py
|
|
164
|
+
# in environment.py or steps/conftest.py
|
|
165
|
+
from assertpy2.behave_matchers import register_assertpy_types
|
|
166
|
+
register_assertpy_types()
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Then use in step definitions:
|
|
170
|
+
|
|
171
|
+
```py
|
|
172
|
+
@given('a user aged {age:PositiveInt}')
|
|
173
|
+
def step_impl(context, age):
|
|
174
|
+
context.age = age # already validated as int > 0
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Available types: `PositiveInt`, `NonNegativeInt`, `PositiveFloat`, `NonEmptyString`, `BoolLike`.
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
<p align="center">
|
|
182
|
+
<a href="https://github.com/Solganis/assertpy2/blob/main/LICENSE">BSD 3-Clause License</a>
|
|
183
|
+
</p>
|
|
@@ -74,6 +74,9 @@ class _MixinBase:
|
|
|
74
74
|
include: object = ...,
|
|
75
75
|
) -> None: ...
|
|
76
76
|
|
|
77
|
+
@staticmethod
|
|
78
|
+
def _to_comparable_dict(obj: object) -> dict[str, object] | None: ...
|
|
79
|
+
|
|
77
80
|
# NumericMixin class attrs used by HelpersMixin._validate_between_args
|
|
78
81
|
_NUMERIC_COMPAREABLE: frozenset[type]
|
|
79
82
|
_NUMERIC_NON_COMPAREABLE: frozenset[type]
|
|
@@ -47,7 +47,7 @@ from .numeric import NumericMixin
|
|
|
47
47
|
from .snapshot import SnapshotMixin
|
|
48
48
|
from .string import StringMixin
|
|
49
49
|
|
|
50
|
-
__version__ = "2.
|
|
50
|
+
__version__ = "2.4.0"
|
|
51
51
|
|
|
52
52
|
__tracebackhide__ = True # clean tracebacks via py.test integration
|
|
53
53
|
contextlib.__tracebackhide__ = True # ty: ignore[unresolved-attribute] # pytest monkey-patch
|