assertpy2 2.0.1__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/CLAUDE.md +113 -0
- {assertpy2-2.0.1 → assertpy2-2.1.0}/PKG-INFO +174 -13
- {assertpy2-2.0.1 → assertpy2-2.1.0}/README.md +173 -12
- {assertpy2-2.0.1 → assertpy2-2.1.0}/assertpy2/__init__.py +9 -0
- assertpy2-2.1.0/assertpy2/_typing.py +152 -0
- {assertpy2-2.0.1 → assertpy2-2.1.0}/assertpy2/assertpy.py +136 -35
- assertpy2-2.1.0/assertpy2/async_assertions.py +83 -0
- {assertpy2-2.0.1 → assertpy2-2.1.0}/assertpy2/base.py +127 -1
- {assertpy2-2.0.1 → assertpy2-2.1.0}/assertpy2/contains.py +20 -4
- assertpy2-2.1.0/assertpy2/errors.py +52 -0
- {assertpy2-2.0.1 → 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.1 → assertpy2-2.1.0}/docs/api.md +270 -0
- {assertpy2-2.0.1 → 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.1 → assertpy2-2.1.0}/tests/test_traceback.py +3 -2
- {assertpy2-2.0.1 → assertpy2-2.1.0}/uv.lock +1 -1
- {assertpy2-2.0.1 → assertpy2-2.1.0}/.codecov.yml +0 -0
- {assertpy2-2.0.1 → assertpy2-2.1.0}/.github/workflows/ci.yml +0 -0
- {assertpy2-2.0.1 → assertpy2-2.1.0}/.github/workflows/publish.yml +0 -0
- {assertpy2-2.0.1 → assertpy2-2.1.0}/.gitignore +0 -0
- {assertpy2-2.0.1 → assertpy2-2.1.0}/CONTRIBUTING.md +0 -0
- {assertpy2-2.0.1 → assertpy2-2.1.0}/LICENSE +0 -0
- {assertpy2-2.0.1 → assertpy2-2.1.0}/assertpy2/collection.py +0 -0
- {assertpy2-2.0.1 → assertpy2-2.1.0}/assertpy2/date.py +0 -0
- {assertpy2-2.0.1 → assertpy2-2.1.0}/assertpy2/dict.py +0 -0
- {assertpy2-2.0.1 → assertpy2-2.1.0}/assertpy2/dynamic.py +0 -0
- {assertpy2-2.0.1 → assertpy2-2.1.0}/assertpy2/exception.py +0 -0
- {assertpy2-2.0.1 → assertpy2-2.1.0}/assertpy2/extracting.py +0 -0
- {assertpy2-2.0.1 → assertpy2-2.1.0}/assertpy2/file.py +0 -0
- {assertpy2-2.0.1 → assertpy2-2.1.0}/assertpy2/numeric.py +0 -0
- {assertpy2-2.0.1 → assertpy2-2.1.0}/assertpy2/py.typed +0 -0
- {assertpy2-2.0.1 → assertpy2-2.1.0}/assertpy2/snapshot.py +0 -0
- {assertpy2-2.0.1 → assertpy2-2.1.0}/assertpy2/string.py +0 -0
- {assertpy2-2.0.1 → assertpy2-2.1.0}/docs/logo-dark.svg +0 -0
- {assertpy2-2.0.1 → assertpy2-2.1.0}/docs/logo.svg +0 -0
- {assertpy2-2.0.1 → assertpy2-2.1.0}/tests/test_bool.py +0 -0
- {assertpy2-2.0.1 → assertpy2-2.1.0}/tests/test_class.py +0 -0
- {assertpy2-2.0.1 → assertpy2-2.1.0}/tests/test_collection.py +0 -0
- {assertpy2-2.0.1 → assertpy2-2.1.0}/tests/test_core.py +0 -0
- {assertpy2-2.0.1 → assertpy2-2.1.0}/tests/test_custom_dict.py +0 -0
- {assertpy2-2.0.1 → assertpy2-2.1.0}/tests/test_custom_list.py +0 -0
- {assertpy2-2.0.1 → assertpy2-2.1.0}/tests/test_datetime.py +0 -0
- {assertpy2-2.0.1 → assertpy2-2.1.0}/tests/test_description.py +0 -0
- {assertpy2-2.0.1 → assertpy2-2.1.0}/tests/test_dict.py +0 -0
- {assertpy2-2.0.1 → assertpy2-2.1.0}/tests/test_dict_compare.py +0 -0
- {assertpy2-2.0.1 → assertpy2-2.1.0}/tests/test_dyn.py +0 -0
- {assertpy2-2.0.1 → assertpy2-2.1.0}/tests/test_equals.py +0 -0
- {assertpy2-2.0.1 → assertpy2-2.1.0}/tests/test_expected_exception.py +0 -0
- {assertpy2-2.0.1 → assertpy2-2.1.0}/tests/test_extensions.py +0 -0
- {assertpy2-2.0.1 → assertpy2-2.1.0}/tests/test_extracting.py +0 -0
- {assertpy2-2.0.1 → assertpy2-2.1.0}/tests/test_fail.py +0 -0
- {assertpy2-2.0.1 → assertpy2-2.1.0}/tests/test_file.py +0 -0
- {assertpy2-2.0.1 → assertpy2-2.1.0}/tests/test_in.py +0 -0
- {assertpy2-2.0.1 → assertpy2-2.1.0}/tests/test_list.py +0 -0
- {assertpy2-2.0.1 → assertpy2-2.1.0}/tests/test_namedtuple.py +0 -0
- {assertpy2-2.0.1 → assertpy2-2.1.0}/tests/test_none.py +0 -0
- {assertpy2-2.0.1 → assertpy2-2.1.0}/tests/test_numbers.py +0 -0
- {assertpy2-2.0.1 → assertpy2-2.1.0}/tests/test_readme.py +0 -0
- {assertpy2-2.0.1 → assertpy2-2.1.0}/tests/test_same_as.py +0 -0
- {assertpy2-2.0.1 → assertpy2-2.1.0}/tests/test_snapshots.py +0 -0
- {assertpy2-2.0.1 → assertpy2-2.1.0}/tests/test_soft.py +0 -0
- {assertpy2-2.0.1 → assertpy2-2.1.0}/tests/test_soft_fail.py +0 -0
- {assertpy2-2.0.1 → assertpy2-2.1.0}/tests/test_string.py +0 -0
- {assertpy2-2.0.1 → assertpy2-2.1.0}/tests/test_type.py +0 -0
- {assertpy2-2.0.1 → assertpy2-2.1.0}/tests/test_warn.py +0 -0
|
@@ -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.0
|
|
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
|
|
@@ -38,7 +38,7 @@ Description-Content-Type: text/markdown
|
|
|
38
38
|
</p>
|
|
39
39
|
|
|
40
40
|
<p align="center">
|
|
41
|
-
<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>
|
|
42
42
|
Maintained fork of <a href="https://github.com/assertpy/assertpy">assertpy</a>.
|
|
43
43
|
</p>
|
|
44
44
|
|
|
@@ -71,14 +71,41 @@ def test_user():
|
|
|
71
71
|
assert_that(user).has_name("Alice")
|
|
72
72
|
```
|
|
73
73
|
|
|
74
|
-
|
|
74
|
+
Composable matchers and structural matching:
|
|
75
75
|
|
|
76
76
|
```py
|
|
77
|
-
assert_that
|
|
78
|
-
|
|
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:
|
|
79
103
|
|
|
80
|
-
|
|
81
|
-
|
|
104
|
+
```
|
|
105
|
+
FAILED test_example.py::test_comparison
|
|
106
|
+
--- AssertionFailure ---
|
|
107
|
+
actual: {'a': 1, 'b': 2}
|
|
108
|
+
expected: {'a': 1, 'b': 99}
|
|
82
109
|
```
|
|
83
110
|
|
|
84
111
|
|
|
@@ -105,9 +132,13 @@ assert_that(items).is_type_of(list).is_length(3).contains("admin")
|
|
|
105
132
|
|
|
106
133
|
## Features
|
|
107
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.
|
|
108
140
|
- **Type safety**: `Self` return types, `py.typed` ([PEP 561](https://peps.python.org/pep-0561/)).
|
|
109
|
-
- **
|
|
110
|
-
- **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()`.
|
|
111
142
|
- **Fluent chaining**: write assertions as readable one-liners that chain naturally.
|
|
112
143
|
- **Dynamic assertions**: `has_<name>()` for any attribute, property, or zero-argument method on objects and dicts.
|
|
113
144
|
- **Dict comparison**: `is_equal_to()` with `ignore` and `include` for selective key matching.
|
|
@@ -118,6 +149,69 @@ assert_that(items).is_type_of(list).is_length(3).contains("admin")
|
|
|
118
149
|
- Strings, numbers, lists, tuples, sets, dicts, dates, booleans, objects, exceptions.
|
|
119
150
|
|
|
120
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
|
+
|
|
121
215
|
## Soft assertions
|
|
122
216
|
|
|
123
217
|
Collect all failures instead of stopping at the first one:
|
|
@@ -143,8 +237,45 @@ AssertionError: soft assertion failures:
|
|
|
143
237
|
|
|
144
238
|
Use `soft_fail("message")` inside the block for non-halting explicit failures (unlike `fail()`, which stops immediately).
|
|
145
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.
|
|
146
265
|
|
|
147
|
-
|
|
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
|
|
148
279
|
|
|
149
280
|
### Dict comparison with ignore/include
|
|
150
281
|
|
|
@@ -173,12 +304,37 @@ assert_that(some_func).raises(RuntimeError).when_called_with("bad_arg")\
|
|
|
173
304
|
.is_length(8).starts_with("some").is_equal_to("some err")
|
|
174
305
|
```
|
|
175
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
|
+
|
|
176
317
|
### Snapshot testing
|
|
177
318
|
|
|
178
319
|
```py
|
|
179
320
|
assert_that({"a": 1, "b": 2, "c": 3}).snapshot()
|
|
180
321
|
```
|
|
181
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
|
+
|
|
182
338
|
See the [full API reference](docs/api.md) for all assertion methods, examples, and advanced features.
|
|
183
339
|
|
|
184
340
|
|
|
@@ -200,9 +356,14 @@ from assertpy2 import assert_that, soft_assertions
|
|
|
200
356
|
|---|---|---|
|
|
201
357
|
| Maintained | Last release 2020 | Active |
|
|
202
358
|
| Python | 2.7+ | 3.10-3.15 |
|
|
203
|
-
| Type safety | No annotations | `Self` return types, `py.typed` (
|
|
204
|
-
| IDE support | No type info | Full autocomplete
|
|
205
|
-
|
|
|
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 |
|
|
206
367
|
| Security | [CVE in snapshots](https://github.com/assertpy/assertpy/issues/156) | Fixed |
|
|
207
368
|
| Open bugs | 15+ unresolved | All resolved |
|
|
208
369
|
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
</p>
|
|
8
8
|
|
|
9
9
|
<p align="center">
|
|
10
|
-
<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>
|
|
11
11
|
Maintained fork of <a href="https://github.com/assertpy/assertpy">assertpy</a>.
|
|
12
12
|
</p>
|
|
13
13
|
|
|
@@ -40,14 +40,41 @@ def test_user():
|
|
|
40
40
|
assert_that(user).has_name("Alice")
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
Composable matchers and structural matching:
|
|
44
44
|
|
|
45
45
|
```py
|
|
46
|
-
assert_that
|
|
47
|
-
|
|
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:
|
|
48
72
|
|
|
49
|
-
|
|
50
|
-
|
|
73
|
+
```
|
|
74
|
+
FAILED test_example.py::test_comparison
|
|
75
|
+
--- AssertionFailure ---
|
|
76
|
+
actual: {'a': 1, 'b': 2}
|
|
77
|
+
expected: {'a': 1, 'b': 99}
|
|
51
78
|
```
|
|
52
79
|
|
|
53
80
|
|
|
@@ -74,9 +101,13 @@ assert_that(items).is_type_of(list).is_length(3).contains("admin")
|
|
|
74
101
|
|
|
75
102
|
## Features
|
|
76
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.
|
|
77
109
|
- **Type safety**: `Self` return types, `py.typed` ([PEP 561](https://peps.python.org/pep-0561/)).
|
|
78
|
-
- **
|
|
79
|
-
- **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()`.
|
|
80
111
|
- **Fluent chaining**: write assertions as readable one-liners that chain naturally.
|
|
81
112
|
- **Dynamic assertions**: `has_<name>()` for any attribute, property, or zero-argument method on objects and dicts.
|
|
82
113
|
- **Dict comparison**: `is_equal_to()` with `ignore` and `include` for selective key matching.
|
|
@@ -87,6 +118,69 @@ assert_that(items).is_type_of(list).is_length(3).contains("admin")
|
|
|
87
118
|
- Strings, numbers, lists, tuples, sets, dicts, dates, booleans, objects, exceptions.
|
|
88
119
|
|
|
89
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
|
+
|
|
90
184
|
## Soft assertions
|
|
91
185
|
|
|
92
186
|
Collect all failures instead of stopping at the first one:
|
|
@@ -112,8 +206,45 @@ AssertionError: soft assertion failures:
|
|
|
112
206
|
|
|
113
207
|
Use `soft_fail("message")` inside the block for non-halting explicit failures (unlike `fail()`, which stops immediately).
|
|
114
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.
|
|
115
234
|
|
|
116
|
-
|
|
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
|
|
117
248
|
|
|
118
249
|
### Dict comparison with ignore/include
|
|
119
250
|
|
|
@@ -142,12 +273,37 @@ assert_that(some_func).raises(RuntimeError).when_called_with("bad_arg")\
|
|
|
142
273
|
.is_length(8).starts_with("some").is_equal_to("some err")
|
|
143
274
|
```
|
|
144
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
|
+
|
|
145
286
|
### Snapshot testing
|
|
146
287
|
|
|
147
288
|
```py
|
|
148
289
|
assert_that({"a": 1, "b": 2, "c": 3}).snapshot()
|
|
149
290
|
```
|
|
150
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
|
+
|
|
151
307
|
See the [full API reference](docs/api.md) for all assertion methods, examples, and advanced features.
|
|
152
308
|
|
|
153
309
|
|
|
@@ -169,9 +325,14 @@ from assertpy2 import assert_that, soft_assertions
|
|
|
169
325
|
|---|---|---|
|
|
170
326
|
| Maintained | Last release 2020 | Active |
|
|
171
327
|
| Python | 2.7+ | 3.10-3.15 |
|
|
172
|
-
| Type safety | No annotations | `Self` return types, `py.typed` (
|
|
173
|
-
| IDE support | No type info | Full autocomplete
|
|
174
|
-
|
|
|
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 |
|
|
175
336
|
| Security | [CVE in snapshots](https://github.com/assertpy/assertpy/issues/156) | Fixed |
|
|
176
337
|
| Open bugs | 15+ unresolved | All resolved |
|
|
177
338
|
|
|
@@ -9,9 +9,17 @@ from .assertpy import (
|
|
|
9
9
|
soft_assertions,
|
|
10
10
|
soft_fail,
|
|
11
11
|
)
|
|
12
|
+
from .async_assertions import AsyncAssertionBuilder
|
|
13
|
+
from .errors import AssertionFailure, DiffEntry, DiffResult
|
|
12
14
|
from .file import contents_of
|
|
15
|
+
from .matchers import Matcher, match
|
|
13
16
|
|
|
14
17
|
__all__ = [
|
|
18
|
+
"AssertionFailure",
|
|
19
|
+
"AsyncAssertionBuilder",
|
|
20
|
+
"DiffEntry",
|
|
21
|
+
"DiffResult",
|
|
22
|
+
"Matcher",
|
|
15
23
|
"WarningLoggingAdapter",
|
|
16
24
|
"__version__",
|
|
17
25
|
"add_extension",
|
|
@@ -19,6 +27,7 @@ __all__ = [
|
|
|
19
27
|
"assert_warn",
|
|
20
28
|
"contents_of",
|
|
21
29
|
"fail",
|
|
30
|
+
"match",
|
|
22
31
|
"remove_extension",
|
|
23
32
|
"soft_assertions",
|
|
24
33
|
"soft_fail",
|