assertpy2 2.4.0__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.
Files changed (100) hide show
  1. assertpy2-2.5.0/.codecov.yml +10 -0
  2. {assertpy2-2.4.0 → assertpy2-2.5.0}/.github/workflows/ci.yml +12 -5
  3. {assertpy2-2.4.0 → assertpy2-2.5.0}/.github/workflows/zizmor.yml +4 -0
  4. assertpy2-2.5.0/PKG-INFO +221 -0
  5. assertpy2-2.5.0/README.md +183 -0
  6. {assertpy2-2.4.0 → assertpy2-2.5.0}/assertpy2/_mixin_base.py +3 -0
  7. {assertpy2-2.4.0 → assertpy2-2.5.0}/assertpy2/base.py +192 -30
  8. {assertpy2-2.4.0 → assertpy2-2.5.0}/assertpy2/helpers.py +83 -16
  9. {assertpy2-2.4.0 → assertpy2-2.5.0}/assertpy2/numeric.py +4 -4
  10. {assertpy2-2.4.0 → assertpy2-2.5.0}/assertpy2/pytest_plugin.py +5 -11
  11. {assertpy2-2.4.0 → assertpy2-2.5.0}/docs/api.md +54 -9
  12. {assertpy2-2.4.0 → assertpy2-2.5.0}/pyproject.toml +2 -2
  13. {assertpy2-2.4.0 → assertpy2-2.5.0}/tests/test_class.py +3 -7
  14. {assertpy2-2.4.0 → assertpy2-2.5.0}/tests/test_collection.py +4 -4
  15. assertpy2-2.5.0/tests/test_core.py +82 -0
  16. assertpy2-2.5.0/tests/test_custom_dict.py +209 -0
  17. {assertpy2-2.4.0 → assertpy2-2.5.0}/tests/test_custom_list.py +9 -9
  18. {assertpy2-2.4.0 → assertpy2-2.5.0}/tests/test_dict.py +2 -2
  19. {assertpy2-2.4.0 → assertpy2-2.5.0}/tests/test_dict_compare.py +7 -7
  20. {assertpy2-2.4.0 → assertpy2-2.5.0}/tests/test_equals.py +2 -2
  21. {assertpy2-2.4.0 → assertpy2-2.5.0}/tests/test_extensions.py +5 -5
  22. {assertpy2-2.4.0 → assertpy2-2.5.0}/tests/test_extracting.py +16 -16
  23. {assertpy2-2.4.0 → assertpy2-2.5.0}/tests/test_list.py +32 -32
  24. {assertpy2-2.4.0 → assertpy2-2.5.0}/tests/test_matchers.py +63 -63
  25. {assertpy2-2.4.0 → assertpy2-2.5.0}/tests/test_numbers.py +0 -2
  26. {assertpy2-2.4.0 → assertpy2-2.5.0}/tests/test_readme.py +26 -27
  27. assertpy2-2.5.0/tests/test_recursive_compare.py +232 -0
  28. assertpy2-2.5.0/tests/test_recursive_compare_attrs.py +53 -0
  29. assertpy2-2.5.0/tests/test_recursive_compare_pydantic.py +55 -0
  30. assertpy2-2.5.0/tests/test_rich_diff.py +979 -0
  31. {assertpy2-2.4.0 → assertpy2-2.5.0}/tests/test_snapshots.py +5 -5
  32. {assertpy2-2.4.0 → assertpy2-2.5.0}/tests/test_string.py +2 -5
  33. {assertpy2-2.4.0 → assertpy2-2.5.0}/tests/test_structural.py +80 -80
  34. {assertpy2-2.4.0 → assertpy2-2.5.0}/tests/test_type.py +2 -2
  35. {assertpy2-2.4.0 → assertpy2-2.5.0}/uv.lock +21 -21
  36. assertpy2-2.4.0/.codecov.yml +0 -10
  37. assertpy2-2.4.0/PKG-INFO +0 -646
  38. assertpy2-2.4.0/README.md +0 -608
  39. assertpy2-2.4.0/tests/test_core.py +0 -82
  40. assertpy2-2.4.0/tests/test_custom_dict.py +0 -209
  41. assertpy2-2.4.0/tests/test_rich_diff.py +0 -427
  42. {assertpy2-2.4.0 → assertpy2-2.5.0}/.github/dependabot.yml +0 -0
  43. {assertpy2-2.4.0 → assertpy2-2.5.0}/.github/workflows/codeql.yml +0 -0
  44. {assertpy2-2.4.0 → assertpy2-2.5.0}/.github/workflows/publish.yml +0 -0
  45. {assertpy2-2.4.0 → assertpy2-2.5.0}/.github/workflows/scorecard.yml +0 -0
  46. {assertpy2-2.4.0 → assertpy2-2.5.0}/.gitignore +0 -0
  47. {assertpy2-2.4.0 → assertpy2-2.5.0}/CONTRIBUTING.md +0 -0
  48. {assertpy2-2.4.0 → assertpy2-2.5.0}/LICENSE +0 -0
  49. {assertpy2-2.4.0 → assertpy2-2.5.0}/SECURITY.md +0 -0
  50. {assertpy2-2.4.0 → assertpy2-2.5.0}/assertpy2/__init__.py +0 -0
  51. {assertpy2-2.4.0 → assertpy2-2.5.0}/assertpy2/_typing.py +0 -0
  52. {assertpy2-2.4.0 → assertpy2-2.5.0}/assertpy2/assertpy.py +0 -0
  53. {assertpy2-2.4.0 → assertpy2-2.5.0}/assertpy2/async_assertions.py +0 -0
  54. {assertpy2-2.4.0 → assertpy2-2.5.0}/assertpy2/behave_matchers.py +0 -0
  55. {assertpy2-2.4.0 → assertpy2-2.5.0}/assertpy2/bytes_mixin.py +0 -0
  56. {assertpy2-2.4.0 → assertpy2-2.5.0}/assertpy2/collection.py +0 -0
  57. {assertpy2-2.4.0 → assertpy2-2.5.0}/assertpy2/contains.py +0 -0
  58. {assertpy2-2.4.0 → assertpy2-2.5.0}/assertpy2/date.py +0 -0
  59. {assertpy2-2.4.0 → assertpy2-2.5.0}/assertpy2/dict.py +0 -0
  60. {assertpy2-2.4.0 → assertpy2-2.5.0}/assertpy2/dynamic.py +0 -0
  61. {assertpy2-2.4.0 → assertpy2-2.5.0}/assertpy2/errors.py +0 -0
  62. {assertpy2-2.4.0 → assertpy2-2.5.0}/assertpy2/exception.py +0 -0
  63. {assertpy2-2.4.0 → assertpy2-2.5.0}/assertpy2/extracting.py +0 -0
  64. {assertpy2-2.4.0 → assertpy2-2.5.0}/assertpy2/file.py +0 -0
  65. {assertpy2-2.4.0 → assertpy2-2.5.0}/assertpy2/json_mixin.py +0 -0
  66. {assertpy2-2.4.0 → assertpy2-2.5.0}/assertpy2/matchers.py +0 -0
  67. {assertpy2-2.4.0 → assertpy2-2.5.0}/assertpy2/py.typed +0 -0
  68. {assertpy2-2.4.0 → assertpy2-2.5.0}/assertpy2/snapshot.py +0 -0
  69. {assertpy2-2.4.0 → assertpy2-2.5.0}/assertpy2/string.py +0 -0
  70. {assertpy2-2.4.0 → assertpy2-2.5.0}/docs/logo-dark.svg +0 -0
  71. {assertpy2-2.4.0 → assertpy2-2.5.0}/docs/logo.svg +0 -0
  72. {assertpy2-2.4.0 → assertpy2-2.5.0}/tests/test_async.py +0 -0
  73. {assertpy2-2.4.0 → assertpy2-2.5.0}/tests/test_behave_matchers.py +0 -0
  74. {assertpy2-2.4.0 → assertpy2-2.5.0}/tests/test_bool.py +0 -0
  75. {assertpy2-2.4.0 → assertpy2-2.5.0}/tests/test_bytes.py +0 -0
  76. {assertpy2-2.4.0 → assertpy2-2.5.0}/tests/test_datetime.py +0 -0
  77. {assertpy2-2.4.0 → assertpy2-2.5.0}/tests/test_description.py +0 -0
  78. {assertpy2-2.4.0 → assertpy2-2.5.0}/tests/test_dyn.py +0 -0
  79. {assertpy2-2.4.0 → assertpy2-2.5.0}/tests/test_errors.py +0 -0
  80. {assertpy2-2.4.0 → assertpy2-2.5.0}/tests/test_expected_exception.py +0 -0
  81. {assertpy2-2.4.0 → assertpy2-2.5.0}/tests/test_fail.py +0 -0
  82. {assertpy2-2.4.0 → assertpy2-2.5.0}/tests/test_file.py +0 -0
  83. {assertpy2-2.4.0 → assertpy2-2.5.0}/tests/test_grouped_soft.py +0 -0
  84. {assertpy2-2.4.0 → assertpy2-2.5.0}/tests/test_in.py +0 -0
  85. {assertpy2-2.4.0 → assertpy2-2.5.0}/tests/test_json.py +0 -0
  86. {assertpy2-2.4.0 → assertpy2-2.5.0}/tests/test_matcher_registry.py +0 -0
  87. {assertpy2-2.4.0 → assertpy2-2.5.0}/tests/test_matchers_phase3.py +0 -0
  88. {assertpy2-2.4.0 → assertpy2-2.5.0}/tests/test_namedtuple.py +0 -0
  89. {assertpy2-2.4.0 → assertpy2-2.5.0}/tests/test_none.py +0 -0
  90. {assertpy2-2.4.0 → assertpy2-2.5.0}/tests/test_not.py +0 -0
  91. {assertpy2-2.4.0 → assertpy2-2.5.0}/tests/test_overloads.py +0 -0
  92. {assertpy2-2.4.0 → assertpy2-2.5.0}/tests/test_phase2.py +0 -0
  93. {assertpy2-2.4.0 → assertpy2-2.5.0}/tests/test_pipeline.py +0 -0
  94. {assertpy2-2.4.0 → assertpy2-2.5.0}/tests/test_pytest_plugin.py +0 -0
  95. {assertpy2-2.4.0 → assertpy2-2.5.0}/tests/test_regex_groups.py +0 -0
  96. {assertpy2-2.4.0 → assertpy2-2.5.0}/tests/test_same_as.py +0 -0
  97. {assertpy2-2.4.0 → assertpy2-2.5.0}/tests/test_soft.py +0 -0
  98. {assertpy2-2.4.0 → assertpy2-2.5.0}/tests/test_soft_fail.py +0 -0
  99. {assertpy2-2.4.0 → assertpy2-2.5.0}/tests/test_traceback.py +0 -0
  100. {assertpy2-2.4.0 → assertpy2-2.5.0}/tests/test_warn.py +0 -0
