assertpy2 2.8.0__tar.gz → 2.8.1__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.8.0 → assertpy2-2.8.1}/PKG-INFO +18 -17
- {assertpy2-2.8.0 → assertpy2-2.8.1}/README.md +17 -16
- assertpy2-2.8.1/assertpy2/_introspection.py +66 -0
- {assertpy2-2.8.0 → assertpy2-2.8.1}/assertpy2/_mixin_base.py +2 -1
- {assertpy2-2.8.0 → assertpy2-2.8.1}/assertpy2/assertpy.py +11 -5
- {assertpy2-2.8.0 → assertpy2-2.8.1}/assertpy2/async_assertions.py +1 -1
- {assertpy2-2.8.0 → assertpy2-2.8.1}/assertpy2/base.py +34 -41
- {assertpy2-2.8.0 → assertpy2-2.8.1}/assertpy2/behave_matchers.py +14 -9
- {assertpy2-2.8.0 → assertpy2-2.8.1}/assertpy2/collection.py +7 -7
- {assertpy2-2.8.0 → assertpy2-2.8.1}/assertpy2/contains.py +11 -17
- {assertpy2-2.8.0 → assertpy2-2.8.1}/assertpy2/date.py +2 -2
- {assertpy2-2.8.0 → assertpy2-2.8.1}/assertpy2/dict.py +8 -10
- {assertpy2-2.8.0 → assertpy2-2.8.1}/assertpy2/dynamic.py +6 -5
- {assertpy2-2.8.0 → assertpy2-2.8.1}/assertpy2/errors.py +3 -4
- {assertpy2-2.8.0 → assertpy2-2.8.1}/assertpy2/exception.py +3 -3
- {assertpy2-2.8.0 → assertpy2-2.8.1}/assertpy2/extracting.py +4 -6
- {assertpy2-2.8.0 → assertpy2-2.8.1}/assertpy2/file.py +2 -2
- {assertpy2-2.8.0 → assertpy2-2.8.1}/assertpy2/helpers.py +12 -12
- {assertpy2-2.8.0 → assertpy2-2.8.1}/assertpy2/matchers.py +25 -22
- {assertpy2-2.8.0 → assertpy2-2.8.1}/assertpy2/pytest_plugin.py +16 -15
- {assertpy2-2.8.0 → assertpy2-2.8.1}/assertpy2/snapshot.py +7 -4
- {assertpy2-2.8.0 → assertpy2-2.8.1}/assertpy2/string.py +7 -5
- {assertpy2-2.8.0 → assertpy2-2.8.1}/docs/assertions.md +15 -1
- assertpy2-2.8.1/docs/assets/diff-equal.png +0 -0
- assertpy2-2.8.1/docs/assets/diff-equal.svg +64 -0
- assertpy2-2.8.1/docs/assets/diff-match.svg +51 -0
- assertpy2-2.8.1/docs/assets/diff-sequence.svg +52 -0
- assertpy2-2.8.1/docs/assets/diff-set.svg +48 -0
- {assertpy2-2.8.0 → assertpy2-2.8.1}/docs/comparison.md +27 -21
- {assertpy2-2.8.0 → assertpy2-2.8.1}/docs/errors.md +21 -18
- {assertpy2-2.8.0 → assertpy2-2.8.1}/docs/extending.md +9 -0
- {assertpy2-2.8.0 → assertpy2-2.8.1}/docs/getting-started.md +5 -7
- {assertpy2-2.8.0 → assertpy2-2.8.1}/docs/index.md +13 -8
- {assertpy2-2.8.0 → assertpy2-2.8.1}/docs/matchers.md +15 -6
- {assertpy2-2.8.0 → assertpy2-2.8.1}/docs/migration.md +1 -1
- assertpy2-2.8.1/docs/type-safety.md +61 -0
- {assertpy2-2.8.0 → assertpy2-2.8.1}/mkdocs.yml +1 -0
- {assertpy2-2.8.0 → assertpy2-2.8.1}/pyproject.toml +4 -4
- {assertpy2-2.8.0 → assertpy2-2.8.1}/tests/test_datetime.py +16 -8
- {assertpy2-2.8.0 → assertpy2-2.8.1}/tests/test_dict_compare.py +10 -5
- {assertpy2-2.8.0 → assertpy2-2.8.1}/tests/test_expected_exception.py +4 -2
- {assertpy2-2.8.0 → assertpy2-2.8.1}/tests/test_pytest_plugin.py +19 -1
- {assertpy2-2.8.0 → assertpy2-2.8.1}/uv.lock +1 -1
- {assertpy2-2.8.0 → assertpy2-2.8.1}/.codecov.yml +0 -0
- {assertpy2-2.8.0 → assertpy2-2.8.1}/.gitattributes +0 -0
- {assertpy2-2.8.0 → assertpy2-2.8.1}/.github/dependabot.yml +0 -0
- {assertpy2-2.8.0 → assertpy2-2.8.1}/.github/workflows/ci.yml +0 -0
- {assertpy2-2.8.0 → assertpy2-2.8.1}/.github/workflows/codeql.yml +0 -0
- {assertpy2-2.8.0 → assertpy2-2.8.1}/.github/workflows/docs.yml +0 -0
- {assertpy2-2.8.0 → assertpy2-2.8.1}/.github/workflows/publish.yml +0 -0
- {assertpy2-2.8.0 → assertpy2-2.8.1}/.github/workflows/scorecard.yml +0 -0
- {assertpy2-2.8.0 → assertpy2-2.8.1}/.github/workflows/zizmor.yml +0 -0
- {assertpy2-2.8.0 → assertpy2-2.8.1}/.gitignore +0 -0
- {assertpy2-2.8.0 → assertpy2-2.8.1}/CONTRIBUTING.md +0 -0
- {assertpy2-2.8.0 → assertpy2-2.8.1}/LICENSE +0 -0
- {assertpy2-2.8.0 → assertpy2-2.8.1}/SECURITY.md +0 -0
- {assertpy2-2.8.0 → assertpy2-2.8.1}/assertpy2/__init__.py +0 -0
- {assertpy2-2.8.0 → assertpy2-2.8.1}/assertpy2/_compat.py +0 -0
- {assertpy2-2.8.0 → assertpy2-2.8.1}/assertpy2/_typing.py +0 -0
- {assertpy2-2.8.0 → assertpy2-2.8.1}/assertpy2/bytes_mixin.py +0 -0
- {assertpy2-2.8.0 → assertpy2-2.8.1}/assertpy2/json_mixin.py +0 -0
- {assertpy2-2.8.0 → assertpy2-2.8.1}/assertpy2/numeric.py +0 -0
- {assertpy2-2.8.0 → assertpy2-2.8.1}/assertpy2/py.typed +0 -0
- {assertpy2-2.8.0 → assertpy2-2.8.1}/assertpy2/warning.py +0 -0
- {assertpy2-2.8.0 → assertpy2-2.8.1}/docs/data.md +0 -0
- {assertpy2-2.8.0 → assertpy2-2.8.1}/docs/fluent.md +0 -0
- {assertpy2-2.8.0 → assertpy2-2.8.1}/docs/integrations.md +0 -0
- {assertpy2-2.8.0 → assertpy2-2.8.1}/docs/logo-dark.svg +0 -0
- {assertpy2-2.8.0 → assertpy2-2.8.1}/docs/logo.svg +0 -0
- {assertpy2-2.8.0 → assertpy2-2.8.1}/docs/testing.md +0 -0
- {assertpy2-2.8.0 → assertpy2-2.8.1}/tests/test_async.py +0 -0
- {assertpy2-2.8.0 → assertpy2-2.8.1}/tests/test_behave_matchers.py +0 -0
- {assertpy2-2.8.0 → assertpy2-2.8.1}/tests/test_bool.py +0 -0
- {assertpy2-2.8.0 → assertpy2-2.8.1}/tests/test_bytes.py +0 -0
- {assertpy2-2.8.0 → assertpy2-2.8.1}/tests/test_class.py +0 -0
- {assertpy2-2.8.0 → assertpy2-2.8.1}/tests/test_collection.py +0 -0
- {assertpy2-2.8.0 → assertpy2-2.8.1}/tests/test_core.py +0 -0
- {assertpy2-2.8.0 → assertpy2-2.8.1}/tests/test_custom_dict.py +0 -0
- {assertpy2-2.8.0 → assertpy2-2.8.1}/tests/test_custom_list.py +0 -0
- {assertpy2-2.8.0 → assertpy2-2.8.1}/tests/test_description.py +0 -0
- {assertpy2-2.8.0 → assertpy2-2.8.1}/tests/test_dict.py +0 -0
- {assertpy2-2.8.0 → assertpy2-2.8.1}/tests/test_dyn.py +0 -0
- {assertpy2-2.8.0 → assertpy2-2.8.1}/tests/test_equals.py +0 -0
- {assertpy2-2.8.0 → assertpy2-2.8.1}/tests/test_errors.py +0 -0
- {assertpy2-2.8.0 → assertpy2-2.8.1}/tests/test_expected_warning.py +0 -0
- {assertpy2-2.8.0 → assertpy2-2.8.1}/tests/test_extensions.py +0 -0
- {assertpy2-2.8.0 → assertpy2-2.8.1}/tests/test_extracting.py +0 -0
- {assertpy2-2.8.0 → assertpy2-2.8.1}/tests/test_fail.py +0 -0
- {assertpy2-2.8.0 → assertpy2-2.8.1}/tests/test_file.py +0 -0
- {assertpy2-2.8.0 → assertpy2-2.8.1}/tests/test_grouped_soft.py +0 -0
- {assertpy2-2.8.0 → assertpy2-2.8.1}/tests/test_in.py +0 -0
- {assertpy2-2.8.0 → assertpy2-2.8.1}/tests/test_json.py +0 -0
- {assertpy2-2.8.0 → assertpy2-2.8.1}/tests/test_list.py +0 -0
- {assertpy2-2.8.0 → assertpy2-2.8.1}/tests/test_matcher_registry.py +0 -0
- {assertpy2-2.8.0 → assertpy2-2.8.1}/tests/test_matchers.py +0 -0
- {assertpy2-2.8.0 → assertpy2-2.8.1}/tests/test_matchers_phase3.py +0 -0
- {assertpy2-2.8.0 → assertpy2-2.8.1}/tests/test_namedtuple.py +0 -0
- {assertpy2-2.8.0 → assertpy2-2.8.1}/tests/test_none.py +0 -0
- {assertpy2-2.8.0 → assertpy2-2.8.1}/tests/test_not.py +0 -0
- {assertpy2-2.8.0 → assertpy2-2.8.1}/tests/test_numbers.py +0 -0
- {assertpy2-2.8.0 → assertpy2-2.8.1}/tests/test_overloads.py +0 -0
- {assertpy2-2.8.0 → assertpy2-2.8.1}/tests/test_phase2.py +0 -0
- {assertpy2-2.8.0 → assertpy2-2.8.1}/tests/test_pipeline.py +0 -0
- {assertpy2-2.8.0 → assertpy2-2.8.1}/tests/test_property_based.py +0 -0
- {assertpy2-2.8.0 → assertpy2-2.8.1}/tests/test_readme.py +0 -0
- {assertpy2-2.8.0 → assertpy2-2.8.1}/tests/test_recursive_compare.py +0 -0
- {assertpy2-2.8.0 → assertpy2-2.8.1}/tests/test_recursive_compare_attrs.py +0 -0
- {assertpy2-2.8.0 → assertpy2-2.8.1}/tests/test_recursive_compare_pydantic.py +0 -0
- {assertpy2-2.8.0 → assertpy2-2.8.1}/tests/test_regex_groups.py +0 -0
- {assertpy2-2.8.0 → assertpy2-2.8.1}/tests/test_rich_diff.py +0 -0
- {assertpy2-2.8.0 → assertpy2-2.8.1}/tests/test_same_as.py +0 -0
- {assertpy2-2.8.0 → assertpy2-2.8.1}/tests/test_snapshots.py +0 -0
- {assertpy2-2.8.0 → assertpy2-2.8.1}/tests/test_soft.py +0 -0
- {assertpy2-2.8.0 → assertpy2-2.8.1}/tests/test_soft_fail.py +0 -0
- {assertpy2-2.8.0 → assertpy2-2.8.1}/tests/test_string.py +0 -0
- {assertpy2-2.8.0 → assertpy2-2.8.1}/tests/test_structural.py +0 -0
- {assertpy2-2.8.0 → assertpy2-2.8.1}/tests/test_traceback.py +0 -0
- {assertpy2-2.8.0 → assertpy2-2.8.1}/tests/test_type.py +0 -0
- {assertpy2-2.8.0 → assertpy2-2.8.1}/tests/test_warn.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: assertpy2
|
|
3
|
-
Version: 2.8.
|
|
3
|
+
Version: 2.8.1
|
|
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
|
|
@@ -66,7 +66,7 @@ Description-Content-Type: text/markdown
|
|
|
66
66
|
|
|
67
67
|
---
|
|
68
68
|
|
|
69
|
-
## Quick start
|
|
69
|
+
## [Quick start](https://solganis.github.io/assertpy2/getting-started/)
|
|
70
70
|
|
|
71
71
|
```bash
|
|
72
72
|
pip install assertpy2 # drop-in replacement for assertpy, just change the import
|
|
@@ -84,10 +84,12 @@ def test_user():
|
|
|
84
84
|
assert_that(user).has_name("Alice")
|
|
85
85
|
```
|
|
86
86
|
|
|
87
|
-
|
|
87
|
+
Browse the [full documentation](https://solganis.github.io/assertpy2/) for every assertion, matcher, and integration.
|
|
88
|
+
|
|
89
|
+
## [Why fluent assertions?](https://solganis.github.io/assertpy2/comparison/)
|
|
88
90
|
|
|
89
91
|
A fluent chain reads as one intent and replaces several bare asserts - and your IDE
|
|
90
|
-
offers only the methods that fit the value's type:
|
|
92
|
+
offers only the [methods that fit the value's type](https://solganis.github.io/assertpy2/type-safety/):
|
|
91
93
|
|
|
92
94
|
```py
|
|
93
95
|
# bare - three statements, no autocomplete help
|
|
@@ -111,25 +113,19 @@ E {'user': {'name': 'Alice', 'role': 'superadmin'}} != {'user': {'name': 'Al
|
|
|
111
113
|
E {'status': 'active'} != {'status': 'disabled'}
|
|
112
114
|
```
|
|
113
115
|
|
|
114
|
-
assertpy2 reports the exact path to every difference:
|
|
116
|
+
assertpy2 reports the [exact path to every difference](https://solganis.github.io/assertpy2/errors/#rich-pytest-diffs), in color:
|
|
115
117
|
|
|
116
|
-
```
|
|
118
|
+
```python
|
|
117
119
|
assert_that(response).is_equal_to(expected)
|
|
118
|
-
--- Structured Diff ---
|
|
119
|
-
diff (dict):
|
|
120
|
-
status:
|
|
121
|
-
- 'active'
|
|
122
|
-
+ 'disabled'
|
|
123
|
-
user.role:
|
|
124
|
-
- 'superadmin'
|
|
125
|
-
+ 'admin'
|
|
126
120
|
```
|
|
127
121
|
|
|
122
|
+
<img src="docs/assets/diff-equal.png" width="300" alt="Structured diff in the terminal: status and user.role shown with their paths, removals in red and additions in green">
|
|
123
|
+
|
|
128
124
|
Recursive diffs work for dicts, dataclasses, namedtuples, attrs, and Pydantic models.
|
|
129
125
|
For responses with dynamic fields (IDs, timestamps), validate a subset with
|
|
130
|
-
`matches_structure()` instead of exact equality.
|
|
126
|
+
[`matches_structure()`](https://solganis.github.io/assertpy2/matchers/#structural-matching) instead of exact equality.
|
|
131
127
|
|
|
132
|
-
## Type-aware autocomplete
|
|
128
|
+
## [Type-aware autocomplete](https://solganis.github.io/assertpy2/type-safety/)
|
|
133
129
|
|
|
134
130
|
`assert_that()` uses `@overload` to return type-specific Protocols.
|
|
135
131
|
Your IDE shows only methods relevant to the value you're testing, not all 100+:
|
|
@@ -142,6 +138,8 @@ Your IDE shows only methods relevant to the value you're testing, not all 100+:
|
|
|
142
138
|
|
|
143
139
|
9 type-specific Protocols instead of one `Any`. Works in PyCharm, VS Code, and any LSP-compatible editor.
|
|
144
140
|
|
|
141
|
+
See the [**Type Safety**](https://solganis.github.io/assertpy2/type-safety/) guide for the full walkthrough.
|
|
142
|
+
|
|
145
143
|
---
|
|
146
144
|
|
|
147
145
|
## Features
|
|
@@ -170,6 +168,7 @@ Your IDE shows only methods relevant to the value you're testing, not all 100+:
|
|
|
170
168
|
- [**Structured errors**](https://solganis.github.io/assertpy2/errors/#structured-errors): `AssertionFailure` with `.actual`, `.expected`, `.diff` attributes.
|
|
171
169
|
- [**Rich pytest diffs**](https://solganis.github.io/assertpy2/errors/#rich-pytest-diffs): recursive structural diffs for lists, sets, strings, dicts, dataclasses, namedtuples, Pydantic models, and matcher-based assertions (`matches_structure()`, `satisfies()`, `each()`). Circular reference protection.
|
|
172
170
|
- [**Snapshot testing**](https://solganis.github.io/assertpy2/testing/#snapshot-testing): store and compare data structures in JSON format.
|
|
171
|
+
- **Property-based tested**: comparison, selective-diff, matcher algebra, and collection logic are checked with [Hypothesis](https://hypothesis.readthedocs.io) against reference semantics, on top of 100% branch coverage.
|
|
173
172
|
|
|
174
173
|
**Type safety**
|
|
175
174
|
|
|
@@ -191,7 +190,7 @@ See the [full documentation](https://solganis.github.io/assertpy2/) for all asse
|
|
|
191
190
|
|
|
192
191
|
---
|
|
193
192
|
|
|
194
|
-
## Integrations
|
|
193
|
+
## [Integrations](https://solganis.github.io/assertpy2/integrations/)
|
|
195
194
|
|
|
196
195
|
### Allure
|
|
197
196
|
|
|
@@ -239,6 +238,8 @@ def step_impl(context, age):
|
|
|
239
238
|
|
|
240
239
|
Available types: `PositiveInt`, `NonNegativeInt`, `PositiveFloat`, `NonEmptyString`, `BoolLike`.
|
|
241
240
|
|
|
241
|
+
See the [Integrations guide](https://solganis.github.io/assertpy2/integrations/) for attachment modes, configuration, and full examples.
|
|
242
|
+
|
|
242
243
|
---
|
|
243
244
|
|
|
244
245
|
<p align="center">
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
|
|
29
29
|
---
|
|
30
30
|
|
|
31
|
-
## Quick start
|
|
31
|
+
## [Quick start](https://solganis.github.io/assertpy2/getting-started/)
|
|
32
32
|
|
|
33
33
|
```bash
|
|
34
34
|
pip install assertpy2 # drop-in replacement for assertpy, just change the import
|
|
@@ -46,10 +46,12 @@ def test_user():
|
|
|
46
46
|
assert_that(user).has_name("Alice")
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
Browse the [full documentation](https://solganis.github.io/assertpy2/) for every assertion, matcher, and integration.
|
|
50
|
+
|
|
51
|
+
## [Why fluent assertions?](https://solganis.github.io/assertpy2/comparison/)
|
|
50
52
|
|
|
51
53
|
A fluent chain reads as one intent and replaces several bare asserts - and your IDE
|
|
52
|
-
offers only the methods that fit the value's type:
|
|
54
|
+
offers only the [methods that fit the value's type](https://solganis.github.io/assertpy2/type-safety/):
|
|
53
55
|
|
|
54
56
|
```py
|
|
55
57
|
# bare - three statements, no autocomplete help
|
|
@@ -73,25 +75,19 @@ E {'user': {'name': 'Alice', 'role': 'superadmin'}} != {'user': {'name': 'Al
|
|
|
73
75
|
E {'status': 'active'} != {'status': 'disabled'}
|
|
74
76
|
```
|
|
75
77
|
|
|
76
|
-
assertpy2 reports the exact path to every difference:
|
|
78
|
+
assertpy2 reports the [exact path to every difference](https://solganis.github.io/assertpy2/errors/#rich-pytest-diffs), in color:
|
|
77
79
|
|
|
78
|
-
```
|
|
80
|
+
```python
|
|
79
81
|
assert_that(response).is_equal_to(expected)
|
|
80
|
-
--- Structured Diff ---
|
|
81
|
-
diff (dict):
|
|
82
|
-
status:
|
|
83
|
-
- 'active'
|
|
84
|
-
+ 'disabled'
|
|
85
|
-
user.role:
|
|
86
|
-
- 'superadmin'
|
|
87
|
-
+ 'admin'
|
|
88
82
|
```
|
|
89
83
|
|
|
84
|
+
<img src="docs/assets/diff-equal.png" width="300" alt="Structured diff in the terminal: status and user.role shown with their paths, removals in red and additions in green">
|
|
85
|
+
|
|
90
86
|
Recursive diffs work for dicts, dataclasses, namedtuples, attrs, and Pydantic models.
|
|
91
87
|
For responses with dynamic fields (IDs, timestamps), validate a subset with
|
|
92
|
-
`matches_structure()` instead of exact equality.
|
|
88
|
+
[`matches_structure()`](https://solganis.github.io/assertpy2/matchers/#structural-matching) instead of exact equality.
|
|
93
89
|
|
|
94
|
-
## Type-aware autocomplete
|
|
90
|
+
## [Type-aware autocomplete](https://solganis.github.io/assertpy2/type-safety/)
|
|
95
91
|
|
|
96
92
|
`assert_that()` uses `@overload` to return type-specific Protocols.
|
|
97
93
|
Your IDE shows only methods relevant to the value you're testing, not all 100+:
|
|
@@ -104,6 +100,8 @@ Your IDE shows only methods relevant to the value you're testing, not all 100+:
|
|
|
104
100
|
|
|
105
101
|
9 type-specific Protocols instead of one `Any`. Works in PyCharm, VS Code, and any LSP-compatible editor.
|
|
106
102
|
|
|
103
|
+
See the [**Type Safety**](https://solganis.github.io/assertpy2/type-safety/) guide for the full walkthrough.
|
|
104
|
+
|
|
107
105
|
---
|
|
108
106
|
|
|
109
107
|
## Features
|
|
@@ -132,6 +130,7 @@ Your IDE shows only methods relevant to the value you're testing, not all 100+:
|
|
|
132
130
|
- [**Structured errors**](https://solganis.github.io/assertpy2/errors/#structured-errors): `AssertionFailure` with `.actual`, `.expected`, `.diff` attributes.
|
|
133
131
|
- [**Rich pytest diffs**](https://solganis.github.io/assertpy2/errors/#rich-pytest-diffs): recursive structural diffs for lists, sets, strings, dicts, dataclasses, namedtuples, Pydantic models, and matcher-based assertions (`matches_structure()`, `satisfies()`, `each()`). Circular reference protection.
|
|
134
132
|
- [**Snapshot testing**](https://solganis.github.io/assertpy2/testing/#snapshot-testing): store and compare data structures in JSON format.
|
|
133
|
+
- **Property-based tested**: comparison, selective-diff, matcher algebra, and collection logic are checked with [Hypothesis](https://hypothesis.readthedocs.io) against reference semantics, on top of 100% branch coverage.
|
|
135
134
|
|
|
136
135
|
**Type safety**
|
|
137
136
|
|
|
@@ -153,7 +152,7 @@ See the [full documentation](https://solganis.github.io/assertpy2/) for all asse
|
|
|
153
152
|
|
|
154
153
|
---
|
|
155
154
|
|
|
156
|
-
## Integrations
|
|
155
|
+
## [Integrations](https://solganis.github.io/assertpy2/integrations/)
|
|
157
156
|
|
|
158
157
|
### Allure
|
|
159
158
|
|
|
@@ -201,6 +200,8 @@ def step_impl(context, age):
|
|
|
201
200
|
|
|
202
201
|
Available types: `PositiveInt`, `NonNegativeInt`, `PositiveFloat`, `NonEmptyString`, `BoolLike`.
|
|
203
202
|
|
|
203
|
+
See the [Integrations guide](https://solganis.github.io/assertpy2/integrations/) for attachment modes, configuration, and full examples.
|
|
204
|
+
|
|
204
205
|
---
|
|
205
206
|
|
|
206
207
|
<p align="center">
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Runtime-checkable protocols for duck-typed object introspection.
|
|
2
|
+
|
|
3
|
+
The comparison and diff code accepts arbitrary user objects, so it inspects them structurally: pydantic
|
|
4
|
+
models (``model_dump``), ``attrs`` classes (``__attrs_attrs__``) and namedtuples (``_fields`` /
|
|
5
|
+
``_asdict``). Expressing those shapes as ``@runtime_checkable`` protocols lets the diff code use
|
|
6
|
+
``isinstance`` checks the type checker can follow - narrowing the value instead of probing it with
|
|
7
|
+
``hasattr`` and then suppressing the attribute access.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from typing import Any, Protocol, TypeGuard, runtime_checkable
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@runtime_checkable
|
|
16
|
+
class SupportsModelDump(Protocol):
|
|
17
|
+
"""A pydantic-style model exposing ``model_dump()``."""
|
|
18
|
+
|
|
19
|
+
def model_dump(self) -> dict[str, Any]: ...
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@runtime_checkable
|
|
23
|
+
class NamedTupleLike(Protocol):
|
|
24
|
+
"""A ``collections.namedtuple`` / ``typing.NamedTuple`` instance."""
|
|
25
|
+
|
|
26
|
+
_fields: tuple[str, ...]
|
|
27
|
+
|
|
28
|
+
def _asdict(self) -> dict[str, Any]: ...
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@runtime_checkable
|
|
32
|
+
class AttrsInstance(Protocol):
|
|
33
|
+
"""An ``attrs``-decorated instance exposing ``__attrs_attrs__``."""
|
|
34
|
+
|
|
35
|
+
__attrs_attrs__: tuple[Any, ...]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@runtime_checkable
|
|
39
|
+
class MappingLike(Protocol):
|
|
40
|
+
"""A dict-like object that can be iterated over and subscripted by key."""
|
|
41
|
+
|
|
42
|
+
def keys(self) -> Any: ...
|
|
43
|
+
|
|
44
|
+
def __iter__(self) -> Any: ...
|
|
45
|
+
|
|
46
|
+
def __getitem__(self, key: Any) -> Any: ...
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def is_model_dump_object(obj: object) -> TypeGuard[SupportsModelDump]:
|
|
50
|
+
"""Return whether ``obj`` exposes a callable ``model_dump()`` (e.g. a pydantic model)."""
|
|
51
|
+
return isinstance(obj, SupportsModelDump) and callable(obj.model_dump)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def is_namedtuple(obj: object) -> TypeGuard[NamedTupleLike]:
|
|
55
|
+
"""Return whether ``obj`` is a namedtuple instance (a ``tuple`` carrying ``_fields``/``_asdict``)."""
|
|
56
|
+
return isinstance(obj, tuple) and isinstance(obj, NamedTupleLike)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def is_attrs_instance(obj: object) -> TypeGuard[AttrsInstance]:
|
|
60
|
+
"""Return whether ``obj`` is an ``attrs``-decorated instance."""
|
|
61
|
+
return isinstance(obj, AttrsInstance)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def is_mapping_like(obj: object) -> TypeGuard[MappingLike]:
|
|
65
|
+
"""Return whether ``obj`` is dict-like: iterable with ``keys()`` and ``[]`` access."""
|
|
66
|
+
return isinstance(obj, MappingLike) and callable(obj.keys)
|
|
@@ -9,17 +9,18 @@ import logging
|
|
|
9
9
|
import os
|
|
10
10
|
import sys
|
|
11
11
|
import types
|
|
12
|
-
from
|
|
13
|
-
from typing import TYPE_CHECKING, overload
|
|
12
|
+
from typing import TYPE_CHECKING, Final, overload
|
|
14
13
|
|
|
15
14
|
if TYPE_CHECKING:
|
|
16
15
|
import datetime
|
|
17
16
|
import pathlib
|
|
17
|
+
from collections.abc import Callable, Iterator
|
|
18
18
|
|
|
19
19
|
from ._compat import Self
|
|
20
20
|
from ._typing import (
|
|
21
21
|
_BytesAssertion,
|
|
22
22
|
_CallableAssertion,
|
|
23
|
+
_CoreAssertion,
|
|
23
24
|
_DateAssertion,
|
|
24
25
|
_DictAssertion,
|
|
25
26
|
_IterableAssertion,
|
|
@@ -47,13 +48,13 @@ from .snapshot import SnapshotMixin
|
|
|
47
48
|
from .string import StringMixin
|
|
48
49
|
from .warning import WarningMixin
|
|
49
50
|
|
|
50
|
-
__version__ = "2.8.
|
|
51
|
+
__version__ = "2.8.1"
|
|
51
52
|
|
|
52
53
|
__tracebackhide__ = True # clean tracebacks via py.test integration
|
|
53
54
|
contextlib.__tracebackhide__ = True # ty: ignore[unresolved-attribute] # pytest monkey-patch
|
|
54
55
|
|
|
55
56
|
# assertpy files
|
|
56
|
-
ASSERTPY_FILES = [
|
|
57
|
+
ASSERTPY_FILES: Final = [
|
|
57
58
|
os.path.join("assertpy2", file)
|
|
58
59
|
for file in [
|
|
59
60
|
"assertpy.py",
|
|
@@ -253,11 +254,16 @@ def assert_that(val: bytes | bytearray, description: str = "") -> _BytesAssertio
|
|
|
253
254
|
def assert_that(val: Callable[..., object], description: str = "") -> _CallableAssertion: ...
|
|
254
255
|
|
|
255
256
|
|
|
257
|
+
# Fallback returns the concrete AssertionBuilder so object- and union-typed values keep the full API.
|
|
258
|
+
# The specific protocols are not assignable to AssertionBuilder, so mypy --strict reports overload-overlap
|
|
259
|
+
# for each specific overload and pyright one reportOverlappingOverload; ty (the gate) does not flag it. Kept
|
|
260
|
+
# intentionally - returning _CoreAssertion here would strip type-specific assertions from object/union values.
|
|
256
261
|
@overload
|
|
257
262
|
def assert_that(val: object, description: str = "") -> AssertionBuilder: ...
|
|
258
263
|
|
|
259
264
|
|
|
260
|
-
|
|
265
|
+
# Return the common base protocol so each overload stays consistent with the impl (no reportInconsistentOverload).
|
|
266
|
+
def assert_that(val, description="") -> _CoreAssertion:
|
|
261
267
|
"""Set the value to be tested, plus an optional description, and allow assertions to be called.
|
|
262
268
|
|
|
263
269
|
This is a factory method for the :class:`AssertionBuilder`, and the single most important
|
|
@@ -69,7 +69,7 @@ class AsyncAssertionBuilder:
|
|
|
69
69
|
method = getattr(builder, name)
|
|
70
70
|
method(*args, **kwargs)
|
|
71
71
|
return builder
|
|
72
|
-
except AssertionError as exc:
|
|
72
|
+
except AssertionError as exc: # noqa: PERF203 # retry-on-failure needs the try/except per poll iteration
|
|
73
73
|
last_error = exc
|
|
74
74
|
if loop.time() >= deadline:
|
|
75
75
|
raise AssertionError(
|
|
@@ -2,8 +2,9 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import collections.abc
|
|
4
4
|
import dataclasses
|
|
5
|
-
from typing import TYPE_CHECKING
|
|
5
|
+
from typing import TYPE_CHECKING, Any, Final
|
|
6
6
|
|
|
7
|
+
from ._introspection import is_model_dump_object, is_namedtuple
|
|
7
8
|
from ._mixin_base import _MixinBase
|
|
8
9
|
from .errors import DiffEntry, DiffResult
|
|
9
10
|
from .matchers import Matcher, StructureMatcher
|
|
@@ -13,11 +14,7 @@ if TYPE_CHECKING:
|
|
|
13
14
|
|
|
14
15
|
__tracebackhide__ = True
|
|
15
16
|
|
|
16
|
-
_SENTINEL = object()
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def _is_model_dump_object(obj: object) -> bool:
|
|
20
|
-
return hasattr(obj, "model_dump") and callable(obj.model_dump)
|
|
17
|
+
_SENTINEL: Final = object()
|
|
21
18
|
|
|
22
19
|
|
|
23
20
|
class BaseMixin(_MixinBase):
|
|
@@ -80,7 +77,9 @@ class BaseMixin(_MixinBase):
|
|
|
80
77
|
assert_that({'a': 1, 'b': 2, 'c': 3}).is_equal_to({'a': 1}, ignore=['b', 'c'])
|
|
81
78
|
|
|
82
79
|
# ignore nested keys
|
|
83
|
-
assert_that({'a': {'b': 2, 'c': 3, 'd': 4}}).is_equal_to(
|
|
80
|
+
assert_that({'a': {'b': 2, 'c': 3, 'd': 4}}).is_equal_to(
|
|
81
|
+
{'a': {'d': 4}}, ignore=[('a', 'b'), ('a', 'c')]
|
|
82
|
+
)
|
|
84
83
|
|
|
85
84
|
When the val is *dict-like*, only certain keys can be *included* when checking equality::
|
|
86
85
|
|
|
@@ -133,8 +132,8 @@ class BaseMixin(_MixinBase):
|
|
|
133
132
|
if self._dict_not_equal(self.val, other, ignore=ignore, include=include):
|
|
134
133
|
self._dict_err(self.val, other, ignore=ignore, include=include)
|
|
135
134
|
elif ignore or include:
|
|
136
|
-
val_is_namedtuple =
|
|
137
|
-
other_is_namedtuple =
|
|
135
|
+
val_is_namedtuple = is_namedtuple(self.val)
|
|
136
|
+
other_is_namedtuple = is_namedtuple(other)
|
|
138
137
|
if (
|
|
139
138
|
isinstance(self.val, (list, tuple))
|
|
140
139
|
and isinstance(other, (list, tuple))
|
|
@@ -208,14 +207,9 @@ class BaseMixin(_MixinBase):
|
|
|
208
207
|
return sub_entries
|
|
209
208
|
return [DiffEntry(path=path, actual=actual_value, expected=expected_value)]
|
|
210
209
|
|
|
211
|
-
if (
|
|
212
|
-
hasattr(actual, "_fields")
|
|
213
|
-
and isinstance(actual, tuple)
|
|
214
|
-
and hasattr(expected, "_fields")
|
|
215
|
-
and isinstance(expected, tuple)
|
|
216
|
-
):
|
|
210
|
+
if is_namedtuple(actual) and is_namedtuple(expected):
|
|
217
211
|
entries: list[DiffEntry] = []
|
|
218
|
-
for field in actual._fields:
|
|
212
|
+
for field in actual._fields:
|
|
219
213
|
actual_value = getattr(actual, field)
|
|
220
214
|
expected_value = getattr(expected, field, _SENTINEL)
|
|
221
215
|
path = f"{_prefix}.{field}"
|
|
@@ -223,9 +217,11 @@ class BaseMixin(_MixinBase):
|
|
|
223
217
|
entries.append(DiffEntry(path=path, actual=actual_value, expected=None))
|
|
224
218
|
elif actual_value != expected_value:
|
|
225
219
|
entries.extend(_field_entries(actual_value, expected_value, path))
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
220
|
+
entries.extend(
|
|
221
|
+
DiffEntry(path=f"{_prefix}.{field}", actual=None, expected=getattr(expected, field))
|
|
222
|
+
for field in expected._fields
|
|
223
|
+
if not hasattr(actual, field)
|
|
224
|
+
)
|
|
229
225
|
return DiffResult(kind="namedtuple", entries=entries)
|
|
230
226
|
if (
|
|
231
227
|
dataclasses.is_dataclass(actual)
|
|
@@ -248,9 +244,9 @@ class BaseMixin(_MixinBase):
|
|
|
248
244
|
if actual_value != expected_value:
|
|
249
245
|
entries.extend(_field_entries(actual_value, expected_value, path))
|
|
250
246
|
return DiffResult(kind="dataclass", entries=entries)
|
|
251
|
-
if
|
|
252
|
-
actual_dict = actual.model_dump()
|
|
253
|
-
expected_dict = expected.model_dump()
|
|
247
|
+
if is_model_dump_object(actual) and is_model_dump_object(expected):
|
|
248
|
+
actual_dict = actual.model_dump()
|
|
249
|
+
expected_dict = expected.model_dump()
|
|
254
250
|
entries = []
|
|
255
251
|
for key in sorted(set(actual_dict) | set(expected_dict)):
|
|
256
252
|
path = f"{_prefix}.{key}" if _prefix else f".{key}"
|
|
@@ -354,37 +350,34 @@ class BaseMixin(_MixinBase):
|
|
|
354
350
|
DiffEntry(path=f"{prefix}.{field.name}", actual=actual_value, expected=expected_value)
|
|
355
351
|
)
|
|
356
352
|
return entries or None
|
|
357
|
-
if (
|
|
358
|
-
hasattr(actual, "_fields")
|
|
359
|
-
and isinstance(actual, tuple)
|
|
360
|
-
and hasattr(expected, "_fields")
|
|
361
|
-
and isinstance(expected, tuple)
|
|
362
|
-
):
|
|
353
|
+
if is_namedtuple(actual) and is_namedtuple(expected):
|
|
363
354
|
child_seen = _seen | {id(actual), id(expected)}
|
|
364
355
|
entries = []
|
|
365
|
-
for
|
|
366
|
-
actual_value = getattr(actual,
|
|
367
|
-
expected_value = getattr(expected,
|
|
356
|
+
for field_name in actual._fields:
|
|
357
|
+
actual_value = getattr(actual, field_name)
|
|
358
|
+
expected_value = getattr(expected, field_name, _SENTINEL)
|
|
368
359
|
if expected_value is _SENTINEL:
|
|
369
|
-
entries.append(DiffEntry(path=f"{prefix}.{
|
|
360
|
+
entries.append(DiffEntry(path=f"{prefix}.{field_name}", actual=actual_value, expected=None))
|
|
370
361
|
elif actual_value != expected_value:
|
|
371
362
|
sub_entries = BaseMixin._sub_diff_entries(
|
|
372
|
-
actual_value, expected_value, f"{prefix}.{
|
|
363
|
+
actual_value, expected_value, f"{prefix}.{field_name}", _seen=child_seen
|
|
373
364
|
)
|
|
374
365
|
if sub_entries is not None:
|
|
375
366
|
entries.extend(sub_entries)
|
|
376
367
|
else:
|
|
377
368
|
entries.append(
|
|
378
|
-
DiffEntry(path=f"{prefix}.{
|
|
369
|
+
DiffEntry(path=f"{prefix}.{field_name}", actual=actual_value, expected=expected_value)
|
|
379
370
|
)
|
|
380
|
-
for
|
|
381
|
-
if not hasattr(actual,
|
|
382
|
-
entries.append(
|
|
371
|
+
for field_name in expected._fields:
|
|
372
|
+
if not hasattr(actual, field_name):
|
|
373
|
+
entries.append(
|
|
374
|
+
DiffEntry(path=f"{prefix}.{field_name}", actual=None, expected=getattr(expected, field_name))
|
|
375
|
+
)
|
|
383
376
|
return entries or None
|
|
384
|
-
if
|
|
377
|
+
if is_model_dump_object(actual) and is_model_dump_object(expected):
|
|
385
378
|
child_seen = _seen | {id(actual), id(expected)}
|
|
386
|
-
actual_dict = actual.model_dump()
|
|
387
|
-
expected_dict = expected.model_dump()
|
|
379
|
+
actual_dict = actual.model_dump()
|
|
380
|
+
expected_dict = expected.model_dump()
|
|
388
381
|
entries = []
|
|
389
382
|
for key in sorted(set(actual_dict) | set(expected_dict)):
|
|
390
383
|
path = f"{prefix}.{key}"
|
|
@@ -494,7 +487,7 @@ class BaseMixin(_MixinBase):
|
|
|
494
487
|
raise TypeError("given arg must be a Matcher or callable")
|
|
495
488
|
return self
|
|
496
489
|
|
|
497
|
-
def matches_structure(self, spec: dict) -> Self:
|
|
490
|
+
def matches_structure(self, spec: dict[Any, Any]) -> Self:
|
|
498
491
|
"""Asserts that val is a dict matching the given structure specification.
|
|
499
492
|
|
|
500
493
|
Each key in ``spec`` maps to either a :class:`~assertpy2.matchers.Matcher`, a raw value
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
from types import MappingProxyType
|
|
4
|
+
from typing import Final
|
|
5
|
+
|
|
6
|
+
_BOOL_TRUE: Final = frozenset({"true", "yes", "1", "on"})
|
|
7
|
+
_BOOL_FALSE: Final = frozenset({"false", "no", "0", "off"})
|
|
5
8
|
|
|
6
9
|
|
|
7
10
|
def _with_pattern(pattern):
|
|
@@ -51,13 +54,15 @@ def _bool_like(text):
|
|
|
51
54
|
raise ValueError(f"expected boolean-like value, got {text!r}")
|
|
52
55
|
|
|
53
56
|
|
|
54
|
-
ASSERTPY_TYPES =
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
57
|
+
ASSERTPY_TYPES: Final = MappingProxyType(
|
|
58
|
+
{
|
|
59
|
+
"PositiveInt": _positive_int,
|
|
60
|
+
"NonNegativeInt": _non_negative_int,
|
|
61
|
+
"PositiveFloat": _positive_float,
|
|
62
|
+
"NonEmptyString": _non_empty_string,
|
|
63
|
+
"BoolLike": _bool_like,
|
|
64
|
+
}
|
|
65
|
+
)
|
|
61
66
|
|
|
62
67
|
|
|
63
68
|
def register_assertpy_types() -> None:
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import collections
|
|
4
|
-
from collections.abc import Callable, Iterable
|
|
3
|
+
import collections.abc
|
|
5
4
|
from typing import TYPE_CHECKING, Any
|
|
6
5
|
|
|
6
|
+
from ._introspection import is_mapping_like
|
|
7
7
|
from ._mixin_base import _MixinBase
|
|
8
8
|
from .matchers import BaseMatcher
|
|
9
9
|
|
|
10
10
|
if TYPE_CHECKING:
|
|
11
|
+
from collections.abc import Callable, Iterable
|
|
12
|
+
|
|
11
13
|
from ._compat import Self
|
|
12
14
|
|
|
13
15
|
__tracebackhide__ = True
|
|
@@ -91,7 +93,7 @@ class CollectionMixin(_MixinBase):
|
|
|
91
93
|
raise ValueError("one or more superset args must be given")
|
|
92
94
|
|
|
93
95
|
missing = []
|
|
94
|
-
if
|
|
96
|
+
if is_mapping_like(self.val):
|
|
95
97
|
superdict = {}
|
|
96
98
|
for superset_index, superset in enumerate(supersets):
|
|
97
99
|
self._check_dict_like(superset, check_values=False, name=f"arg #{superset_index + 1}")
|
|
@@ -114,12 +116,10 @@ class CollectionMixin(_MixinBase):
|
|
|
114
116
|
try:
|
|
115
117
|
for key in superset:
|
|
116
118
|
superset_values.add(key)
|
|
117
|
-
except TypeError:
|
|
119
|
+
except TypeError: # noqa: PERF203 # per-item fallback: a non-iterable superset is treated as a single value
|
|
118
120
|
superset_values.add(superset)
|
|
119
121
|
|
|
120
|
-
for item in self.val
|
|
121
|
-
if item not in superset_values:
|
|
122
|
-
missing.append(item)
|
|
122
|
+
missing.extend(item for item in self.val if item not in superset_values)
|
|
123
123
|
if missing:
|
|
124
124
|
return self.error(
|
|
125
125
|
f"Expected <{self.val}> to be subset of {self._fmt_items(superset_values)}, "
|
|
@@ -128,10 +128,7 @@ class ContainsMixin(_MixinBase):
|
|
|
128
128
|
if items[0] in self.val:
|
|
129
129
|
return self.error(f"Expected <{self.val}> to not contain item <{items[0]}>, but did.")
|
|
130
130
|
else:
|
|
131
|
-
found = []
|
|
132
|
-
for item in items:
|
|
133
|
-
if item in self.val:
|
|
134
|
-
found.append(item)
|
|
131
|
+
found = [item for item in items if item in self.val]
|
|
135
132
|
if found:
|
|
136
133
|
return self.error(
|
|
137
134
|
f"Expected <{self.val}> to not contain items {self._fmt_items(items)},"
|
|
@@ -165,20 +162,14 @@ class ContainsMixin(_MixinBase):
|
|
|
165
162
|
if len(items) == 0:
|
|
166
163
|
raise ValueError("one or more args must be given")
|
|
167
164
|
else:
|
|
168
|
-
extra = []
|
|
169
|
-
for item in self.val:
|
|
170
|
-
if item not in items:
|
|
171
|
-
extra.append(item)
|
|
165
|
+
extra = [item for item in self.val if item not in items]
|
|
172
166
|
if extra:
|
|
173
167
|
return self.error(
|
|
174
168
|
f"Expected <{self.val}> to contain only {self._fmt_items(items)},"
|
|
175
169
|
f" but did contain {self._fmt_items(extra)}."
|
|
176
170
|
)
|
|
177
171
|
|
|
178
|
-
missing = []
|
|
179
|
-
for item in items:
|
|
180
|
-
if item not in self.val:
|
|
181
|
-
missing.append(item)
|
|
172
|
+
missing = [item for item in items if item not in self.val]
|
|
182
173
|
if missing:
|
|
183
174
|
return self.error(
|
|
184
175
|
f"Expected <{self.val}> to contain only {self._fmt_items(items)},"
|
|
@@ -359,11 +350,14 @@ class ContainsMixin(_MixinBase):
|
|
|
359
350
|
if val_list != list(items):
|
|
360
351
|
val_counts = Counter(val_list)
|
|
361
352
|
item_counts = Counter(items)
|
|
362
|
-
entries = [
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
353
|
+
entries = [
|
|
354
|
+
DiffEntry(path="extra", actual=item, expected=None)
|
|
355
|
+
for item in sorted((val_counts - item_counts).elements(), key=repr)
|
|
356
|
+
]
|
|
357
|
+
entries.extend(
|
|
358
|
+
DiffEntry(path="missing", actual=None, expected=item)
|
|
359
|
+
for item in sorted((item_counts - val_counts).elements(), key=repr)
|
|
360
|
+
)
|
|
367
361
|
diff = DiffResult(kind="contains", entries=entries) if entries else None
|
|
368
362
|
return self.error(
|
|
369
363
|
f"Expected <{self.val}> to contain exactly {self._fmt_items(items)}, but did not.",
|
|
@@ -38,7 +38,7 @@ class DateMixin(_MixinBase):
|
|
|
38
38
|
|
|
39
39
|
See Also:
|
|
40
40
|
:meth:`~assertpy.string.NumericMixin.is_less_than` - numeric assertion, but also works with datetime BR
|
|
41
|
-
:meth:`~assertpy.string.NumericMixin.is_less_than_or_equal_to` -
|
|
41
|
+
:meth:`~assertpy.string.NumericMixin.is_less_than_or_equal_to` - also works with datetime
|
|
42
42
|
"""
|
|
43
43
|
if type(self.val) is not datetime.datetime:
|
|
44
44
|
raise TypeError(f"val must be datetime, but was type <{type(self.val).__name__}>")
|
|
@@ -75,7 +75,7 @@ class DateMixin(_MixinBase):
|
|
|
75
75
|
|
|
76
76
|
See Also:
|
|
77
77
|
:meth:`~assertpy.string.NumericMixin.is_greater_than` - numeric assertion, but also works with datetime BR
|
|
78
|
-
:meth:`~assertpy.string.NumericMixin.is_greater_than_or_equal_to` -
|
|
78
|
+
:meth:`~assertpy.string.NumericMixin.is_greater_than_or_equal_to` - also works with datetime
|
|
79
79
|
"""
|
|
80
80
|
if type(self.val) is not datetime.datetime:
|
|
81
81
|
raise TypeError(f"val must be datetime, but was type <{type(self.val).__name__}>")
|