assertpy2 2.10.0__tar.gz → 2.11.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 (127) hide show
  1. {assertpy2-2.10.0 → assertpy2-2.11.0}/PKG-INFO +29 -61
  2. {assertpy2-2.10.0 → assertpy2-2.11.0}/README.md +18 -60
  3. {assertpy2-2.10.0 → assertpy2-2.11.0}/assertpy2/_mixin_base.py +6 -1
  4. {assertpy2-2.10.0 → assertpy2-2.11.0}/assertpy2/assertpy.py +3 -1
  5. {assertpy2-2.10.0 → assertpy2-2.11.0}/assertpy2/base.py +62 -12
  6. assertpy2-2.11.0/assertpy2/dataframe.py +153 -0
  7. {assertpy2-2.10.0 → assertpy2-2.11.0}/assertpy2/helpers.py +3 -54
  8. {assertpy2-2.10.0 → assertpy2-2.11.0}/docs/comparison.md +1 -0
  9. {assertpy2-2.10.0 → assertpy2-2.11.0}/docs/errors.md +2 -2
  10. {assertpy2-2.10.0 → assertpy2-2.11.0}/docs/integrations.md +62 -0
  11. {assertpy2-2.10.0 → assertpy2-2.11.0}/pyproject.toml +6 -2
  12. assertpy2-2.11.0/tests/test_dataframe.py +163 -0
  13. assertpy2-2.11.0/tests/test_equals.py +138 -0
  14. {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_rich_diff.py +121 -0
  15. {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_snapshots.py +1 -1
  16. {assertpy2-2.10.0 → assertpy2-2.11.0}/uv.lock +468 -28
  17. assertpy2-2.10.0/tests/test_equals.py +0 -66
  18. {assertpy2-2.10.0 → assertpy2-2.11.0}/.codecov.yml +0 -0
  19. {assertpy2-2.10.0 → assertpy2-2.11.0}/.gitattributes +0 -0
  20. {assertpy2-2.10.0 → assertpy2-2.11.0}/.github/dependabot.yml +0 -0
  21. {assertpy2-2.10.0 → assertpy2-2.11.0}/.github/workflows/ci.yml +0 -0
  22. {assertpy2-2.10.0 → assertpy2-2.11.0}/.github/workflows/codeql.yml +0 -0
  23. {assertpy2-2.10.0 → assertpy2-2.11.0}/.github/workflows/docs.yml +0 -0
  24. {assertpy2-2.10.0 → assertpy2-2.11.0}/.github/workflows/mutation.yml +0 -0
  25. {assertpy2-2.10.0 → assertpy2-2.11.0}/.github/workflows/publish.yml +0 -0
  26. {assertpy2-2.10.0 → assertpy2-2.11.0}/.github/workflows/scorecard.yml +0 -0
  27. {assertpy2-2.10.0 → assertpy2-2.11.0}/.github/workflows/zizmor.yml +0 -0
  28. {assertpy2-2.10.0 → assertpy2-2.11.0}/.gitignore +0 -0
  29. {assertpy2-2.10.0 → assertpy2-2.11.0}/CONTRIBUTING.md +0 -0
  30. {assertpy2-2.10.0 → assertpy2-2.11.0}/LICENSE +0 -0
  31. {assertpy2-2.10.0 → assertpy2-2.11.0}/SECURITY.md +0 -0
  32. {assertpy2-2.10.0 → assertpy2-2.11.0}/assertpy2/__init__.py +0 -0
  33. {assertpy2-2.10.0 → assertpy2-2.11.0}/assertpy2/_compat.py +0 -0
  34. {assertpy2-2.10.0 → assertpy2-2.11.0}/assertpy2/_introspection.py +0 -0
  35. {assertpy2-2.10.0 → assertpy2-2.11.0}/assertpy2/_typing.py +0 -0
  36. {assertpy2-2.10.0 → assertpy2-2.11.0}/assertpy2/async_assertions.py +0 -0
  37. {assertpy2-2.10.0 → assertpy2-2.11.0}/assertpy2/behave_matchers.py +0 -0
  38. {assertpy2-2.10.0 → assertpy2-2.11.0}/assertpy2/bytes_mixin.py +0 -0
  39. {assertpy2-2.10.0 → assertpy2-2.11.0}/assertpy2/collection.py +0 -0
  40. {assertpy2-2.10.0 → assertpy2-2.11.0}/assertpy2/contains.py +0 -0
  41. {assertpy2-2.10.0 → assertpy2-2.11.0}/assertpy2/date.py +0 -0
  42. {assertpy2-2.10.0 → assertpy2-2.11.0}/assertpy2/dict.py +0 -0
  43. {assertpy2-2.10.0 → assertpy2-2.11.0}/assertpy2/dynamic.py +0 -0
  44. {assertpy2-2.10.0 → assertpy2-2.11.0}/assertpy2/errors.py +0 -0
  45. {assertpy2-2.10.0 → assertpy2-2.11.0}/assertpy2/exception.py +0 -0
  46. {assertpy2-2.10.0 → assertpy2-2.11.0}/assertpy2/extracting.py +0 -0
  47. {assertpy2-2.10.0 → assertpy2-2.11.0}/assertpy2/file.py +0 -0
  48. {assertpy2-2.10.0 → assertpy2-2.11.0}/assertpy2/json_mixin.py +0 -0
  49. {assertpy2-2.10.0 → assertpy2-2.11.0}/assertpy2/matchers.py +0 -0
  50. {assertpy2-2.10.0 → assertpy2-2.11.0}/assertpy2/numeric.py +0 -0
  51. {assertpy2-2.10.0 → assertpy2-2.11.0}/assertpy2/py.typed +0 -0
  52. {assertpy2-2.10.0 → assertpy2-2.11.0}/assertpy2/pytest_plugin.py +0 -0
  53. {assertpy2-2.10.0 → assertpy2-2.11.0}/assertpy2/snapshot.py +0 -0
  54. {assertpy2-2.10.0 → assertpy2-2.11.0}/assertpy2/string.py +0 -0
  55. {assertpy2-2.10.0 → assertpy2-2.11.0}/assertpy2/warning.py +0 -0
  56. {assertpy2-2.10.0 → assertpy2-2.11.0}/docs/assertions.md +0 -0
  57. {assertpy2-2.10.0 → assertpy2-2.11.0}/docs/assets/diff-equal.png +0 -0
  58. {assertpy2-2.10.0 → assertpy2-2.11.0}/docs/assets/diff-equal.svg +0 -0
  59. {assertpy2-2.10.0 → assertpy2-2.11.0}/docs/assets/diff-gallery.png +0 -0
  60. {assertpy2-2.10.0 → assertpy2-2.11.0}/docs/assets/diff-match.svg +0 -0
  61. {assertpy2-2.10.0 → assertpy2-2.11.0}/docs/assets/diff-sequence.svg +0 -0
  62. {assertpy2-2.10.0 → assertpy2-2.11.0}/docs/assets/diff-set.svg +0 -0
  63. {assertpy2-2.10.0 → assertpy2-2.11.0}/docs/data.md +0 -0
  64. {assertpy2-2.10.0 → assertpy2-2.11.0}/docs/extending.md +0 -0
  65. {assertpy2-2.10.0 → assertpy2-2.11.0}/docs/fluent.md +0 -0
  66. {assertpy2-2.10.0 → assertpy2-2.11.0}/docs/getting-started.md +0 -0
  67. {assertpy2-2.10.0 → assertpy2-2.11.0}/docs/index.md +0 -0
  68. {assertpy2-2.10.0 → assertpy2-2.11.0}/docs/logo-dark.svg +0 -0
  69. {assertpy2-2.10.0 → assertpy2-2.11.0}/docs/logo.svg +0 -0
  70. {assertpy2-2.10.0 → assertpy2-2.11.0}/docs/matchers.md +0 -0
  71. {assertpy2-2.10.0 → assertpy2-2.11.0}/docs/migration.md +0 -0
  72. {assertpy2-2.10.0 → assertpy2-2.11.0}/docs/testing.md +0 -0
  73. {assertpy2-2.10.0 → assertpy2-2.11.0}/docs/type-safety.md +0 -0
  74. {assertpy2-2.10.0 → assertpy2-2.11.0}/mkdocs.yml +0 -0
  75. {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_async.py +0 -0
  76. {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_behave_matchers.py +0 -0
  77. {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_bool.py +0 -0
  78. {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_bytes.py +0 -0
  79. {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_callable.py +0 -0
  80. {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_chaining.py +0 -0
  81. {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_class.py +0 -0
  82. {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_collection.py +0 -0
  83. {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_core.py +0 -0
  84. {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_custom_dict.py +0 -0
  85. {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_custom_list.py +0 -0
  86. {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_datetime.py +0 -0
  87. {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_description.py +0 -0
  88. {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_dict.py +0 -0
  89. {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_dict_compare.py +0 -0
  90. {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_dynamic.py +0 -0
  91. {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_errors.py +0 -0
  92. {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_expected_exception.py +0 -0
  93. {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_expected_warning.py +0 -0
  94. {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_extensions.py +0 -0
  95. {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_extracting.py +0 -0
  96. {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_fail.py +0 -0
  97. {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_file.py +0 -0
  98. {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_grouped_soft.py +0 -0
  99. {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_in.py +0 -0
  100. {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_json.py +0 -0
  101. {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_list.py +0 -0
  102. {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_matcher_registry.py +0 -0
  103. {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_matchers.py +0 -0
  104. {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_mutation_hardening.py +0 -0
  105. {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_namedtuple.py +0 -0
  106. {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_none.py +0 -0
  107. {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_not.py +0 -0
  108. {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_numbers.py +0 -0
  109. {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_overloads.py +0 -0
  110. {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_pipeline.py +0 -0
  111. {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_property_based.py +0 -0
  112. {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_pytest_plugin.py +0 -0
  113. {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_readme.py +0 -0
  114. {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_recursive_compare.py +0 -0
  115. {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_recursive_compare_attrs.py +0 -0
  116. {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_recursive_compare_pydantic.py +0 -0
  117. {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_regex_groups.py +0 -0
  118. {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_same_as.py +0 -0
  119. {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_satisfy.py +0 -0
  120. {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_soft.py +0 -0
  121. {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_soft_fail.py +0 -0
  122. {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_string.py +0 -0
  123. {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_structural.py +0 -0
  124. {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_traceback.py +0 -0
  125. {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_type.py +0 -0
  126. {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_typing.py +0 -0
  127. {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_warn.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: assertpy2
3
- Version: 2.10.0
3
+ Version: 2.11.0
4
4
  Summary: Fluent assertion library for Python with composable matchers, structural matching, and full type safety
5
5
  Project-URL: Homepage, https://github.com/Solganis/assertpy2
6
6
  Project-URL: Repository, https://github.com/Solganis/assertpy2
@@ -31,9 +31,19 @@ Provides-Extra: allure
31
31
  Requires-Dist: allure-pytest>=2.13; extra == 'allure'
32
32
  Provides-Extra: behave
33
33
  Requires-Dist: behave>=1.2.6; extra == 'behave'
34
+ Provides-Extra: data
35
+ Requires-Dist: numpy>=1.26; extra == 'data'
36
+ Requires-Dist: pandas>=2.0; extra == 'data'
37
+ Requires-Dist: polars>=1.0; extra == 'data'
34
38
  Provides-Extra: json
35
39
  Requires-Dist: jsonpath-ng>=1.8; extra == 'json'
36
40
  Requires-Dist: jsonschema>=4.0; extra == 'json'
41
+ Provides-Extra: numpy
42
+ Requires-Dist: numpy>=1.26; extra == 'numpy'
43
+ Provides-Extra: pandas
44
+ Requires-Dist: pandas>=2.0; extra == 'pandas'
45
+ Provides-Extra: polars
46
+ Requires-Dist: polars>=1.0; extra == 'polars'
37
47
  Description-Content-Type: text/markdown
38
48
 
39
49
  <p align="center">
@@ -68,7 +78,7 @@ Description-Content-Type: text/markdown
68
78
 
69
79
  ---
70
80
 
71
- ## [Quick start](https://solganis.github.io/assertpy2/getting-started/)
81
+ <h2 align="center"><a href="https://solganis.github.io/assertpy2/getting-started/">Quick start</a></h2>
72
82
 
73
83
  ```bash
74
84
  pip install assertpy2 # drop-in replacement for assertpy, just change the import
@@ -88,7 +98,7 @@ def test_user():
88
98
 
89
99
  Browse the [full documentation](https://solganis.github.io/assertpy2/) for every assertion, matcher, and integration.
90
100
 
91
- ## [Why fluent assertions?](https://solganis.github.io/assertpy2/comparison/)
101
+ <h2 align="center"><a href="https://solganis.github.io/assertpy2/comparison/">Why fluent assertions?</a></h2>
92
102
 
93
103
  A fluent chain reads as one intent and replaces several bare asserts - and your IDE
94
104
  offers only the [methods that fit the value's type](https://solganis.github.io/assertpy2/type-safety/):
@@ -121,7 +131,9 @@ assertpy2 reports the [exact path to every difference](https://solganis.github.i
121
131
  assert_that(response).is_equal_to(expected)
122
132
  ```
123
133
 
124
- <img src="https://raw.githubusercontent.com/Solganis/assertpy2/main/docs/assets/diff-equal.png" width="300" alt="Structured diff in the terminal: user.role shown with its path, removal in red and addition in green">
134
+ <p align="center">
135
+ <img src="https://raw.githubusercontent.com/Solganis/assertpy2/main/docs/assets/diff-equal.png" width="300" alt="Structured diff in the terminal: user.role shown with its path, removal in red and addition in green">
136
+ </p>
125
137
 
126
138
  Recursive diffs work for dicts, dataclasses, namedtuples, attrs, and Pydantic models.
127
139
  For responses with dynamic fields (IDs, timestamps), validate a subset with
@@ -129,9 +141,11 @@ For responses with dynamic fields (IDs, timestamps), validate a subset with
129
141
 
130
142
  The same path-level treatment for dicts, lists, sets, and matcher predicates:
131
143
 
132
- <img src="https://raw.githubusercontent.com/Solganis/assertpy2/main/docs/assets/diff-gallery.png" width="640" alt="Structured diffs in the terminal: dict path, list element, set extra/missing, and structural-matcher predicate diffs, side by side">
144
+ <p align="center">
145
+ <img src="https://raw.githubusercontent.com/Solganis/assertpy2/main/docs/assets/diff-gallery.png" width="640" alt="Structured diffs in the terminal: dict path, list element, set extra/missing, and structural-matcher predicate diffs, side by side">
146
+ </p>
133
147
 
134
- ## [Type-aware autocomplete](https://solganis.github.io/assertpy2/type-safety/)
148
+ <h2 align="center"><a href="https://solganis.github.io/assertpy2/type-safety/">Type-aware autocomplete</a></h2>
135
149
 
136
150
  `assert_that()` uses `@overload` to return type-specific Protocols.
137
151
  Your IDE shows only methods relevant to the value you're testing, not all 100+:
@@ -148,7 +162,7 @@ See the [**Type Safety**](https://solganis.github.io/assertpy2/type-safety/) gui
148
162
 
149
163
  ---
150
164
 
151
- ## Features
165
+ <h2 align="center">Features</h2>
152
166
 
153
167
  **Fluent API**
154
168
 
@@ -162,7 +176,6 @@ See the [**Type Safety**](https://solganis.github.io/assertpy2/type-safety/) gui
162
176
 
163
177
  - [Strings](https://solganis.github.io/assertpy2/assertions/#strings), [numbers](https://solganis.github.io/assertpy2/assertions/#numbers), [lists](https://solganis.github.io/assertpy2/assertions/#lists), [tuples](https://solganis.github.io/assertpy2/assertions/#tuples), [sets](https://solganis.github.io/assertpy2/assertions/#sets), [dicts](https://solganis.github.io/assertpy2/assertions/#dicts), [dates](https://solganis.github.io/assertpy2/assertions/#dates), [booleans](https://solganis.github.io/assertpy2/assertions/#booleans), [objects](https://solganis.github.io/assertpy2/assertions/#objects), [bytes](https://solganis.github.io/assertpy2/assertions/#bytes--bytearray), [files](https://solganis.github.io/assertpy2/assertions/#files), [exceptions](https://solganis.github.io/assertpy2/errors/#expected-exceptions).
164
178
  - [**Bytes assertions**](https://solganis.github.io/assertpy2/assertions/#bytes--bytearray): `is_valid_utf8()`, `starts_with_bytes()`, `is_hex_equal_to()`, `decoded_as()` for `bytes`/`bytearray`.
165
- - [**JSON assertions**](https://solganis.github.io/assertpy2/data/#json-path--schema): JSONPath navigation and JSON Schema validation. `pip install assertpy2[json]`.
166
179
  - [**Dynamic assertions**](https://solganis.github.io/assertpy2/assertions/#dynamic-assertions-on-objects): `has_<name>()` for any attribute, property, or zero-argument method.
167
180
  - [**Dict comparison**](https://solganis.github.io/assertpy2/assertions/#selective-comparison-ignore--include): `is_equal_to()` with `ignore` and `include` for selective key/field matching (dicts, dataclasses, namedtuples, Pydantic models, attrs, plain objects).
168
181
  - [**Extracting**](https://solganis.github.io/assertpy2/assertions/#extracting-attributes-from-objects): flatten collections on attributes with `filter` and `sort` support.
@@ -178,7 +191,7 @@ See the [**Type Safety**](https://solganis.github.io/assertpy2/type-safety/) gui
178
191
 
179
192
  **Type safety**
180
193
 
181
- - [**Type-aware autocomplete**](#type-aware-autocomplete): 9 Protocols, IDE shows only relevant methods per type.
194
+ - [**Type-aware autocomplete**](https://solganis.github.io/assertpy2/type-safety/): 9 Protocols, IDE shows only relevant methods per type.
182
195
  - **py.typed**: `Self` return types, PEP 561 compliant ([PEP 561](https://peps.python.org/pep-0561/)).
183
196
 
184
197
  **Extensibility**
@@ -187,64 +200,19 @@ See the [**Type Safety**](https://solganis.github.io/assertpy2/type-safety/) gui
187
200
  - [**Regex group extraction**](https://solganis.github.io/assertpy2/data/#regex-group-extraction): `extracting_group()` and `matches_with_groups()` for regex captures.
188
201
  - [**Extensions**](https://solganis.github.io/assertpy2/extending/): `add_extension()` for custom assertion methods.
189
202
 
190
- **Integrations**
191
-
192
- - [**Allure**](https://solganis.github.io/assertpy2/integrations/#allure): auto-attach structured diff and actual/expected data to reports. `pip install assertpy2[allure]`.
193
- - [**Behave**](https://solganis.github.io/assertpy2/integrations/#behave): ready-made parameter types for step definitions. `pip install assertpy2[behave]`.
194
-
195
203
  See the [full documentation](https://solganis.github.io/assertpy2/) for all assertion methods, examples, and advanced features.
196
204
 
197
205
  ---
198
206
 
199
- ## [Integrations](https://solganis.github.io/assertpy2/integrations/)
200
-
201
- ### Allure
202
-
203
- When `allure-pytest` is installed, the pytest plugin auto-attaches structured failure data to Allure reports as JSON attachments.
204
-
205
- ```bash
206
- pip install assertpy2[allure]
207
- ```
208
-
209
- Three modes controlled via `pytest.ini` (or `pyproject.toml`):
210
-
211
- | Mode | What is attached |
212
- |---|---|
213
- | `diff` (default) | Structured Diff JSON (path-level breakdown) |
214
- | `full` | Structured Diff + actual/expected JSON |
215
- | `off` | Nothing |
216
-
217
- ```toml
218
- # pyproject.toml
219
- [tool.pytest.ini_options]
220
- assertpy2_allure = "full"
221
- ```
222
-
223
- ### Behave
224
-
225
- Ready-made parameter types for Behave step definitions:
226
-
227
- ```bash
228
- pip install assertpy2[behave]
229
- ```
230
-
231
- ```py
232
- # in environment.py or steps/conftest.py
233
- from assertpy2.behave_matchers import register_assertpy_types
234
- register_assertpy_types()
235
- ```
236
-
237
- Then use in step definitions:
238
-
239
- ```py
240
- @given('a user aged {age:PositiveInt}')
241
- def step_impl(context, age):
242
- context.age = age # already validated as int > 0
243
- ```
207
+ <h2 align="center"><a href="https://solganis.github.io/assertpy2/integrations/">Integrations</a></h2>
244
208
 
245
- Available types: `PositiveInt`, `NonNegativeInt`, `PositiveFloat`, `NonEmptyString`, `BoolLike`.
209
+ Optional adapters, each its own extra; full configuration and examples are in the
210
+ [Integrations guide](https://solganis.github.io/assertpy2/integrations/).
246
211
 
247
- See the [Integrations guide](https://solganis.github.io/assertpy2/integrations/) for attachment modes, configuration, and full examples.
212
+ - [**Allure**](https://solganis.github.io/assertpy2/integrations/#allure) (`pip install assertpy2[allure]`): the pytest plugin auto-attaches structured diff and actual/expected data to Allure reports, in three configurable modes.
213
+ - [**Behave**](https://solganis.github.io/assertpy2/integrations/#behave) (`pip install assertpy2[behave]`): ready-made parameter types (`PositiveInt`, `NonEmptyString`, ...) for step definitions like `{age:PositiveInt}`.
214
+ - [**JSON**](https://solganis.github.io/assertpy2/data/#json-path--schema) (`pip install assertpy2[json]`): JSONPath navigation (`at_json_path()`, `has_json_path()`) and JSON Schema validation (`matches_json_schema()`).
215
+ - [**Data frames**](https://solganis.github.io/assertpy2/integrations/#data-frames-and-arrays) (`pip install assertpy2[pandas]` / `[polars]` / `[numpy]`): fluent equality for pandas/polars frames and numpy arrays, carrying each library's own diff.
248
216
 
249
217
  ---
250
218
 
@@ -30,7 +30,7 @@
30
30
 
31
31
  ---
32
32
 
33
- ## [Quick start](https://solganis.github.io/assertpy2/getting-started/)
33
+ <h2 align="center"><a href="https://solganis.github.io/assertpy2/getting-started/">Quick start</a></h2>
34
34
 
35
35
  ```bash
36
36
  pip install assertpy2 # drop-in replacement for assertpy, just change the import
@@ -50,7 +50,7 @@ def test_user():
50
50
 
51
51
  Browse the [full documentation](https://solganis.github.io/assertpy2/) for every assertion, matcher, and integration.
52
52
 
53
- ## [Why fluent assertions?](https://solganis.github.io/assertpy2/comparison/)
53
+ <h2 align="center"><a href="https://solganis.github.io/assertpy2/comparison/">Why fluent assertions?</a></h2>
54
54
 
55
55
  A fluent chain reads as one intent and replaces several bare asserts - and your IDE
56
56
  offers only the [methods that fit the value's type](https://solganis.github.io/assertpy2/type-safety/):
@@ -83,7 +83,9 @@ assertpy2 reports the [exact path to every difference](https://solganis.github.i
83
83
  assert_that(response).is_equal_to(expected)
84
84
  ```
85
85
 
86
- <img src="https://raw.githubusercontent.com/Solganis/assertpy2/main/docs/assets/diff-equal.png" width="300" alt="Structured diff in the terminal: user.role shown with its path, removal in red and addition in green">
86
+ <p align="center">
87
+ <img src="https://raw.githubusercontent.com/Solganis/assertpy2/main/docs/assets/diff-equal.png" width="300" alt="Structured diff in the terminal: user.role shown with its path, removal in red and addition in green">
88
+ </p>
87
89
 
88
90
  Recursive diffs work for dicts, dataclasses, namedtuples, attrs, and Pydantic models.
89
91
  For responses with dynamic fields (IDs, timestamps), validate a subset with
@@ -91,9 +93,11 @@ For responses with dynamic fields (IDs, timestamps), validate a subset with
91
93
 
92
94
  The same path-level treatment for dicts, lists, sets, and matcher predicates:
93
95
 
94
- <img src="https://raw.githubusercontent.com/Solganis/assertpy2/main/docs/assets/diff-gallery.png" width="640" alt="Structured diffs in the terminal: dict path, list element, set extra/missing, and structural-matcher predicate diffs, side by side">
96
+ <p align="center">
97
+ <img src="https://raw.githubusercontent.com/Solganis/assertpy2/main/docs/assets/diff-gallery.png" width="640" alt="Structured diffs in the terminal: dict path, list element, set extra/missing, and structural-matcher predicate diffs, side by side">
98
+ </p>
95
99
 
96
- ## [Type-aware autocomplete](https://solganis.github.io/assertpy2/type-safety/)
100
+ <h2 align="center"><a href="https://solganis.github.io/assertpy2/type-safety/">Type-aware autocomplete</a></h2>
97
101
 
98
102
  `assert_that()` uses `@overload` to return type-specific Protocols.
99
103
  Your IDE shows only methods relevant to the value you're testing, not all 100+:
@@ -110,7 +114,7 @@ See the [**Type Safety**](https://solganis.github.io/assertpy2/type-safety/) gui
110
114
 
111
115
  ---
112
116
 
113
- ## Features
117
+ <h2 align="center">Features</h2>
114
118
 
115
119
  **Fluent API**
116
120
 
@@ -124,7 +128,6 @@ See the [**Type Safety**](https://solganis.github.io/assertpy2/type-safety/) gui
124
128
 
125
129
  - [Strings](https://solganis.github.io/assertpy2/assertions/#strings), [numbers](https://solganis.github.io/assertpy2/assertions/#numbers), [lists](https://solganis.github.io/assertpy2/assertions/#lists), [tuples](https://solganis.github.io/assertpy2/assertions/#tuples), [sets](https://solganis.github.io/assertpy2/assertions/#sets), [dicts](https://solganis.github.io/assertpy2/assertions/#dicts), [dates](https://solganis.github.io/assertpy2/assertions/#dates), [booleans](https://solganis.github.io/assertpy2/assertions/#booleans), [objects](https://solganis.github.io/assertpy2/assertions/#objects), [bytes](https://solganis.github.io/assertpy2/assertions/#bytes--bytearray), [files](https://solganis.github.io/assertpy2/assertions/#files), [exceptions](https://solganis.github.io/assertpy2/errors/#expected-exceptions).
126
130
  - [**Bytes assertions**](https://solganis.github.io/assertpy2/assertions/#bytes--bytearray): `is_valid_utf8()`, `starts_with_bytes()`, `is_hex_equal_to()`, `decoded_as()` for `bytes`/`bytearray`.
127
- - [**JSON assertions**](https://solganis.github.io/assertpy2/data/#json-path--schema): JSONPath navigation and JSON Schema validation. `pip install assertpy2[json]`.
128
131
  - [**Dynamic assertions**](https://solganis.github.io/assertpy2/assertions/#dynamic-assertions-on-objects): `has_<name>()` for any attribute, property, or zero-argument method.
129
132
  - [**Dict comparison**](https://solganis.github.io/assertpy2/assertions/#selective-comparison-ignore--include): `is_equal_to()` with `ignore` and `include` for selective key/field matching (dicts, dataclasses, namedtuples, Pydantic models, attrs, plain objects).
130
133
  - [**Extracting**](https://solganis.github.io/assertpy2/assertions/#extracting-attributes-from-objects): flatten collections on attributes with `filter` and `sort` support.
@@ -140,7 +143,7 @@ See the [**Type Safety**](https://solganis.github.io/assertpy2/type-safety/) gui
140
143
 
141
144
  **Type safety**
142
145
 
143
- - [**Type-aware autocomplete**](#type-aware-autocomplete): 9 Protocols, IDE shows only relevant methods per type.
146
+ - [**Type-aware autocomplete**](https://solganis.github.io/assertpy2/type-safety/): 9 Protocols, IDE shows only relevant methods per type.
144
147
  - **py.typed**: `Self` return types, PEP 561 compliant ([PEP 561](https://peps.python.org/pep-0561/)).
145
148
 
146
149
  **Extensibility**
@@ -149,64 +152,19 @@ See the [**Type Safety**](https://solganis.github.io/assertpy2/type-safety/) gui
149
152
  - [**Regex group extraction**](https://solganis.github.io/assertpy2/data/#regex-group-extraction): `extracting_group()` and `matches_with_groups()` for regex captures.
150
153
  - [**Extensions**](https://solganis.github.io/assertpy2/extending/): `add_extension()` for custom assertion methods.
151
154
 
152
- **Integrations**
153
-
154
- - [**Allure**](https://solganis.github.io/assertpy2/integrations/#allure): auto-attach structured diff and actual/expected data to reports. `pip install assertpy2[allure]`.
155
- - [**Behave**](https://solganis.github.io/assertpy2/integrations/#behave): ready-made parameter types for step definitions. `pip install assertpy2[behave]`.
156
-
157
155
  See the [full documentation](https://solganis.github.io/assertpy2/) for all assertion methods, examples, and advanced features.
158
156
 
159
157
  ---
160
158
 
161
- ## [Integrations](https://solganis.github.io/assertpy2/integrations/)
162
-
163
- ### Allure
164
-
165
- When `allure-pytest` is installed, the pytest plugin auto-attaches structured failure data to Allure reports as JSON attachments.
166
-
167
- ```bash
168
- pip install assertpy2[allure]
169
- ```
170
-
171
- Three modes controlled via `pytest.ini` (or `pyproject.toml`):
172
-
173
- | Mode | What is attached |
174
- |---|---|
175
- | `diff` (default) | Structured Diff JSON (path-level breakdown) |
176
- | `full` | Structured Diff + actual/expected JSON |
177
- | `off` | Nothing |
178
-
179
- ```toml
180
- # pyproject.toml
181
- [tool.pytest.ini_options]
182
- assertpy2_allure = "full"
183
- ```
184
-
185
- ### Behave
186
-
187
- Ready-made parameter types for Behave step definitions:
188
-
189
- ```bash
190
- pip install assertpy2[behave]
191
- ```
192
-
193
- ```py
194
- # in environment.py or steps/conftest.py
195
- from assertpy2.behave_matchers import register_assertpy_types
196
- register_assertpy_types()
197
- ```
198
-
199
- Then use in step definitions:
200
-
201
- ```py
202
- @given('a user aged {age:PositiveInt}')
203
- def step_impl(context, age):
204
- context.age = age # already validated as int > 0
205
- ```
159
+ <h2 align="center"><a href="https://solganis.github.io/assertpy2/integrations/">Integrations</a></h2>
206
160
 
207
- Available types: `PositiveInt`, `NonNegativeInt`, `PositiveFloat`, `NonEmptyString`, `BoolLike`.
161
+ Optional adapters, each its own extra; full configuration and examples are in the
162
+ [Integrations guide](https://solganis.github.io/assertpy2/integrations/).
208
163
 
209
- See the [Integrations guide](https://solganis.github.io/assertpy2/integrations/) for attachment modes, configuration, and full examples.
164
+ - [**Allure**](https://solganis.github.io/assertpy2/integrations/#allure) (`pip install assertpy2[allure]`): the pytest plugin auto-attaches structured diff and actual/expected data to Allure reports, in three configurable modes.
165
+ - [**Behave**](https://solganis.github.io/assertpy2/integrations/#behave) (`pip install assertpy2[behave]`): ready-made parameter types (`PositiveInt`, `NonEmptyString`, ...) for step definitions like `{age:PositiveInt}`.
166
+ - [**JSON**](https://solganis.github.io/assertpy2/data/#json-path--schema) (`pip install assertpy2[json]`): JSONPath navigation (`at_json_path()`, `has_json_path()`) and JSON Schema validation (`matches_json_schema()`).
167
+ - [**Data frames**](https://solganis.github.io/assertpy2/integrations/#data-frames-and-arrays) (`pip install assertpy2[pandas]` / `[polars]` / `[numpy]`): fluent equality for pandas/polars frames and numpy arrays, carrying each library's own diff.
210
168
 
211
169
  ---
212
170
 
@@ -6,7 +6,7 @@ if TYPE_CHECKING:
6
6
  import logging
7
7
 
8
8
  from ._compat import Self
9
- from .errors import DiffResult
9
+ from .errors import DiffEntry, DiffResult
10
10
 
11
11
 
12
12
  class _MixinBase:
@@ -96,6 +96,11 @@ class _MixinBase:
96
96
  @staticmethod
97
97
  def _to_comparable_dict(obj: object) -> dict[str, object] | None: ...
98
98
 
99
+ @staticmethod
100
+ def _sub_diff_entries(
101
+ actual: object, expected: object, prefix: str, *, _seen: set[int] | None = ...
102
+ ) -> list[DiffEntry] | None: ...
103
+
99
104
  # NumericMixin class attrs used by HelpersMixin._validate_between_args
100
105
  _NUMERIC_COMPAREABLE: frozenset[type]
101
106
  _NUMERIC_NON_COMPAREABLE: frozenset[type]
@@ -34,6 +34,7 @@ from .base import BaseMixin
34
34
  from .bytes_mixin import BytesMixin
35
35
  from .collection import CollectionMixin
36
36
  from .contains import ContainsMixin
37
+ from .dataframe import DataFrameMixin
37
38
  from .date import DateMixin
38
39
  from .dict import DictMixin
39
40
  from .dynamic import DynamicMixin
@@ -48,7 +49,7 @@ from .snapshot import SnapshotMixin
48
49
  from .string import StringMixin
49
50
  from .warning import WarningMixin
50
51
 
51
- __version__ = "2.10.0"
52
+ __version__ = "2.11.0"
52
53
 
53
54
  __tracebackhide__ = True # clean tracebacks via py.test integration
54
55
  contextlib.__tracebackhide__ = True # ty: ignore[unresolved-attribute] # pytest monkey-patch
@@ -548,6 +549,7 @@ class AssertionBuilder(
548
549
  ContainsMixin,
549
550
  CollectionMixin,
550
551
  BytesMixin,
552
+ DataFrameMixin,
551
553
  BaseMixin,
552
554
  ):
553
555
  """The main assertion class. Never call the constructor directly, always use the
@@ -4,7 +4,7 @@ import collections.abc
4
4
  import dataclasses
5
5
  from typing import TYPE_CHECKING, Any, Final
6
6
 
7
- from ._introspection import is_model_dump_object, is_namedtuple
7
+ from ._introspection import is_mapping_like, is_model_dump_object, is_namedtuple
8
8
  from ._mixin_base import _MixinBase
9
9
  from .errors import DiffEntry, DiffResult
10
10
  from .matchers import Matcher, StructureMatcher
@@ -17,6 +17,34 @@ __tracebackhide__ = True
17
17
  _SENTINEL: Final = object()
18
18
 
19
19
 
20
+ def _ambiguous_array_operand(value: object, other: object) -> object | None:
21
+ """Return the array/frame-like operand whose ``==`` has no single truth value, else ``None``.
22
+
23
+ numpy/pandas/polars containers expose ``__array__`` and compare element-wise, so ``bool(a == b)``
24
+ raises rather than yielding one bool (and a ``DataFrame`` also quacks dict-like, which would otherwise
25
+ mis-dispatch the comparison). The ``__array__`` gate keeps the extra comparison off the hot path; the
26
+ truth test is actually attempted, so 0-d / scalar array values (which *are* truth-testable) pass
27
+ through unchanged.
28
+ """
29
+ for candidate, counterpart in ((value, other), (other, value)):
30
+ if hasattr(candidate, "__array__"):
31
+ try:
32
+ bool(candidate == counterpart)
33
+ except (ValueError, TypeError):
34
+ return candidate
35
+ return None
36
+
37
+
38
+ def _array_equality_error(method: str, operand: object) -> TypeError:
39
+ """Build the actionable error raised when ``method`` is given an element-wise array/frame-like."""
40
+ return TypeError(
41
+ f"{method}() cannot directly compare <{type(operand).__name__}>: its '==' is element-wise and has"
42
+ " no single truth value. Compare the value's own equality (e.g."
43
+ " assert_that(actual.equals(expected)).is_true()), assert on extracted scalars (columns, shape,"
44
+ " length), or use satisfies(...) with an explicit predicate."
45
+ )
46
+
47
+
20
48
  class BaseMixin(_MixinBase):
21
49
  """Base mixin."""
22
50
 
@@ -115,7 +143,9 @@ class BaseMixin(_MixinBase):
115
143
  Raises:
116
144
  AssertionError: if actual is **not** equal to expected
117
145
  TypeError: if ``ignore``/``include`` is a one-shot or otherwise unsupported iterable, or is
118
- used on a value that is neither dict-like nor has introspectable fields
146
+ used on a value that is neither dict-like nor has introspectable fields; or if val or
147
+ other is an element-wise array/frame-like (numpy/pandas/polars) whose ``==`` has no single
148
+ truth value (compare the value's own equality, e.g. ``actual.equals(expected)``, instead)
119
149
 
120
150
  Tip:
121
151
  Using :meth:`is_equal_to` with a ``float`` val is just asking for trouble. Instead, you'll
@@ -128,6 +158,10 @@ class BaseMixin(_MixinBase):
128
158
  ignore = kwargs.get("ignore")
129
159
  include = kwargs.get("include")
130
160
 
161
+ operand = _ambiguous_array_operand(self.val, other)
162
+ if operand is not None:
163
+ raise _array_equality_error("is_equal_to", operand)
164
+
131
165
  if self._is_dict_like(self.val, check_values=False) and self._is_dict_like(other, check_values=False):
132
166
  if self._dict_not_equal(self.val, other, ignore=ignore, include=include):
133
167
  self._dict_err(self.val, other, ignore=ignore, include=include)
@@ -335,26 +369,36 @@ class BaseMixin(_MixinBase):
335
369
  def _sub_diff_entries(
336
370
  actual: object, expected: object, prefix: str, *, _seen: set[int] | None = None
337
371
  ) -> list[DiffEntry] | None:
372
+ """Canonical recursive diff for a value, returning path-level entries (or ``None`` for a leaf).
373
+
374
+ Recurses into mappings, dataclasses, namedtuples, model-dump objects and sequences; returns
375
+ ``None`` for anything else so the caller renders a single leaf entry. This is the single nested
376
+ engine shared by the top-level paths: :meth:`_build_equality_diff` (lists, dataclasses, ...) and
377
+ the dict path (:meth:`HelpersMixin._dict_err`), which calls it with an empty ``prefix`` so the
378
+ top-level dict keys render bare (``b``) and nested keys render dotted (``u.b``).
379
+ """
338
380
  if _seen is None:
339
381
  _seen = set()
340
382
  if id(actual) in _seen or id(expected) in _seen:
341
383
  return [DiffEntry(path=prefix, actual="<circular ref>", expected="<circular ref>")]
342
384
 
343
- if isinstance(actual, dict) and isinstance(expected, dict):
385
+ if is_mapping_like(actual) and is_mapping_like(expected):
344
386
  child_seen = _seen | {id(actual), id(expected)}
345
387
  entries: list[DiffEntry] = []
346
- for key in sorted(set(actual) | set(expected), key=repr):
347
- path = f"{prefix}.{key}"
348
- if key not in expected:
349
- entries.append(DiffEntry(path=path, actual=actual[key], expected=None)) # ty: ignore[invalid-argument-type]
350
- elif key not in actual:
351
- entries.append(DiffEntry(path=path, actual=None, expected=expected[key])) # ty: ignore[invalid-argument-type]
352
- elif actual[key] != expected[key]: # ty: ignore[invalid-argument-type]
353
- sub_entries = BaseMixin._sub_diff_entries(actual[key], expected[key], path, _seen=child_seen) # ty: ignore[invalid-argument-type]
388
+ actual_keys = set(actual)
389
+ expected_keys = set(expected)
390
+ for key in sorted(actual_keys | expected_keys, key=repr):
391
+ path = f"{prefix}.{key}" if prefix else str(key)
392
+ if key not in expected_keys:
393
+ entries.append(DiffEntry(path=path, actual=actual[key], expected=None))
394
+ elif key not in actual_keys:
395
+ entries.append(DiffEntry(path=path, actual=None, expected=expected[key]))
396
+ elif actual[key] != expected[key]:
397
+ sub_entries = BaseMixin._sub_diff_entries(actual[key], expected[key], path, _seen=child_seen)
354
398
  if sub_entries is not None:
355
399
  entries.extend(sub_entries)
356
400
  else:
357
- entries.append(DiffEntry(path=path, actual=actual[key], expected=expected[key])) # ty: ignore[invalid-argument-type]
401
+ entries.append(DiffEntry(path=path, actual=actual[key], expected=expected[key]))
358
402
  return entries or None
359
403
  if (
360
404
  dataclasses.is_dataclass(actual)
@@ -715,7 +759,13 @@ class BaseMixin(_MixinBase):
715
759
 
716
760
  Raises:
717
761
  AssertionError: if actual **is** equal to expected
762
+ TypeError: if val or other is an element-wise array/frame-like (numpy/pandas/polars) whose
763
+ ``==`` has no single truth value; compare the value's own equality instead
718
764
  """
765
+ operand = _ambiguous_array_operand(self.val, other)
766
+ if operand is not None:
767
+ raise _array_equality_error("is_not_equal_to", operand)
768
+
719
769
  if self.val == other:
720
770
  return self.error(f"Expected <{self.val}> to be not equal to <{other}>, but was.")
721
771
  return self
@@ -0,0 +1,153 @@
1
+ """Optional fluent assertions for data-science containers (pandas / polars / numpy).
2
+
3
+ This is an *integration* layer in the same spirit as the Allure and Behave adapters: each library is its
4
+ own optional extra (``pip install assertpy2[pandas]`` / ``[polars]`` / ``[numpy]``, or ``[data]`` for all
5
+ three), imported lazily by name, so the core stays free of runtime dependencies. Comparison
6
+ **semantics** are delegated entirely to each library's own testing
7
+ utilities (``assert_frame_equal`` / ``assert_series_equal`` / ``assert_array_equal`` / ``assert_allclose``),
8
+ so dtype, tolerance and NaN handling match the library exactly. This layer only adds the fluent entry
9
+ point and routes failures through the standard assertpy2 error model.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import importlib
15
+ from typing import TYPE_CHECKING, Any
16
+
17
+ from ._mixin_base import _MixinBase
18
+
19
+ if TYPE_CHECKING:
20
+ from ._compat import Self
21
+
22
+ __tracebackhide__ = True
23
+
24
+ _FRAME_ROOTS = ("pandas", "polars")
25
+
26
+
27
+ def _ensure_module(name: str) -> Any:
28
+ """Import optional library *name* by string, or raise a clear ImportError pointing at its extra."""
29
+ try:
30
+ return importlib.import_module(name)
31
+ except ImportError:
32
+ raise ImportError(
33
+ f"{name} is required for these assertions. Install it with: pip install assertpy2[{name}]"
34
+ ) from None
35
+
36
+
37
+ def _load(root: str) -> tuple[Any, Any]:
38
+ """Return ``(library, library.testing)`` for *root* (``pandas``/``polars``/``numpy``)."""
39
+ library = _ensure_module(root)
40
+ return library, importlib.import_module(f"{root}.testing")
41
+
42
+
43
+ class DataFrameMixin(_MixinBase):
44
+ """Fluent assertions for pandas/polars frames and numpy arrays (optional ``[data]`` extra)."""
45
+
46
+ def is_frame_equal(self, expected: object, **options: Any) -> Self:
47
+ """Asserts that a pandas/polars ``DataFrame`` or ``Series`` equals *expected*.
48
+
49
+ Delegates to the owning library's own ``assert_frame_equal`` / ``assert_series_equal``, so all
50
+ comparison semantics (dtype strictness, row/column order, tolerance, categoricals, ...) are the
51
+ library's. Any keyword options are passed straight through.
52
+
53
+ Args:
54
+ expected: the expected frame/series (same library as val)
55
+ **options: keyword options forwarded to the library's ``assert_frame_equal`` /
56
+ ``assert_series_equal`` (e.g. ``check_dtype=False``, ``check_exact=False``, ``rtol=1e-3``)
57
+
58
+ Examples:
59
+ Usage::
60
+
61
+ import pandas as pd
62
+
63
+ assert_that(pd.DataFrame({"a": [1, 2]})).is_frame_equal(pd.DataFrame({"a": [1, 2]}))
64
+ assert_that(actual).is_frame_equal(expected, check_dtype=False)
65
+
66
+ Returns:
67
+ AssertionBuilder: returns this instance to chain to the next assertion
68
+
69
+ Raises:
70
+ AssertionError: if the frames/series are not equal (carrying the library's own diff message)
71
+ TypeError: if val is not a pandas or polars ``DataFrame``/``Series``
72
+ ImportError: if the owning library is not installed
73
+ """
74
+ actual = self.val
75
+ root = type(actual).__module__.split(".", 1)[0]
76
+ if root not in _FRAME_ROOTS:
77
+ raise TypeError(
78
+ f"is_frame_equal() expects a pandas or polars DataFrame/Series, but was <{type(actual).__name__}>."
79
+ )
80
+ library, testing = _load(root)
81
+ if isinstance(actual, library.Series):
82
+ assert_equal, label = testing.assert_series_equal, "Series"
83
+ else:
84
+ assert_equal, label = testing.assert_frame_equal, "DataFrame"
85
+ try:
86
+ assert_equal(actual, expected, **options)
87
+ except AssertionError as exc:
88
+ return self.error(f"Expected the {label} to equal the expected one, but they differ:\n{exc}")
89
+ return self
90
+
91
+ def is_array_equal(self, expected: object) -> Self:
92
+ """Asserts that val equals *expected* element-wise, via numpy's ``assert_array_equal``.
93
+
94
+ Works on any array-likes numpy can coerce (``ndarray``, nested lists, ...); shape and every
95
+ element must match exactly (with ``NaN`` treated as equal, per numpy).
96
+
97
+ Args:
98
+ expected: the expected array-like
99
+
100
+ Examples:
101
+ Usage::
102
+
103
+ import numpy as np
104
+
105
+ assert_that(np.array([1, 2, 3])).is_array_equal(np.array([1, 2, 3]))
106
+
107
+ Returns:
108
+ AssertionBuilder: returns this instance to chain to the next assertion
109
+
110
+ Raises:
111
+ AssertionError: if the arrays are not equal (carrying numpy's own diff message)
112
+ ImportError: if numpy is not installed
113
+ """
114
+ _, testing = _load("numpy")
115
+ try:
116
+ testing.assert_array_equal(self.val, expected)
117
+ except AssertionError as exc:
118
+ return self.error(f"Expected the arrays to be equal, but they differ:\n{exc}")
119
+ return self
120
+
121
+ def is_array_close_to(
122
+ self, expected: object, *, rtol: float = 1e-05, atol: float = 1e-08, equal_nan: bool = False
123
+ ) -> Self:
124
+ """Asserts that val is element-wise close to *expected*, via numpy's ``assert_allclose``.
125
+
126
+ The float-tolerant counterpart to :meth:`is_array_equal`, for comparing computed arrays.
127
+
128
+ Args:
129
+ expected: the expected array-like
130
+ rtol: relative tolerance (numpy default ``1e-05``)
131
+ atol: absolute tolerance (numpy default ``1e-08``)
132
+ equal_nan: whether ``NaN`` in the same position compares equal
133
+
134
+ Examples:
135
+ Usage::
136
+
137
+ import numpy as np
138
+
139
+ assert_that(np.array([1.0, 2.0])).is_array_close_to(np.array([1.0, 2.0000001]))
140
+
141
+ Returns:
142
+ AssertionBuilder: returns this instance to chain to the next assertion
143
+
144
+ Raises:
145
+ AssertionError: if the arrays are not close (carrying numpy's own diff message)
146
+ ImportError: if numpy is not installed
147
+ """
148
+ _, testing = _load("numpy")
149
+ try:
150
+ testing.assert_allclose(self.val, expected, rtol=rtol, atol=atol, equal_nan=equal_nan)
151
+ except AssertionError as exc:
152
+ return self.error(f"Expected the arrays to be close, but they differ:\n{exc}")
153
+ return self