assertpy2 2.0.0__tar.gz → 2.1.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.1.0/.codecov.yml +10 -0
- {assertpy2-2.0.0 → assertpy2-2.1.0}/.github/workflows/ci.yml +5 -2
- assertpy2-2.1.0/CLAUDE.md +113 -0
- {assertpy2-2.0.0 → assertpy2-2.1.0}/PKG-INFO +180 -14
- {assertpy2-2.0.0 → assertpy2-2.1.0}/README.md +179 -13
- {assertpy2-2.0.0 → assertpy2-2.1.0}/assertpy2/__init__.py +9 -0
- assertpy2-2.1.0/assertpy2/_typing.py +152 -0
- {assertpy2-2.0.0 → assertpy2-2.1.0}/assertpy2/assertpy.py +136 -35
- assertpy2-2.1.0/assertpy2/async_assertions.py +83 -0
- {assertpy2-2.0.0 → assertpy2-2.1.0}/assertpy2/base.py +127 -1
- {assertpy2-2.0.0 → assertpy2-2.1.0}/assertpy2/contains.py +20 -4
- assertpy2-2.1.0/assertpy2/errors.py +52 -0
- {assertpy2-2.0.0 → assertpy2-2.1.0}/assertpy2/helpers.py +3 -1
- assertpy2-2.1.0/assertpy2/matchers.py +594 -0
- assertpy2-2.1.0/assertpy2/pytest_plugin.py +42 -0
- {assertpy2-2.0.0 → assertpy2-2.1.0}/docs/api.md +270 -0
- assertpy2-2.1.0/docs/logo-dark.svg +5 -0
- {assertpy2-2.0.0 → assertpy2-2.1.0}/pyproject.toml +6 -2
- assertpy2-2.1.0/tests/test_async.py +157 -0
- assertpy2-2.1.0/tests/test_errors.py +132 -0
- assertpy2-2.1.0/tests/test_matchers.py +529 -0
- assertpy2-2.1.0/tests/test_overloads.py +73 -0
- assertpy2-2.1.0/tests/test_pytest_plugin.py +154 -0
- assertpy2-2.1.0/tests/test_structural.py +271 -0
- {assertpy2-2.0.0 → assertpy2-2.1.0}/tests/test_traceback.py +3 -2
- {assertpy2-2.0.0 → assertpy2-2.1.0}/uv.lock +1 -1
- {assertpy2-2.0.0 → assertpy2-2.1.0}/.github/workflows/publish.yml +0 -0
- {assertpy2-2.0.0 → assertpy2-2.1.0}/.gitignore +0 -0
- {assertpy2-2.0.0 → assertpy2-2.1.0}/CONTRIBUTING.md +0 -0
- {assertpy2-2.0.0 → assertpy2-2.1.0}/LICENSE +0 -0
- {assertpy2-2.0.0 → assertpy2-2.1.0}/assertpy2/collection.py +0 -0
- {assertpy2-2.0.0 → assertpy2-2.1.0}/assertpy2/date.py +0 -0
- {assertpy2-2.0.0 → assertpy2-2.1.0}/assertpy2/dict.py +0 -0
- {assertpy2-2.0.0 → assertpy2-2.1.0}/assertpy2/dynamic.py +0 -0
- {assertpy2-2.0.0 → assertpy2-2.1.0}/assertpy2/exception.py +0 -0
- {assertpy2-2.0.0 → assertpy2-2.1.0}/assertpy2/extracting.py +0 -0
- {assertpy2-2.0.0 → assertpy2-2.1.0}/assertpy2/file.py +0 -0
- {assertpy2-2.0.0 → assertpy2-2.1.0}/assertpy2/numeric.py +0 -0
- {assertpy2-2.0.0 → assertpy2-2.1.0}/assertpy2/py.typed +0 -0
- {assertpy2-2.0.0 → assertpy2-2.1.0}/assertpy2/snapshot.py +0 -0
- {assertpy2-2.0.0 → assertpy2-2.1.0}/assertpy2/string.py +0 -0
- {assertpy2-2.0.0 → assertpy2-2.1.0}/docs/logo.svg +0 -0
- {assertpy2-2.0.0 → assertpy2-2.1.0}/tests/test_bool.py +0 -0
- {assertpy2-2.0.0 → assertpy2-2.1.0}/tests/test_class.py +0 -0
- {assertpy2-2.0.0 → assertpy2-2.1.0}/tests/test_collection.py +0 -0
- {assertpy2-2.0.0 → assertpy2-2.1.0}/tests/test_core.py +0 -0
- {assertpy2-2.0.0 → assertpy2-2.1.0}/tests/test_custom_dict.py +0 -0
- {assertpy2-2.0.0 → assertpy2-2.1.0}/tests/test_custom_list.py +0 -0
- {assertpy2-2.0.0 → assertpy2-2.1.0}/tests/test_datetime.py +0 -0
- {assertpy2-2.0.0 → assertpy2-2.1.0}/tests/test_description.py +0 -0
- {assertpy2-2.0.0 → assertpy2-2.1.0}/tests/test_dict.py +0 -0
- {assertpy2-2.0.0 → assertpy2-2.1.0}/tests/test_dict_compare.py +0 -0
- {assertpy2-2.0.0 → assertpy2-2.1.0}/tests/test_dyn.py +0 -0
- {assertpy2-2.0.0 → assertpy2-2.1.0}/tests/test_equals.py +0 -0
- {assertpy2-2.0.0 → assertpy2-2.1.0}/tests/test_expected_exception.py +0 -0
- {assertpy2-2.0.0 → assertpy2-2.1.0}/tests/test_extensions.py +0 -0
- {assertpy2-2.0.0 → assertpy2-2.1.0}/tests/test_extracting.py +0 -0
- {assertpy2-2.0.0 → assertpy2-2.1.0}/tests/test_fail.py +0 -0
- {assertpy2-2.0.0 → assertpy2-2.1.0}/tests/test_file.py +0 -0
- {assertpy2-2.0.0 → assertpy2-2.1.0}/tests/test_in.py +0 -0
- {assertpy2-2.0.0 → assertpy2-2.1.0}/tests/test_list.py +0 -0
- {assertpy2-2.0.0 → assertpy2-2.1.0}/tests/test_namedtuple.py +0 -0
- {assertpy2-2.0.0 → assertpy2-2.1.0}/tests/test_none.py +0 -0
- {assertpy2-2.0.0 → assertpy2-2.1.0}/tests/test_numbers.py +0 -0
- {assertpy2-2.0.0 → assertpy2-2.1.0}/tests/test_readme.py +0 -0
- {assertpy2-2.0.0 → assertpy2-2.1.0}/tests/test_same_as.py +0 -0
- {assertpy2-2.0.0 → assertpy2-2.1.0}/tests/test_snapshots.py +0 -0
- {assertpy2-2.0.0 → assertpy2-2.1.0}/tests/test_soft.py +0 -0
- {assertpy2-2.0.0 → assertpy2-2.1.0}/tests/test_soft_fail.py +0 -0
- {assertpy2-2.0.0 → assertpy2-2.1.0}/tests/test_string.py +0 -0
- {assertpy2-2.0.0 → assertpy2-2.1.0}/tests/test_type.py +0 -0
- {assertpy2-2.0.0 → assertpy2-2.1.0}/tests/test_warn.py +0 -0
|
@@ -31,11 +31,14 @@ jobs:
|
|
|
31
31
|
run: uv run ruff check .
|
|
32
32
|
|
|
33
33
|
- name: Test with coverage
|
|
34
|
-
run: uv run pytest -v --cov=assertpy2 --cov-report=term-missing tests
|
|
34
|
+
run: uv run pytest -v --cov=assertpy2 --cov-report=term-missing --cov-report=xml tests
|
|
35
35
|
|
|
36
36
|
- name: Upload coverage to Codecov
|
|
37
37
|
if: matrix.python-version == '3.14'
|
|
38
|
-
uses: codecov/codecov-action@
|
|
38
|
+
uses: codecov/codecov-action@v6
|
|
39
|
+
with:
|
|
40
|
+
token: ${{ secrets.CODECOV_TOKEN }}
|
|
41
|
+
files: coverage.xml
|
|
39
42
|
|
|
40
43
|
lint:
|
|
41
44
|
runs-on: ubuntu-latest
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# assertpy2
|
|
2
|
+
|
|
3
|
+
Fluent assertion library for Python. Fork of assertpy с type safety, soft assertions, snapshot testing.
|
|
4
|
+
Python 3.10+, единственная зависимость: typing_extensions>=4.0.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Project layout
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
assertpy2/
|
|
12
|
+
├── assertpy.py # assert_that(), AssertionBuilder, soft_assertions(), fail()
|
|
13
|
+
├── async_assertions.py # AsyncAssertionBuilder - eventually() polling
|
|
14
|
+
├── base.py # BaseMixin - is_equal_to, satisfies, each, matches_structure
|
|
15
|
+
├── string.py # StringMixin - starts_with, ends_with, matches, is_alpha, is_digit
|
|
16
|
+
├── numeric.py # NumericMixin - is_zero, is_positive, is_between, is_close_to
|
|
17
|
+
├── collection.py # CollectionMixin - is_iterable, is_subset_of, is_sorted
|
|
18
|
+
├── contains.py # ContainsMixin - contains, does_not_contain, is_empty
|
|
19
|
+
├── dict.py # DictMixin - contains_key, contains_value, contains_entry
|
|
20
|
+
├── date.py # DateMixin - is_before, is_after, is_equal_to_ignoring_time
|
|
21
|
+
├── file.py # FileMixin + contents_of() - exists, is_file, is_directory
|
|
22
|
+
├── exception.py # ExceptionMixin - raises, when_called_with
|
|
23
|
+
├── extracting.py # ExtractingMixin - extracting with filter/sort
|
|
24
|
+
├── dynamic.py # DynamicMixin - has_<attr>() via __getattr__
|
|
25
|
+
├── snapshot.py # SnapshotMixin - snapshot testing
|
|
26
|
+
├── helpers.py # HelpersMixin - _dict_not_equal, _fmt_items, validation
|
|
27
|
+
├── errors.py # AssertionFailure, DiffResult, DiffEntry - structured errors
|
|
28
|
+
├── matchers.py # Matcher protocol, composable matchers, match namespace
|
|
29
|
+
├── pytest_plugin.py # pytest entry point - rich diff output for AssertionFailure
|
|
30
|
+
├── _typing.py # TYPE_CHECKING-only Protocol classes for @overload return types
|
|
31
|
+
├── __init__.py # Public API exports
|
|
32
|
+
└── py.typed # PEP 561 marker
|
|
33
|
+
|
|
34
|
+
tests/ # 100% coverage required
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Architecture
|
|
40
|
+
|
|
41
|
+
- AssertionBuilder наследует миксины. Каждый миксин - одна категория assertions.
|
|
42
|
+
- Все assertion-методы возвращают Self для chaining.
|
|
43
|
+
- error() в AssertionBuilder маршрутизирует: raise (default), log (warn), collect (soft).
|
|
44
|
+
- __tracebackhide__ = True во всех миксинах для чистого pytest traceback.
|
|
45
|
+
- Расширения через add_extension(func) - динамическая привязка через types.MethodType.
|
|
46
|
+
|
|
47
|
+
Правило: новые assertions добавляются в существующий миксин по категории.
|
|
48
|
+
Новый миксин создаётся только для принципиально новой категории (не для 1-2 методов).
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Tooling
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
uv sync
|
|
56
|
+
uv run pytest
|
|
57
|
+
uv run pytest -v --cov=assertpy2 --cov-report=term-missing
|
|
58
|
+
uv run ruff check .
|
|
59
|
+
uv run ruff format .
|
|
60
|
+
uv run ruff format --check .
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## CI/CD
|
|
66
|
+
|
|
67
|
+
GitHub Actions, два workflow:
|
|
68
|
+
|
|
69
|
+
**CI** (.github/workflows/ci.yml):
|
|
70
|
+
- Триггер: push/PR в main
|
|
71
|
+
- Матрица: Python 3.10-3.15
|
|
72
|
+
- Шаги: uv sync, ruff check, pytest с coverage (xml + term-missing)
|
|
73
|
+
- Codecov upload: только с Python 3.14 (token в secrets)
|
|
74
|
+
- Отдельный lint job: ruff check + ruff format --check
|
|
75
|
+
|
|
76
|
+
**Publish** (.github/workflows/publish.yml):
|
|
77
|
+
- Триггер: GitHub Release (published)
|
|
78
|
+
- Trusted Publisher (id-token: write), без API-ключей
|
|
79
|
+
- uv build, pypa/gh-action-pypi-publish
|
|
80
|
+
|
|
81
|
+
Правила:
|
|
82
|
+
- Версия только в pyproject.toml (одно место)
|
|
83
|
+
- Релиз: обновить version в pyproject.toml, создать GitHub Release с тегом
|
|
84
|
+
- Не мержить без зелёного CI
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## Key dependencies
|
|
89
|
+
|
|
90
|
+
| Package | Version | Role |
|
|
91
|
+
|---|---|---|
|
|
92
|
+
| typing_extensions | >=4.0 | Self type, runtime-free typing |
|
|
93
|
+
| pytest | >=9.0.3 | test runner (dev) |
|
|
94
|
+
| pytest-cov | >=6.1 | coverage (dev) |
|
|
95
|
+
| ruff | >=0.15.14 | linter + formatter (dev) |
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## Naming
|
|
100
|
+
|
|
101
|
+
- Assertion-методы: is_*, has_*, does_not_*, contains_*, starts_with, ends_with
|
|
102
|
+
- Новые assertions следуют существующему паттерну: глагол + предикат
|
|
103
|
+
- Тестовые файлы: test_<feature>.py
|
|
104
|
+
- Миксины: <Category>Mixin
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## Testing
|
|
109
|
+
|
|
110
|
+
- Coverage 100%. Каждый assertion-метод покрыт: happy path, error path, edge cases.
|
|
111
|
+
- Тесты используют pytest.raises(AssertionError) для проверки сообщений об ошибках.
|
|
112
|
+
- match= в pytest.raises для валидации текста ошибки.
|
|
113
|
+
- Snapshot-тесты хранят данные в tests/__snapshots__/.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: assertpy2
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.1.0
|
|
4
4
|
Summary: Fluent assertion library for Python with full type safety and soft assertions
|
|
5
5
|
Project-URL: Homepage, https://github.com/Solganis/assertpy2
|
|
6
6
|
Project-URL: Repository, https://github.com/Solganis/assertpy2
|
|
@@ -30,11 +30,15 @@ Requires-Dist: typing-extensions>=4.0
|
|
|
30
30
|
Description-Content-Type: text/markdown
|
|
31
31
|
|
|
32
32
|
<p align="center">
|
|
33
|
-
<
|
|
33
|
+
<picture>
|
|
34
|
+
<source media="(prefers-color-scheme: dark)" srcset="docs/logo-dark.svg">
|
|
35
|
+
<source media="(prefers-color-scheme: light)" srcset="docs/logo.svg">
|
|
36
|
+
<img src="docs/logo.svg" alt="assertpy2" width="280">
|
|
37
|
+
</picture>
|
|
34
38
|
</p>
|
|
35
39
|
|
|
36
40
|
<p align="center">
|
|
37
|
-
<b>Fluent assertion library for Python with
|
|
41
|
+
<b>Fluent assertion library for Python with composable matchers, structural matching, and full type safety.</b><br>
|
|
38
42
|
Maintained fork of <a href="https://github.com/assertpy/assertpy">assertpy</a>.
|
|
39
43
|
</p>
|
|
40
44
|
|
|
@@ -45,6 +49,7 @@ Description-Content-Type: text/markdown
|
|
|
45
49
|
<a href="https://pypi.org/project/assertpy2/"><img src="https://img.shields.io/pypi/pyversions/assertpy2" alt="Python"></a>
|
|
46
50
|
<a href="https://codecov.io/gh/Solganis/assertpy2"><img src="https://codecov.io/gh/Solganis/assertpy2/graph/badge.svg" alt="Coverage"></a>
|
|
47
51
|
<a href="https://github.com/Solganis/assertpy2/blob/main/LICENSE"><img src="https://img.shields.io/github/license/Solganis/assertpy2" alt="License"></a>
|
|
52
|
+
<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>
|
|
48
53
|
</p>
|
|
49
54
|
|
|
50
55
|
|
|
@@ -66,14 +71,41 @@ def test_user():
|
|
|
66
71
|
assert_that(user).has_name("Alice")
|
|
67
72
|
```
|
|
68
73
|
|
|
69
|
-
|
|
74
|
+
Composable matchers and structural matching:
|
|
70
75
|
|
|
71
76
|
```py
|
|
72
|
-
assert_that
|
|
73
|
-
|
|
77
|
+
from assertpy2 import assert_that, match
|
|
78
|
+
|
|
79
|
+
# matchers with & | ~ operators
|
|
80
|
+
assert_that([3, 7, 12]).contains(match.greater_than(10))
|
|
81
|
+
assert_that(42).satisfies(match.greater_than(0) & match.less_than(100))
|
|
82
|
+
|
|
83
|
+
# validate dict structure declaratively
|
|
84
|
+
assert_that(api_response).matches_structure({
|
|
85
|
+
"id": match.is_uuid(),
|
|
86
|
+
"name": match.equal_to("Alice"),
|
|
87
|
+
"status": match.is_non_empty_string(),
|
|
88
|
+
})
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Structured errors with rich data:
|
|
92
|
+
|
|
93
|
+
```py
|
|
94
|
+
try:
|
|
95
|
+
assert_that({"a": 1, "b": 2}).is_equal_to({"a": 1, "b": 99})
|
|
96
|
+
except AssertionError as e:
|
|
97
|
+
e.actual # {"a": 1, "b": 2}
|
|
98
|
+
e.expected # {"a": 1, "b": 99}
|
|
99
|
+
e.diff # DiffResult(kind='dict', entries=[DiffEntry(path='b', actual=2, expected=99)])
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
The pytest plugin auto-renders this as rich diff sections in failure reports:
|
|
74
103
|
|
|
75
|
-
|
|
76
|
-
|
|
104
|
+
```
|
|
105
|
+
FAILED test_example.py::test_comparison
|
|
106
|
+
--- AssertionFailure ---
|
|
107
|
+
actual: {'a': 1, 'b': 2}
|
|
108
|
+
expected: {'a': 1, 'b': 99}
|
|
77
109
|
```
|
|
78
110
|
|
|
79
111
|
|
|
@@ -100,9 +132,13 @@ assert_that(items).is_type_of(list).is_length(3).contains("admin")
|
|
|
100
132
|
|
|
101
133
|
## Features
|
|
102
134
|
|
|
135
|
+
- **Composable matchers**: `match.greater_than(5)`, `match.is_uuid()`, combine with `&`, `|`, `~` operators.
|
|
136
|
+
- **Structural matching**: `matches_structure()` for declarative dict/API response validation.
|
|
137
|
+
- **Async assertions**: `eventually()` with polling/retry for async and eventual consistency testing.
|
|
138
|
+
- **Structured errors**: `AssertionFailure` with `.actual`, `.expected`, `.diff` attributes, pytest plugin with rich diff output.
|
|
139
|
+
- **Typed overloads**: `assert_that()` returns type-specific Protocols, IDE shows only relevant methods per type.
|
|
103
140
|
- **Type safety**: `Self` return types, `py.typed` ([PEP 561](https://peps.python.org/pep-0561/)).
|
|
104
|
-
- **
|
|
105
|
-
- **Soft assertions**: collect all failures with `soft_assertions()`, plus `soft_fail()` for non-halting explicit failures.
|
|
141
|
+
- **Soft assertions**: thread-safe and async-safe via `contextvars`, collect all failures with `soft_assertions()`.
|
|
106
142
|
- **Fluent chaining**: write assertions as readable one-liners that chain naturally.
|
|
107
143
|
- **Dynamic assertions**: `has_<name>()` for any attribute, property, or zero-argument method on objects and dicts.
|
|
108
144
|
- **Dict comparison**: `is_equal_to()` with `ignore` and `include` for selective key matching.
|
|
@@ -113,6 +149,69 @@ assert_that(items).is_type_of(list).is_length(3).contains("admin")
|
|
|
113
149
|
- Strings, numbers, lists, tuples, sets, dicts, dates, booleans, objects, exceptions.
|
|
114
150
|
|
|
115
151
|
|
|
152
|
+
## Composable matchers
|
|
153
|
+
|
|
154
|
+
Matchers are objects that describe conditions. Combine them with `&` (and), `|` (or), `~` (not):
|
|
155
|
+
|
|
156
|
+
```py
|
|
157
|
+
from assertpy2 import assert_that, match
|
|
158
|
+
|
|
159
|
+
# check a value against a composed condition
|
|
160
|
+
assert_that(42).satisfies(match.greater_than(0) & match.less_than(100))
|
|
161
|
+
|
|
162
|
+
# matchers inside contains - find element by condition
|
|
163
|
+
assert_that([3, 7, 12]).contains(match.greater_than(10))
|
|
164
|
+
|
|
165
|
+
# check every element in a collection
|
|
166
|
+
assert_that([18, 25, 30]).each(match.between(18, 120))
|
|
167
|
+
|
|
168
|
+
# invert with ~
|
|
169
|
+
assert_that("hello").satisfies(~match.equal_to("world"))
|
|
170
|
+
|
|
171
|
+
# combine freely
|
|
172
|
+
assert_that(150).satisfies(match.is_negative() | match.greater_than(100))
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Available matchers: `equal_to`, `greater_than`, `greater_than_or_equal_to`, `less_than`, `less_than_or_equal_to`, `between`, `close_to`, `is_none`, `is_not_none`, `is_instance_of`, `has_length`, `is_empty`, `is_not_empty`, `is_positive`, `is_negative`, `contains_string`, `matches_regex`, `is_uuid`, `is_non_empty_string`, `ignore`, `each_item`, `structure`.
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
## Structural matching
|
|
179
|
+
|
|
180
|
+
Validate dict structure declaratively, even when values are dynamic (UUIDs, timestamps):
|
|
181
|
+
|
|
182
|
+
```py
|
|
183
|
+
from assertpy2 import assert_that, match
|
|
184
|
+
|
|
185
|
+
assert_that(api_response).matches_structure({
|
|
186
|
+
"id": match.is_uuid(),
|
|
187
|
+
"name": match.equal_to("Alice"),
|
|
188
|
+
"created_at": match.is_non_empty_string(),
|
|
189
|
+
"metadata": match.structure({
|
|
190
|
+
"version": match.greater_than(0),
|
|
191
|
+
"tags": match.each_item(match.is_instance_of(str)),
|
|
192
|
+
}),
|
|
193
|
+
"debug_info": match.ignore(),
|
|
194
|
+
})
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
## Async assertions
|
|
199
|
+
|
|
200
|
+
Poll a callable until the assertion passes or timeout is reached:
|
|
201
|
+
|
|
202
|
+
```py
|
|
203
|
+
from assertpy2 import assert_that
|
|
204
|
+
|
|
205
|
+
async def test_eventual_consistency():
|
|
206
|
+
await assert_that(get_status).eventually().within(5).every(0.5).is_equal_to("ready")
|
|
207
|
+
|
|
208
|
+
# works with async callables
|
|
209
|
+
await assert_that(async_get_count).eventually().within(10).is_greater_than(100)
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Any assertion method is available after `eventually()`. Only `AssertionError` is retried, other exceptions propagate immediately.
|
|
213
|
+
|
|
214
|
+
|
|
116
215
|
## Soft assertions
|
|
117
216
|
|
|
118
217
|
Collect all failures instead of stopping at the first one:
|
|
@@ -138,8 +237,45 @@ AssertionError: soft assertion failures:
|
|
|
138
237
|
|
|
139
238
|
Use `soft_fail("message")` inside the block for non-halting explicit failures (unlike `fail()`, which stops immediately).
|
|
140
239
|
|
|
240
|
+
Soft assertions are thread-safe and async-safe: each thread and each `asyncio` task gets independent state via `contextvars`.
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
## Structured errors
|
|
244
|
+
|
|
245
|
+
When assertions fail, `AssertionFailure` carries structured data alongside the human-readable message:
|
|
246
|
+
|
|
247
|
+
```py
|
|
248
|
+
try:
|
|
249
|
+
assert_that(1).is_equal_to(2)
|
|
250
|
+
except AssertionError as e:
|
|
251
|
+
e.actual # 1
|
|
252
|
+
e.expected # 2
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
For dict comparisons, a `DiffResult` with per-key diff entries is available:
|
|
256
|
+
|
|
257
|
+
```py
|
|
258
|
+
try:
|
|
259
|
+
assert_that({"a": 1, "b": 2}).is_equal_to({"a": 1, "b": 99})
|
|
260
|
+
except AssertionError as e:
|
|
261
|
+
e.diff # DiffResult(kind='dict', entries=[DiffEntry(path='b', actual=2, expected=99)])
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
`AssertionFailure` is a subclass of `AssertionError`, so all existing `except AssertionError` handlers work unchanged.
|
|
141
265
|
|
|
142
|
-
|
|
266
|
+
The pytest plugin (auto-registered, no configuration needed) renders structured data as extra sections in failure reports:
|
|
267
|
+
|
|
268
|
+
```
|
|
269
|
+
FAILED test_example.py::test_comparison
|
|
270
|
+
--- AssertionFailure ---
|
|
271
|
+
actual: {'a': 1, 'b': 2}
|
|
272
|
+
expected: {'a': 1, 'b': 99}
|
|
273
|
+
--- Structured Diff ---
|
|
274
|
+
DiffResult(kind='dict', entries=[DiffEntry(path='b', actual=2, expected=99)])
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
## More features
|
|
143
279
|
|
|
144
280
|
### Dict comparison with ignore/include
|
|
145
281
|
|
|
@@ -168,12 +304,37 @@ assert_that(some_func).raises(RuntimeError).when_called_with("bad_arg")\
|
|
|
168
304
|
.is_length(8).starts_with("some").is_equal_to("some err")
|
|
169
305
|
```
|
|
170
306
|
|
|
307
|
+
### Dynamic assertions
|
|
308
|
+
|
|
309
|
+
```py
|
|
310
|
+
fred = {"first_name": "Fred", "last_name": "Smith", "shoe_size": 12}
|
|
311
|
+
|
|
312
|
+
assert_that(fred).has_first_name("Fred")
|
|
313
|
+
assert_that(fred).has_last_name("Smith")
|
|
314
|
+
assert_that(fred).has_shoe_size(12)
|
|
315
|
+
```
|
|
316
|
+
|
|
171
317
|
### Snapshot testing
|
|
172
318
|
|
|
173
319
|
```py
|
|
174
320
|
assert_that({"a": 1, "b": 2, "c": 3}).snapshot()
|
|
175
321
|
```
|
|
176
322
|
|
|
323
|
+
### Extensions
|
|
324
|
+
|
|
325
|
+
```py
|
|
326
|
+
from assertpy2 import add_extension
|
|
327
|
+
|
|
328
|
+
def is_even(self):
|
|
329
|
+
if self.val % 2 != 0:
|
|
330
|
+
return self.error(f'{self.val} is not even!')
|
|
331
|
+
return self
|
|
332
|
+
|
|
333
|
+
add_extension(is_even)
|
|
334
|
+
|
|
335
|
+
assert_that(4).is_even()
|
|
336
|
+
```
|
|
337
|
+
|
|
177
338
|
See the [full API reference](docs/api.md) for all assertion methods, examples, and advanced features.
|
|
178
339
|
|
|
179
340
|
|
|
@@ -195,9 +356,14 @@ from assertpy2 import assert_that, soft_assertions
|
|
|
195
356
|
|---|---|---|
|
|
196
357
|
| Maintained | Last release 2020 | Active |
|
|
197
358
|
| Python | 2.7+ | 3.10-3.15 |
|
|
198
|
-
| Type safety | No annotations | `Self` return types, `py.typed` (
|
|
199
|
-
| IDE support | No type info | Full autocomplete
|
|
200
|
-
|
|
|
359
|
+
| Type safety | No annotations | `Self` return types, `py.typed`, typed `@overload` on `assert_that()` |
|
|
360
|
+
| IDE support | No type info | Full autocomplete, type-specific method suggestions |
|
|
361
|
+
| Matchers | None | Composable matchers with `&` `\|` `~` operators |
|
|
362
|
+
| Structural matching | None | `matches_structure()` with recursive dict validation |
|
|
363
|
+
| Async | None | `eventually()` with polling/retry |
|
|
364
|
+
| Error reporting | Flat strings | `AssertionFailure` with `.actual`, `.expected`, `.diff` |
|
|
365
|
+
| Pytest integration | None | Rich diff sections in failure reports |
|
|
366
|
+
| Soft assertions | Global state, not thread-safe | `contextvars`, thread-safe and async-safe |
|
|
201
367
|
| Security | [CVE in snapshots](https://github.com/assertpy/assertpy/issues/156) | Fixed |
|
|
202
368
|
| Open bugs | 15+ unresolved | All resolved |
|
|
203
369
|
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<
|
|
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>
|
|
3
7
|
</p>
|
|
4
8
|
|
|
5
9
|
<p align="center">
|
|
6
|
-
<b>Fluent assertion library for Python with
|
|
10
|
+
<b>Fluent assertion library for Python with composable matchers, structural matching, and full type safety.</b><br>
|
|
7
11
|
Maintained fork of <a href="https://github.com/assertpy/assertpy">assertpy</a>.
|
|
8
12
|
</p>
|
|
9
13
|
|
|
@@ -14,6 +18,7 @@
|
|
|
14
18
|
<a href="https://pypi.org/project/assertpy2/"><img src="https://img.shields.io/pypi/pyversions/assertpy2" alt="Python"></a>
|
|
15
19
|
<a href="https://codecov.io/gh/Solganis/assertpy2"><img src="https://codecov.io/gh/Solganis/assertpy2/graph/badge.svg" alt="Coverage"></a>
|
|
16
20
|
<a href="https://github.com/Solganis/assertpy2/blob/main/LICENSE"><img src="https://img.shields.io/github/license/Solganis/assertpy2" alt="License"></a>
|
|
21
|
+
<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>
|
|
17
22
|
</p>
|
|
18
23
|
|
|
19
24
|
|
|
@@ -35,14 +40,41 @@ def test_user():
|
|
|
35
40
|
assert_that(user).has_name("Alice")
|
|
36
41
|
```
|
|
37
42
|
|
|
38
|
-
|
|
43
|
+
Composable matchers and structural matching:
|
|
39
44
|
|
|
40
45
|
```py
|
|
41
|
-
assert_that
|
|
42
|
-
|
|
46
|
+
from assertpy2 import assert_that, match
|
|
47
|
+
|
|
48
|
+
# matchers with & | ~ operators
|
|
49
|
+
assert_that([3, 7, 12]).contains(match.greater_than(10))
|
|
50
|
+
assert_that(42).satisfies(match.greater_than(0) & match.less_than(100))
|
|
51
|
+
|
|
52
|
+
# validate dict structure declaratively
|
|
53
|
+
assert_that(api_response).matches_structure({
|
|
54
|
+
"id": match.is_uuid(),
|
|
55
|
+
"name": match.equal_to("Alice"),
|
|
56
|
+
"status": match.is_non_empty_string(),
|
|
57
|
+
})
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Structured errors with rich data:
|
|
61
|
+
|
|
62
|
+
```py
|
|
63
|
+
try:
|
|
64
|
+
assert_that({"a": 1, "b": 2}).is_equal_to({"a": 1, "b": 99})
|
|
65
|
+
except AssertionError as e:
|
|
66
|
+
e.actual # {"a": 1, "b": 2}
|
|
67
|
+
e.expected # {"a": 1, "b": 99}
|
|
68
|
+
e.diff # DiffResult(kind='dict', entries=[DiffEntry(path='b', actual=2, expected=99)])
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
The pytest plugin auto-renders this as rich diff sections in failure reports:
|
|
43
72
|
|
|
44
|
-
|
|
45
|
-
|
|
73
|
+
```
|
|
74
|
+
FAILED test_example.py::test_comparison
|
|
75
|
+
--- AssertionFailure ---
|
|
76
|
+
actual: {'a': 1, 'b': 2}
|
|
77
|
+
expected: {'a': 1, 'b': 99}
|
|
46
78
|
```
|
|
47
79
|
|
|
48
80
|
|
|
@@ -69,9 +101,13 @@ assert_that(items).is_type_of(list).is_length(3).contains("admin")
|
|
|
69
101
|
|
|
70
102
|
## Features
|
|
71
103
|
|
|
104
|
+
- **Composable matchers**: `match.greater_than(5)`, `match.is_uuid()`, combine with `&`, `|`, `~` operators.
|
|
105
|
+
- **Structural matching**: `matches_structure()` for declarative dict/API response validation.
|
|
106
|
+
- **Async assertions**: `eventually()` with polling/retry for async and eventual consistency testing.
|
|
107
|
+
- **Structured errors**: `AssertionFailure` with `.actual`, `.expected`, `.diff` attributes, pytest plugin with rich diff output.
|
|
108
|
+
- **Typed overloads**: `assert_that()` returns type-specific Protocols, IDE shows only relevant methods per type.
|
|
72
109
|
- **Type safety**: `Self` return types, `py.typed` ([PEP 561](https://peps.python.org/pep-0561/)).
|
|
73
|
-
- **
|
|
74
|
-
- **Soft assertions**: collect all failures with `soft_assertions()`, plus `soft_fail()` for non-halting explicit failures.
|
|
110
|
+
- **Soft assertions**: thread-safe and async-safe via `contextvars`, collect all failures with `soft_assertions()`.
|
|
75
111
|
- **Fluent chaining**: write assertions as readable one-liners that chain naturally.
|
|
76
112
|
- **Dynamic assertions**: `has_<name>()` for any attribute, property, or zero-argument method on objects and dicts.
|
|
77
113
|
- **Dict comparison**: `is_equal_to()` with `ignore` and `include` for selective key matching.
|
|
@@ -82,6 +118,69 @@ assert_that(items).is_type_of(list).is_length(3).contains("admin")
|
|
|
82
118
|
- Strings, numbers, lists, tuples, sets, dicts, dates, booleans, objects, exceptions.
|
|
83
119
|
|
|
84
120
|
|
|
121
|
+
## Composable matchers
|
|
122
|
+
|
|
123
|
+
Matchers are objects that describe conditions. Combine them with `&` (and), `|` (or), `~` (not):
|
|
124
|
+
|
|
125
|
+
```py
|
|
126
|
+
from assertpy2 import assert_that, match
|
|
127
|
+
|
|
128
|
+
# check a value against a composed condition
|
|
129
|
+
assert_that(42).satisfies(match.greater_than(0) & match.less_than(100))
|
|
130
|
+
|
|
131
|
+
# matchers inside contains - find element by condition
|
|
132
|
+
assert_that([3, 7, 12]).contains(match.greater_than(10))
|
|
133
|
+
|
|
134
|
+
# check every element in a collection
|
|
135
|
+
assert_that([18, 25, 30]).each(match.between(18, 120))
|
|
136
|
+
|
|
137
|
+
# invert with ~
|
|
138
|
+
assert_that("hello").satisfies(~match.equal_to("world"))
|
|
139
|
+
|
|
140
|
+
# combine freely
|
|
141
|
+
assert_that(150).satisfies(match.is_negative() | match.greater_than(100))
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Available matchers: `equal_to`, `greater_than`, `greater_than_or_equal_to`, `less_than`, `less_than_or_equal_to`, `between`, `close_to`, `is_none`, `is_not_none`, `is_instance_of`, `has_length`, `is_empty`, `is_not_empty`, `is_positive`, `is_negative`, `contains_string`, `matches_regex`, `is_uuid`, `is_non_empty_string`, `ignore`, `each_item`, `structure`.
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
## Structural matching
|
|
148
|
+
|
|
149
|
+
Validate dict structure declaratively, even when values are dynamic (UUIDs, timestamps):
|
|
150
|
+
|
|
151
|
+
```py
|
|
152
|
+
from assertpy2 import assert_that, match
|
|
153
|
+
|
|
154
|
+
assert_that(api_response).matches_structure({
|
|
155
|
+
"id": match.is_uuid(),
|
|
156
|
+
"name": match.equal_to("Alice"),
|
|
157
|
+
"created_at": match.is_non_empty_string(),
|
|
158
|
+
"metadata": match.structure({
|
|
159
|
+
"version": match.greater_than(0),
|
|
160
|
+
"tags": match.each_item(match.is_instance_of(str)),
|
|
161
|
+
}),
|
|
162
|
+
"debug_info": match.ignore(),
|
|
163
|
+
})
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
## Async assertions
|
|
168
|
+
|
|
169
|
+
Poll a callable until the assertion passes or timeout is reached:
|
|
170
|
+
|
|
171
|
+
```py
|
|
172
|
+
from assertpy2 import assert_that
|
|
173
|
+
|
|
174
|
+
async def test_eventual_consistency():
|
|
175
|
+
await assert_that(get_status).eventually().within(5).every(0.5).is_equal_to("ready")
|
|
176
|
+
|
|
177
|
+
# works with async callables
|
|
178
|
+
await assert_that(async_get_count).eventually().within(10).is_greater_than(100)
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Any assertion method is available after `eventually()`. Only `AssertionError` is retried, other exceptions propagate immediately.
|
|
182
|
+
|
|
183
|
+
|
|
85
184
|
## Soft assertions
|
|
86
185
|
|
|
87
186
|
Collect all failures instead of stopping at the first one:
|
|
@@ -107,8 +206,45 @@ AssertionError: soft assertion failures:
|
|
|
107
206
|
|
|
108
207
|
Use `soft_fail("message")` inside the block for non-halting explicit failures (unlike `fail()`, which stops immediately).
|
|
109
208
|
|
|
209
|
+
Soft assertions are thread-safe and async-safe: each thread and each `asyncio` task gets independent state via `contextvars`.
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
## Structured errors
|
|
213
|
+
|
|
214
|
+
When assertions fail, `AssertionFailure` carries structured data alongside the human-readable message:
|
|
215
|
+
|
|
216
|
+
```py
|
|
217
|
+
try:
|
|
218
|
+
assert_that(1).is_equal_to(2)
|
|
219
|
+
except AssertionError as e:
|
|
220
|
+
e.actual # 1
|
|
221
|
+
e.expected # 2
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
For dict comparisons, a `DiffResult` with per-key diff entries is available:
|
|
225
|
+
|
|
226
|
+
```py
|
|
227
|
+
try:
|
|
228
|
+
assert_that({"a": 1, "b": 2}).is_equal_to({"a": 1, "b": 99})
|
|
229
|
+
except AssertionError as e:
|
|
230
|
+
e.diff # DiffResult(kind='dict', entries=[DiffEntry(path='b', actual=2, expected=99)])
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
`AssertionFailure` is a subclass of `AssertionError`, so all existing `except AssertionError` handlers work unchanged.
|
|
110
234
|
|
|
111
|
-
|
|
235
|
+
The pytest plugin (auto-registered, no configuration needed) renders structured data as extra sections in failure reports:
|
|
236
|
+
|
|
237
|
+
```
|
|
238
|
+
FAILED test_example.py::test_comparison
|
|
239
|
+
--- AssertionFailure ---
|
|
240
|
+
actual: {'a': 1, 'b': 2}
|
|
241
|
+
expected: {'a': 1, 'b': 99}
|
|
242
|
+
--- Structured Diff ---
|
|
243
|
+
DiffResult(kind='dict', entries=[DiffEntry(path='b', actual=2, expected=99)])
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
## More features
|
|
112
248
|
|
|
113
249
|
### Dict comparison with ignore/include
|
|
114
250
|
|
|
@@ -137,12 +273,37 @@ assert_that(some_func).raises(RuntimeError).when_called_with("bad_arg")\
|
|
|
137
273
|
.is_length(8).starts_with("some").is_equal_to("some err")
|
|
138
274
|
```
|
|
139
275
|
|
|
276
|
+
### Dynamic assertions
|
|
277
|
+
|
|
278
|
+
```py
|
|
279
|
+
fred = {"first_name": "Fred", "last_name": "Smith", "shoe_size": 12}
|
|
280
|
+
|
|
281
|
+
assert_that(fred).has_first_name("Fred")
|
|
282
|
+
assert_that(fred).has_last_name("Smith")
|
|
283
|
+
assert_that(fred).has_shoe_size(12)
|
|
284
|
+
```
|
|
285
|
+
|
|
140
286
|
### Snapshot testing
|
|
141
287
|
|
|
142
288
|
```py
|
|
143
289
|
assert_that({"a": 1, "b": 2, "c": 3}).snapshot()
|
|
144
290
|
```
|
|
145
291
|
|
|
292
|
+
### Extensions
|
|
293
|
+
|
|
294
|
+
```py
|
|
295
|
+
from assertpy2 import add_extension
|
|
296
|
+
|
|
297
|
+
def is_even(self):
|
|
298
|
+
if self.val % 2 != 0:
|
|
299
|
+
return self.error(f'{self.val} is not even!')
|
|
300
|
+
return self
|
|
301
|
+
|
|
302
|
+
add_extension(is_even)
|
|
303
|
+
|
|
304
|
+
assert_that(4).is_even()
|
|
305
|
+
```
|
|
306
|
+
|
|
146
307
|
See the [full API reference](docs/api.md) for all assertion methods, examples, and advanced features.
|
|
147
308
|
|
|
148
309
|
|
|
@@ -164,9 +325,14 @@ from assertpy2 import assert_that, soft_assertions
|
|
|
164
325
|
|---|---|---|
|
|
165
326
|
| Maintained | Last release 2020 | Active |
|
|
166
327
|
| Python | 2.7+ | 3.10-3.15 |
|
|
167
|
-
| Type safety | No annotations | `Self` return types, `py.typed` (
|
|
168
|
-
| IDE support | No type info | Full autocomplete
|
|
169
|
-
|
|
|
328
|
+
| Type safety | No annotations | `Self` return types, `py.typed`, typed `@overload` on `assert_that()` |
|
|
329
|
+
| IDE support | No type info | Full autocomplete, type-specific method suggestions |
|
|
330
|
+
| Matchers | None | Composable matchers with `&` `\|` `~` operators |
|
|
331
|
+
| Structural matching | None | `matches_structure()` with recursive dict validation |
|
|
332
|
+
| Async | None | `eventually()` with polling/retry |
|
|
333
|
+
| Error reporting | Flat strings | `AssertionFailure` with `.actual`, `.expected`, `.diff` |
|
|
334
|
+
| Pytest integration | None | Rich diff sections in failure reports |
|
|
335
|
+
| Soft assertions | Global state, not thread-safe | `contextvars`, thread-safe and async-safe |
|
|
170
336
|
| Security | [CVE in snapshots](https://github.com/assertpy/assertpy/issues/156) | Fixed |
|
|
171
337
|
| Open bugs | 15+ unresolved | All resolved |
|
|
172
338
|
|