@@ -0,0 +1,10 @@
1
+ coverage:
2
+ status:
3
+ project:
4
+ default:
5
+ target: 100%
6
+ threshold: 0%
7
+ patch:
8
+ default:
9
+ target: 100%
10
+ threshold: 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: ubuntu-latest
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 }}
@@ -6,6 +6,10 @@ 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
 
@@ -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").` &rarr; string methods: `starts_with`, `matches`, `is_alpha`, ...
113
+ - `assert_that(42).` &rarr; numeric methods: `is_positive`, `is_between`, `is_close_to`, ...
114
+ - `assert_that(Path("/tmp")).` &rarr; path methods: `exists`, `is_file`, `is_readable`, ...
115
+ - `assert_that(my_dict).` &rarr; dict methods: `contains_key`, `contains_entry`, `has_json_path`, ...
116
+ - `assert_that(b"\x89PNG").` &rarr; 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").` &rarr; string methods: `starts_with`, `matches`, `is_alpha`, ...
75
+ - `assert_that(42).` &rarr; numeric methods: `is_positive`, `is_between`, `is_close_to`, ...
76
+ - `assert_that(Path("/tmp")).` &rarr; path methods: `exists`, `is_file`, `is_readable`, ...
77
+ - `assert_that(my_dict).` &rarr; dict methods: `contains_key`, `contains_entry`, `has_json_path`, ...
78
+ - `assert_that(b"\x89PNG").` &rarr; 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]