assertpy2 2.2.0__tar.gz → 2.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {assertpy2-2.2.0 → assertpy2-2.3.0}/PKG-INFO +56 -2
- {assertpy2-2.2.0 → assertpy2-2.3.0}/README.md +51 -1
- {assertpy2-2.2.0 → assertpy2-2.3.0}/assertpy2/assertpy.py +5 -5
- {assertpy2-2.2.0 → assertpy2-2.3.0}/assertpy2/async_assertions.py +2 -2
- {assertpy2-2.2.0 → assertpy2-2.3.0}/assertpy2/base.py +19 -17
- assertpy2-2.3.0/assertpy2/behave_matchers.py +85 -0
- {assertpy2-2.2.0 → assertpy2-2.3.0}/assertpy2/collection.py +9 -19
- {assertpy2-2.2.0 → assertpy2-2.3.0}/assertpy2/contains.py +22 -27
- {assertpy2-2.2.0 → assertpy2-2.3.0}/assertpy2/date.py +20 -20
- {assertpy2-2.2.0 → assertpy2-2.3.0}/assertpy2/dict.py +8 -8
- {assertpy2-2.2.0 → assertpy2-2.3.0}/assertpy2/dynamic.py +7 -7
- {assertpy2-2.2.0 → assertpy2-2.3.0}/assertpy2/errors.py +2 -2
- {assertpy2-2.2.0 → assertpy2-2.3.0}/assertpy2/exception.py +5 -9
- {assertpy2-2.2.0 → assertpy2-2.3.0}/assertpy2/extracting.py +4 -4
- {assertpy2-2.2.0 → assertpy2-2.3.0}/assertpy2/file.py +11 -11
- {assertpy2-2.2.0 → assertpy2-2.3.0}/assertpy2/helpers.py +61 -42
- {assertpy2-2.2.0 → assertpy2-2.3.0}/assertpy2/matchers.py +40 -42
- {assertpy2-2.2.0 → assertpy2-2.3.0}/assertpy2/numeric.py +33 -41
- assertpy2-2.3.0/assertpy2/pytest_plugin.py +115 -0
- {assertpy2-2.2.0 → assertpy2-2.3.0}/assertpy2/snapshot.py +1 -1
- {assertpy2-2.2.0 → assertpy2-2.3.0}/assertpy2/string.py +17 -17
- {assertpy2-2.2.0 → assertpy2-2.3.0}/docs/api.md +152 -4
- {assertpy2-2.2.0 → assertpy2-2.3.0}/pyproject.toml +6 -2
- {assertpy2-2.2.0 → assertpy2-2.3.0}/tests/test_async.py +5 -5
- assertpy2-2.3.0/tests/test_behave_matchers.py +108 -0
- {assertpy2-2.2.0 → assertpy2-2.3.0}/tests/test_class.py +3 -3
- {assertpy2-2.2.0 → assertpy2-2.3.0}/tests/test_dyn.py +3 -3
- {assertpy2-2.2.0 → assertpy2-2.3.0}/tests/test_errors.py +29 -1
- {assertpy2-2.2.0 → assertpy2-2.3.0}/tests/test_expected_exception.py +1 -2
- {assertpy2-2.2.0 → assertpy2-2.3.0}/tests/test_extensions.py +5 -5
- {assertpy2-2.2.0 → assertpy2-2.3.0}/tests/test_extracting.py +2 -2
- {assertpy2-2.2.0 → assertpy2-2.3.0}/tests/test_numbers.py +1 -1
- assertpy2-2.3.0/tests/test_pytest_plugin.py +383 -0
- {assertpy2-2.2.0 → assertpy2-2.3.0}/tests/test_readme.py +4 -4
- {assertpy2-2.2.0 → assertpy2-2.3.0}/uv.lock +116 -1
- assertpy2-2.2.0/assertpy2/pytest_plugin.py +0 -42
- assertpy2-2.2.0/tests/test_pytest_plugin.py +0 -154
- {assertpy2-2.2.0 → assertpy2-2.3.0}/.codecov.yml +0 -0
- {assertpy2-2.2.0 → assertpy2-2.3.0}/.github/dependabot.yml +0 -0
- {assertpy2-2.2.0 → assertpy2-2.3.0}/.github/workflows/ci.yml +0 -0
- {assertpy2-2.2.0 → assertpy2-2.3.0}/.github/workflows/codeql.yml +0 -0
- {assertpy2-2.2.0 → assertpy2-2.3.0}/.github/workflows/publish.yml +0 -0
- {assertpy2-2.2.0 → assertpy2-2.3.0}/.github/workflows/scorecard.yml +0 -0
- {assertpy2-2.2.0 → assertpy2-2.3.0}/.gitignore +0 -0
- {assertpy2-2.2.0 → assertpy2-2.3.0}/CONTRIBUTING.md +0 -0
- {assertpy2-2.2.0 → assertpy2-2.3.0}/LICENSE +0 -0
- {assertpy2-2.2.0 → assertpy2-2.3.0}/SECURITY.md +0 -0
- {assertpy2-2.2.0 → assertpy2-2.3.0}/assertpy2/__init__.py +0 -0
- {assertpy2-2.2.0 → assertpy2-2.3.0}/assertpy2/_typing.py +0 -0
- {assertpy2-2.2.0 → assertpy2-2.3.0}/assertpy2/py.typed +0 -0
- {assertpy2-2.2.0 → assertpy2-2.3.0}/docs/logo-dark.svg +0 -0
- {assertpy2-2.2.0 → assertpy2-2.3.0}/docs/logo.svg +0 -0
- {assertpy2-2.2.0 → assertpy2-2.3.0}/tests/test_bool.py +0 -0
- {assertpy2-2.2.0 → assertpy2-2.3.0}/tests/test_collection.py +0 -0
- {assertpy2-2.2.0 → assertpy2-2.3.0}/tests/test_core.py +0 -0
- {assertpy2-2.2.0 → assertpy2-2.3.0}/tests/test_custom_dict.py +0 -0
- {assertpy2-2.2.0 → assertpy2-2.3.0}/tests/test_custom_list.py +0 -0
- {assertpy2-2.2.0 → assertpy2-2.3.0}/tests/test_datetime.py +0 -0
- {assertpy2-2.2.0 → assertpy2-2.3.0}/tests/test_description.py +0 -0
- {assertpy2-2.2.0 → assertpy2-2.3.0}/tests/test_dict.py +0 -0
- {assertpy2-2.2.0 → assertpy2-2.3.0}/tests/test_dict_compare.py +0 -0
- {assertpy2-2.2.0 → assertpy2-2.3.0}/tests/test_equals.py +0 -0
- {assertpy2-2.2.0 → assertpy2-2.3.0}/tests/test_fail.py +0 -0
- {assertpy2-2.2.0 → assertpy2-2.3.0}/tests/test_file.py +0 -0
- {assertpy2-2.2.0 → assertpy2-2.3.0}/tests/test_in.py +0 -0
- {assertpy2-2.2.0 → assertpy2-2.3.0}/tests/test_list.py +0 -0
- {assertpy2-2.2.0 → assertpy2-2.3.0}/tests/test_matchers.py +0 -0
- {assertpy2-2.2.0 → assertpy2-2.3.0}/tests/test_matchers_phase3.py +0 -0
- {assertpy2-2.2.0 → assertpy2-2.3.0}/tests/test_namedtuple.py +0 -0
- {assertpy2-2.2.0 → assertpy2-2.3.0}/tests/test_none.py +0 -0
- {assertpy2-2.2.0 → assertpy2-2.3.0}/tests/test_overloads.py +0 -0
- {assertpy2-2.2.0 → assertpy2-2.3.0}/tests/test_phase2.py +0 -0
- {assertpy2-2.2.0 → assertpy2-2.3.0}/tests/test_same_as.py +0 -0
- {assertpy2-2.2.0 → assertpy2-2.3.0}/tests/test_snapshots.py +0 -0
- {assertpy2-2.2.0 → assertpy2-2.3.0}/tests/test_soft.py +0 -0
- {assertpy2-2.2.0 → assertpy2-2.3.0}/tests/test_soft_fail.py +0 -0
- {assertpy2-2.2.0 → assertpy2-2.3.0}/tests/test_string.py +0 -0
- {assertpy2-2.2.0 → assertpy2-2.3.0}/tests/test_structural.py +0 -0
- {assertpy2-2.2.0 → assertpy2-2.3.0}/tests/test_traceback.py +0 -0
- {assertpy2-2.2.0 → assertpy2-2.3.0}/tests/test_type.py +0 -0
- {assertpy2-2.2.0 → assertpy2-2.3.0}/tests/test_warn.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: assertpy2
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.0
|
|
4
4
|
Summary: Fluent assertion library for Python with full type safety and soft assertions
|
|
5
5
|
Project-URL: Homepage, https://github.com/Solganis/assertpy2
|
|
6
6
|
Project-URL: Repository, https://github.com/Solganis/assertpy2
|
|
@@ -27,6 +27,10 @@ Classifier: Topic :: Software Development
|
|
|
27
27
|
Classifier: Topic :: Software Development :: Testing
|
|
28
28
|
Requires-Python: >=3.10
|
|
29
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'
|
|
30
34
|
Description-Content-Type: text/markdown
|
|
31
35
|
|
|
32
36
|
<p align="center">
|
|
@@ -126,7 +130,7 @@ FAILED test_example.py::test_comparison
|
|
|
126
130
|
| **Async assertions** | No | No | No | **eventually() with polling** |
|
|
127
131
|
| **Soft assertions** | No | No | Yes (not thread-safe) | **Yes (thread-safe, async-safe)** |
|
|
128
132
|
| **Structured errors** | Rewrite only | Mismatch string | String only | **.actual .expected .diff** |
|
|
129
|
-
| **Maintained** | N/A | Minimal | 2020 | **Active
|
|
133
|
+
| **Maintained** | N/A | Minimal | 2020 | **Active** |
|
|
130
134
|
|
|
131
135
|
</div>
|
|
132
136
|
|
|
@@ -167,6 +171,8 @@ assert_that(items).is_type_of(list).is_length(3).contains("admin")
|
|
|
167
171
|
- **Extracting**: flatten collections on attributes with `filter` and `sort` support.
|
|
168
172
|
- **File assertions**: `exists()`, `is_file()`, `is_readable()`, `is_writable()`, `is_executable()` with `pathlib.Path` support.
|
|
169
173
|
- **Snapshot testing**: store and compare data structures in JSON format, inspired by Jest.
|
|
174
|
+
- **Allure integration**: auto-attach structured diff and actual/expected data to Allure reports.
|
|
175
|
+
- **Behave step matchers**: ready-made parameter types (`PositiveInt`, `BoolLike`, etc.) for Behave step definitions.
|
|
170
176
|
- **Extensions**: add custom assertions via `add_extension()`.
|
|
171
177
|
- Strings, numbers, lists, tuples, sets, dicts, dates, booleans, objects, exceptions.
|
|
172
178
|
|
|
@@ -360,6 +366,54 @@ assert_that(5).is_5()
|
|
|
360
366
|
See the [full API reference](docs/api.md) for all assertion methods, examples, and advanced features.
|
|
361
367
|
|
|
362
368
|
|
|
369
|
+
## Allure integration
|
|
370
|
+
|
|
371
|
+
When `allure-pytest` is installed, the pytest plugin auto-attaches structured failure data to Allure reports as JSON attachments.
|
|
372
|
+
|
|
373
|
+
```bash
|
|
374
|
+
pip install assertpy2[allure]
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
Three modes controlled via `pytest.ini` (or `pyproject.toml`):
|
|
378
|
+
|
|
379
|
+
| Mode | What is attached |
|
|
380
|
+
|---|---|
|
|
381
|
+
| `diff` (default) | Structured Diff JSON (path-level breakdown) |
|
|
382
|
+
| `full` | Structured Diff + actual/expected JSON |
|
|
383
|
+
| `off` | Nothing |
|
|
384
|
+
|
|
385
|
+
```toml
|
|
386
|
+
# pyproject.toml
|
|
387
|
+
[tool.pytest.ini_options]
|
|
388
|
+
assertpy2_allure = "full"
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
## Behave step matchers
|
|
393
|
+
|
|
394
|
+
Ready-made parameter types for Behave step definitions:
|
|
395
|
+
|
|
396
|
+
```bash
|
|
397
|
+
pip install assertpy2[behave]
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
```py
|
|
401
|
+
# in environment.py or steps/conftest.py
|
|
402
|
+
from assertpy2.behave_matchers import register_assertpy_types
|
|
403
|
+
register_assertpy_types()
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
Then use in step definitions:
|
|
407
|
+
|
|
408
|
+
```py
|
|
409
|
+
@given('a user aged {age:PositiveInt}')
|
|
410
|
+
def step_impl(context, age):
|
|
411
|
+
context.age = age # already validated as int > 0
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
Available types: `PositiveInt`, `NonNegativeInt`, `PositiveFloat`, `NonEmptyString`, `BoolLike`.
|
|
415
|
+
|
|
416
|
+
|
|
363
417
|
## Migration from assertpy
|
|
364
418
|
|
|
365
419
|
assertpy2 is a drop-in replacement for Python 3.10+. Change the import, everything else works:
|
|
@@ -95,7 +95,7 @@ FAILED test_example.py::test_comparison
|
|
|
95
95
|
| **Async assertions** | No | No | No | **eventually() with polling** |
|
|
96
96
|
| **Soft assertions** | No | No | Yes (not thread-safe) | **Yes (thread-safe, async-safe)** |
|
|
97
97
|
| **Structured errors** | Rewrite only | Mismatch string | String only | **.actual .expected .diff** |
|
|
98
|
-
| **Maintained** | N/A | Minimal | 2020 | **Active
|
|
98
|
+
| **Maintained** | N/A | Minimal | 2020 | **Active** |
|
|
99
99
|
|
|
100
100
|
</div>
|
|
101
101
|
|
|
@@ -136,6 +136,8 @@ assert_that(items).is_type_of(list).is_length(3).contains("admin")
|
|
|
136
136
|
- **Extracting**: flatten collections on attributes with `filter` and `sort` support.
|
|
137
137
|
- **File assertions**: `exists()`, `is_file()`, `is_readable()`, `is_writable()`, `is_executable()` with `pathlib.Path` support.
|
|
138
138
|
- **Snapshot testing**: store and compare data structures in JSON format, inspired by Jest.
|
|
139
|
+
- **Allure integration**: auto-attach structured diff and actual/expected data to Allure reports.
|
|
140
|
+
- **Behave step matchers**: ready-made parameter types (`PositiveInt`, `BoolLike`, etc.) for Behave step definitions.
|
|
139
141
|
- **Extensions**: add custom assertions via `add_extension()`.
|
|
140
142
|
- Strings, numbers, lists, tuples, sets, dicts, dates, booleans, objects, exceptions.
|
|
141
143
|
|
|
@@ -329,6 +331,54 @@ assert_that(5).is_5()
|
|
|
329
331
|
See the [full API reference](docs/api.md) for all assertion methods, examples, and advanced features.
|
|
330
332
|
|
|
331
333
|
|
|
334
|
+
## Allure integration
|
|
335
|
+
|
|
336
|
+
When `allure-pytest` is installed, the pytest plugin auto-attaches structured failure data to Allure reports as JSON attachments.
|
|
337
|
+
|
|
338
|
+
```bash
|
|
339
|
+
pip install assertpy2[allure]
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
Three modes controlled via `pytest.ini` (or `pyproject.toml`):
|
|
343
|
+
|
|
344
|
+
| Mode | What is attached |
|
|
345
|
+
|---|---|
|
|
346
|
+
| `diff` (default) | Structured Diff JSON (path-level breakdown) |
|
|
347
|
+
| `full` | Structured Diff + actual/expected JSON |
|
|
348
|
+
| `off` | Nothing |
|
|
349
|
+
|
|
350
|
+
```toml
|
|
351
|
+
# pyproject.toml
|
|
352
|
+
[tool.pytest.ini_options]
|
|
353
|
+
assertpy2_allure = "full"
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
## Behave step matchers
|
|
358
|
+
|
|
359
|
+
Ready-made parameter types for Behave step definitions:
|
|
360
|
+
|
|
361
|
+
```bash
|
|
362
|
+
pip install assertpy2[behave]
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
```py
|
|
366
|
+
# in environment.py or steps/conftest.py
|
|
367
|
+
from assertpy2.behave_matchers import register_assertpy_types
|
|
368
|
+
register_assertpy_types()
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
Then use in step definitions:
|
|
372
|
+
|
|
373
|
+
```py
|
|
374
|
+
@given('a user aged {age:PositiveInt}')
|
|
375
|
+
def step_impl(context, age):
|
|
376
|
+
context.age = age # already validated as int > 0
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
Available types: `PositiveInt`, `NonNegativeInt`, `PositiveFloat`, `NonEmptyString`, `BoolLike`.
|
|
380
|
+
|
|
381
|
+
|
|
332
382
|
## Migration from assertpy
|
|
333
383
|
|
|
334
384
|
assertpy2 is a drop-in replacement for Python 3.10+. Change the import, everything else works:
|
|
@@ -160,7 +160,7 @@ def soft_assertions() -> Iterator[None]:
|
|
|
160
160
|
|
|
161
161
|
errs = _soft_err.get([])
|
|
162
162
|
if errs and _soft_ctx.get() == 0:
|
|
163
|
-
out = "soft assertion failures:\n" + "\n".join("
|
|
163
|
+
out = "soft assertion failures:\n" + "\n".join(f"{i + 1}. {msg}" for i, msg in enumerate(errs))
|
|
164
164
|
_soft_err.set([])
|
|
165
165
|
raise AssertionError(out)
|
|
166
166
|
|
|
@@ -300,7 +300,7 @@ def fail(msg=""):
|
|
|
300
300
|
except TypeError as e:
|
|
301
301
|
assert_that(str(e)).contains('unsupported operand')
|
|
302
302
|
"""
|
|
303
|
-
raise AssertionError("Fail:
|
|
303
|
+
raise AssertionError(f"Fail: {msg}!" if msg else "Fail!")
|
|
304
304
|
|
|
305
305
|
|
|
306
306
|
def soft_fail(msg=""):
|
|
@@ -332,7 +332,7 @@ def soft_fail(msg=""):
|
|
|
332
332
|
|
|
333
333
|
"""
|
|
334
334
|
if _soft_ctx.get():
|
|
335
|
-
_soft_err.get().append("Fail:
|
|
335
|
+
_soft_err.get().append(f"Fail: {msg}!" if msg else "Fail!")
|
|
336
336
|
return
|
|
337
337
|
fail(msg)
|
|
338
338
|
|
|
@@ -427,7 +427,7 @@ class WarningLoggingAdapter(logging.LoggerAdapter):
|
|
|
427
427
|
prev = frame
|
|
428
428
|
|
|
429
429
|
filename, lineno = _unwind(inspect.currentframe())
|
|
430
|
-
return "[
|
|
430
|
+
return f"[{os.path.basename(filename)}:{lineno}]: {msg}", kwargs
|
|
431
431
|
|
|
432
432
|
|
|
433
433
|
_logger = logging.getLogger("assertpy2")
|
|
@@ -512,7 +512,7 @@ class AssertionBuilder(
|
|
|
512
512
|
AssertionBuilder: returns this instance to chain to the next assertion, but only when
|
|
513
513
|
``AssertionError`` is not raised, as is the case when ``kind`` is ``warn`` or ``soft``.
|
|
514
514
|
"""
|
|
515
|
-
out = "
|
|
515
|
+
out = f"{f'[{self.description}] ' if len(self.description) > 0 else ''}{msg}"
|
|
516
516
|
if self.kind == "warn":
|
|
517
517
|
self.logger.warning(out)
|
|
518
518
|
return self
|
|
@@ -73,8 +73,8 @@ class AsyncAssertionBuilder:
|
|
|
73
73
|
last_error = exc
|
|
74
74
|
if loop.time() >= deadline:
|
|
75
75
|
raise AssertionError(
|
|
76
|
-
"Expected condition not met after
|
|
77
|
-
|
|
76
|
+
f"Expected condition not met after {self._timeout:.1f} seconds."
|
|
77
|
+
f" Last failure: {last_error}"
|
|
78
78
|
) from last_error
|
|
79
79
|
await asyncio.sleep(self._interval)
|
|
80
80
|
|
|
@@ -31,6 +31,7 @@ from __future__ import annotations
|
|
|
31
31
|
import collections.abc
|
|
32
32
|
from typing import TYPE_CHECKING
|
|
33
33
|
|
|
34
|
+
from .errors import DiffEntry, DiffResult
|
|
34
35
|
from .matchers import Matcher, StructureMatcher
|
|
35
36
|
|
|
36
37
|
if TYPE_CHECKING:
|
|
@@ -135,9 +136,10 @@ class BaseMixin:
|
|
|
135
136
|
else:
|
|
136
137
|
if self.val != other:
|
|
137
138
|
return self.error(
|
|
138
|
-
"Expected
|
|
139
|
+
f"Expected <{self.val}> to be equal to <{other}>, but was not.",
|
|
139
140
|
actual=self.val,
|
|
140
141
|
expected=other,
|
|
142
|
+
diff=DiffResult(kind="scalar", entries=[DiffEntry(path=".", actual=self.val, expected=other)]),
|
|
141
143
|
)
|
|
142
144
|
return self
|
|
143
145
|
|
|
@@ -168,10 +170,10 @@ class BaseMixin:
|
|
|
168
170
|
"""
|
|
169
171
|
if isinstance(matcher, Matcher):
|
|
170
172
|
if not matcher.matches(self.val):
|
|
171
|
-
return self.error("Expected
|
|
173
|
+
return self.error(f"Expected {matcher.describe()}, but {matcher.describe_mismatch(self.val)}.")
|
|
172
174
|
elif callable(matcher):
|
|
173
175
|
if not matcher(self.val):
|
|
174
|
-
return self.error("Expected
|
|
176
|
+
return self.error(f"Expected <{self.val}> to satisfy <{matcher}>, but did not.")
|
|
175
177
|
else:
|
|
176
178
|
raise TypeError("given arg must be a Matcher or callable")
|
|
177
179
|
return self
|
|
@@ -207,14 +209,14 @@ class BaseMixin:
|
|
|
207
209
|
for i, item in enumerate(self.val):
|
|
208
210
|
if not matcher.matches(item):
|
|
209
211
|
return self.error(
|
|
210
|
-
"Expected all items to satisfy
|
|
211
|
-
|
|
212
|
+
f"Expected all items to satisfy {matcher.describe()}, but item at index {i} <{item}> did not:"
|
|
213
|
+
f" {matcher.describe_mismatch(item)}."
|
|
212
214
|
)
|
|
213
215
|
elif callable(matcher):
|
|
214
216
|
for i, item in enumerate(self.val):
|
|
215
217
|
if not matcher(item):
|
|
216
218
|
return self.error(
|
|
217
|
-
"Expected all items to satisfy
|
|
219
|
+
f"Expected all items to satisfy <{matcher}>, but item at index {i} <{item}> did not."
|
|
218
220
|
)
|
|
219
221
|
else:
|
|
220
222
|
raise TypeError("given arg must be a Matcher or callable")
|
|
@@ -255,8 +257,8 @@ class BaseMixin:
|
|
|
255
257
|
matcher = StructureMatcher(spec)
|
|
256
258
|
if not matcher.matches(self.val):
|
|
257
259
|
return self.error(
|
|
258
|
-
"Expected
|
|
259
|
-
|
|
260
|
+
f"Expected <{self.val}> to match structure {matcher.describe()}, but"
|
|
261
|
+
f" {matcher.describe_mismatch(self.val)}."
|
|
260
262
|
)
|
|
261
263
|
return self
|
|
262
264
|
|
|
@@ -425,7 +427,7 @@ class BaseMixin:
|
|
|
425
427
|
AssertionError: if actual **is** equal to expected
|
|
426
428
|
"""
|
|
427
429
|
if self.val == other:
|
|
428
|
-
return self.error("Expected
|
|
430
|
+
return self.error(f"Expected <{self.val}> to be not equal to <{other}>, but was.")
|
|
429
431
|
return self
|
|
430
432
|
|
|
431
433
|
def is_same_as(self, other) -> Self:
|
|
@@ -467,7 +469,7 @@ class BaseMixin:
|
|
|
467
469
|
AssertionError: if actual is **not** identical to expected
|
|
468
470
|
"""
|
|
469
471
|
if self.val is not other:
|
|
470
|
-
return self.error("Expected
|
|
472
|
+
return self.error(f"Expected <{self.val}> to be identical to <{other}>, but was not.")
|
|
471
473
|
return self
|
|
472
474
|
|
|
473
475
|
def is_not_same_as(self, other) -> Self:
|
|
@@ -498,7 +500,7 @@ class BaseMixin:
|
|
|
498
500
|
AssertionError: if actual **is** identical to expected
|
|
499
501
|
"""
|
|
500
502
|
if self.val is other:
|
|
501
|
-
return self.error("Expected
|
|
503
|
+
return self.error(f"Expected <{self.val}> to be not identical to <{other}>, but was.")
|
|
502
504
|
return self
|
|
503
505
|
|
|
504
506
|
def is_true(self) -> Self:
|
|
@@ -523,7 +525,7 @@ class BaseMixin:
|
|
|
523
525
|
AssertionError: if val **is** false
|
|
524
526
|
"""
|
|
525
527
|
if not self.val:
|
|
526
|
-
return self.error("Expected
|
|
528
|
+
return self.error(f"Expected <{self.val}> to be <True>, but was not.")
|
|
527
529
|
return self
|
|
528
530
|
|
|
529
531
|
def is_false(self) -> Self:
|
|
@@ -548,7 +550,7 @@ class BaseMixin:
|
|
|
548
550
|
AssertionError: if val **is** true
|
|
549
551
|
"""
|
|
550
552
|
if self.val:
|
|
551
|
-
return self.error("Expected
|
|
553
|
+
return self.error(f"Expected <{self.val}> to be <False>, but was not.")
|
|
552
554
|
return self
|
|
553
555
|
|
|
554
556
|
def is_none(self) -> Self:
|
|
@@ -567,7 +569,7 @@ class BaseMixin:
|
|
|
567
569
|
AssertionError: if val is **not** none
|
|
568
570
|
"""
|
|
569
571
|
if self.val is not None:
|
|
570
|
-
return self.error("Expected
|
|
572
|
+
return self.error(f"Expected <{self.val}> to be <None>, but was not.")
|
|
571
573
|
return self
|
|
572
574
|
|
|
573
575
|
def is_not_none(self) -> Self:
|
|
@@ -623,7 +625,7 @@ class BaseMixin:
|
|
|
623
625
|
raise TypeError("given arg must be a type")
|
|
624
626
|
if type(self.val) is not some_type:
|
|
625
627
|
t = self._type(self.val)
|
|
626
|
-
return self.error("Expected
|
|
628
|
+
return self.error(f"Expected <{self.val}:{t}> to be of type <{some_type.__name__}>, but was not.")
|
|
627
629
|
return self
|
|
628
630
|
|
|
629
631
|
def is_instance_of(self, some_class) -> Self:
|
|
@@ -661,7 +663,7 @@ class BaseMixin:
|
|
|
661
663
|
if not isinstance(self.val, some_class):
|
|
662
664
|
t = self._type(self.val)
|
|
663
665
|
return self.error(
|
|
664
|
-
"Expected
|
|
666
|
+
f"Expected <{self.val}:{t}> to be instance of class <{some_class.__name__}>, but was not."
|
|
665
667
|
)
|
|
666
668
|
except TypeError:
|
|
667
669
|
raise TypeError("given arg must be a class") from None
|
|
@@ -695,5 +697,5 @@ class BaseMixin:
|
|
|
695
697
|
if length < 0:
|
|
696
698
|
raise ValueError("given arg must be a positive int")
|
|
697
699
|
if len(self.val) != length:
|
|
698
|
-
return self.error("Expected
|
|
700
|
+
return self.error(f"Expected <{self.val}> to be of length <{length}>, but was <{len(self.val)}>.")
|
|
699
701
|
return self
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
_BOOL_TRUE = frozenset({"true", "yes", "1", "on"})
|
|
4
|
+
_BOOL_FALSE = frozenset({"false", "no", "0", "off"})
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _with_pattern(pattern):
|
|
8
|
+
def decorator(func):
|
|
9
|
+
func.pattern = pattern
|
|
10
|
+
return func
|
|
11
|
+
|
|
12
|
+
return decorator
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@_with_pattern(r"\d+")
|
|
16
|
+
def _positive_int(text):
|
|
17
|
+
value = int(text)
|
|
18
|
+
if value <= 0:
|
|
19
|
+
raise ValueError(f"expected positive integer, got {value}")
|
|
20
|
+
return value
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@_with_pattern(r"\d+")
|
|
24
|
+
def _non_negative_int(text):
|
|
25
|
+
return int(text)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@_with_pattern(r"\d+\.?\d*")
|
|
29
|
+
def _positive_float(text):
|
|
30
|
+
value = float(text)
|
|
31
|
+
if value <= 0:
|
|
32
|
+
raise ValueError(f"expected positive float, got {value}")
|
|
33
|
+
return value
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@_with_pattern(r".+?")
|
|
37
|
+
def _non_empty_string(text):
|
|
38
|
+
stripped = text.strip()
|
|
39
|
+
if not stripped:
|
|
40
|
+
raise ValueError("expected non-empty string, got blank")
|
|
41
|
+
return stripped
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@_with_pattern(r"\w+")
|
|
45
|
+
def _bool_like(text):
|
|
46
|
+
lower = text.strip().lower()
|
|
47
|
+
if lower in _BOOL_TRUE:
|
|
48
|
+
return True
|
|
49
|
+
if lower in _BOOL_FALSE:
|
|
50
|
+
return False
|
|
51
|
+
raise ValueError(f"expected boolean-like value, got {text!r}")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
ASSERTPY_TYPES = {
|
|
55
|
+
"PositiveInt": _positive_int,
|
|
56
|
+
"NonNegativeInt": _non_negative_int,
|
|
57
|
+
"PositiveFloat": _positive_float,
|
|
58
|
+
"NonEmptyString": _non_empty_string,
|
|
59
|
+
"BoolLike": _bool_like,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def register_assertpy_types() -> None:
|
|
64
|
+
"""Register assertpy2 parameter types for Behave step definitions.
|
|
65
|
+
|
|
66
|
+
Registers the following types for use in step patterns:
|
|
67
|
+
|
|
68
|
+
- ``{param:PositiveInt}`` - positive integer (> 0)
|
|
69
|
+
- ``{param:NonNegativeInt}`` - non-negative integer (>= 0)
|
|
70
|
+
- ``{param:PositiveFloat}`` - positive float (> 0)
|
|
71
|
+
- ``{param:NonEmptyString}`` - non-empty, stripped string
|
|
72
|
+
- ``{param:BoolLike}`` - boolean from true/false/yes/no/1/0/on/off
|
|
73
|
+
|
|
74
|
+
Requires ``behave`` to be installed (``pip install assertpy2[behave]``).
|
|
75
|
+
|
|
76
|
+
Raises:
|
|
77
|
+
ImportError: if behave is not installed
|
|
78
|
+
"""
|
|
79
|
+
try:
|
|
80
|
+
from behave import register_type
|
|
81
|
+
except ImportError:
|
|
82
|
+
raise ImportError(
|
|
83
|
+
"behave is required for register_assertpy_types(). Install it with: pip install assertpy2[behave]"
|
|
84
|
+
) from None
|
|
85
|
+
register_type(**ASSERTPY_TYPES)
|
|
@@ -119,7 +119,7 @@ class CollectionMixin:
|
|
|
119
119
|
# flatten superset dicts
|
|
120
120
|
superdict = {}
|
|
121
121
|
for idx, j in enumerate(supersets):
|
|
122
|
-
self._check_dict_like(j, check_values=False, name="arg
|
|
122
|
+
self._check_dict_like(j, check_values=False, name=f"arg #{idx + 1}")
|
|
123
123
|
for k in j:
|
|
124
124
|
superdict.update({k: j[k]})
|
|
125
125
|
|
|
@@ -130,13 +130,8 @@ class CollectionMixin:
|
|
|
130
130
|
missing.append({i: self.val[i]}) # bad val
|
|
131
131
|
if missing:
|
|
132
132
|
return self.error(
|
|
133
|
-
"Expected
|
|
134
|
-
|
|
135
|
-
self.val,
|
|
136
|
-
self._fmt_items(superdict),
|
|
137
|
-
self._fmt_items(missing),
|
|
138
|
-
"was" if len(missing) == 1 else "were",
|
|
139
|
-
)
|
|
133
|
+
f"Expected <{self.val}> to be subset of {self._fmt_items(superdict)}, "
|
|
134
|
+
f"but {self._fmt_items(missing)} {'was' if len(missing) == 1 else 'were'} missing."
|
|
140
135
|
)
|
|
141
136
|
else:
|
|
142
137
|
# flatten supersets
|
|
@@ -153,13 +148,8 @@ class CollectionMixin:
|
|
|
153
148
|
missing.append(i)
|
|
154
149
|
if missing:
|
|
155
150
|
return self.error(
|
|
156
|
-
"Expected
|
|
157
|
-
|
|
158
|
-
self.val,
|
|
159
|
-
self._fmt_items(superset),
|
|
160
|
-
self._fmt_items(missing),
|
|
161
|
-
"was" if len(missing) == 1 else "were",
|
|
162
|
-
)
|
|
151
|
+
f"Expected <{self.val}> to be subset of {self._fmt_items(superset)}, "
|
|
152
|
+
f"but {self._fmt_items(missing)} {'was' if len(missing) == 1 else 'were'} missing."
|
|
163
153
|
)
|
|
164
154
|
|
|
165
155
|
return self
|
|
@@ -203,14 +193,14 @@ class CollectionMixin:
|
|
|
203
193
|
if reverse:
|
|
204
194
|
if key(x) > key(prev):
|
|
205
195
|
return self.error(
|
|
206
|
-
"Expected
|
|
207
|
-
|
|
196
|
+
f"Expected <{self.val}> to be sorted reverse, "
|
|
197
|
+
f"but subset {self._fmt_items([prev, x])} at index {i - 1} is not."
|
|
208
198
|
)
|
|
209
199
|
else:
|
|
210
200
|
if key(x) < key(prev):
|
|
211
201
|
return self.error(
|
|
212
|
-
"Expected
|
|
213
|
-
|
|
202
|
+
f"Expected <{self.val}> to be sorted, "
|
|
203
|
+
f"but subset {self._fmt_items([prev, x])} at index {i - 1} is not."
|
|
214
204
|
)
|
|
215
205
|
prev = x
|
|
216
206
|
|
|
@@ -80,13 +80,13 @@ class ContainsMixin:
|
|
|
80
80
|
if isinstance(items[0], Matcher):
|
|
81
81
|
if not any(items[0].matches(v) for v in self.val):
|
|
82
82
|
return self.error(
|
|
83
|
-
"Expected
|
|
83
|
+
f"Expected <{self.val}> to contain item matching {items[0].describe()}, but did not."
|
|
84
84
|
)
|
|
85
85
|
elif items[0] not in self.val:
|
|
86
86
|
if self._check_dict_like(self.val, return_as_bool=True):
|
|
87
|
-
return self.error("Expected
|
|
87
|
+
return self.error(f"Expected <{self.val}> to contain key <{items[0]}>, but did not.")
|
|
88
88
|
else:
|
|
89
|
-
return self.error("Expected
|
|
89
|
+
return self.error(f"Expected <{self.val}> to contain item <{items[0]}>, but did not.")
|
|
90
90
|
else:
|
|
91
91
|
missing = []
|
|
92
92
|
for i in items:
|
|
@@ -99,18 +99,13 @@ class ContainsMixin:
|
|
|
99
99
|
missing_desc = [m.describe() if isinstance(m, Matcher) else m for m in missing]
|
|
100
100
|
if self._check_dict_like(self.val, return_as_bool=True):
|
|
101
101
|
return self.error(
|
|
102
|
-
"Expected
|
|
103
|
-
|
|
104
|
-
self.val,
|
|
105
|
-
self._fmt_items(items),
|
|
106
|
-
"" if len(missing) == 0 else "s",
|
|
107
|
-
self._fmt_items(missing_desc),
|
|
108
|
-
)
|
|
102
|
+
f"Expected <{self.val}> to contain keys {self._fmt_items(items)}, but did not contain"
|
|
103
|
+
f" key{'' if len(missing) == 0 else 's'} {self._fmt_items(missing_desc)}."
|
|
109
104
|
)
|
|
110
105
|
else:
|
|
111
106
|
return self.error(
|
|
112
|
-
"Expected
|
|
113
|
-
|
|
107
|
+
f"Expected <{self.val}> to contain items {self._fmt_items(items)},"
|
|
108
|
+
f" but did not contain {self._fmt_items(missing_desc)}."
|
|
114
109
|
)
|
|
115
110
|
return self
|
|
116
111
|
|
|
@@ -146,7 +141,7 @@ class ContainsMixin:
|
|
|
146
141
|
raise ValueError("one or more args must be given")
|
|
147
142
|
elif len(items) == 1:
|
|
148
143
|
if items[0] in self.val:
|
|
149
|
-
return self.error("Expected
|
|
144
|
+
return self.error(f"Expected <{self.val}> to not contain item <{items[0]}>, but did.")
|
|
150
145
|
else:
|
|
151
146
|
found = []
|
|
152
147
|
for i in items:
|
|
@@ -154,8 +149,8 @@ class ContainsMixin:
|
|
|
154
149
|
found.append(i)
|
|
155
150
|
if found:
|
|
156
151
|
return self.error(
|
|
157
|
-
"Expected
|
|
158
|
-
|
|
152
|
+
f"Expected <{self.val}> to not contain items {self._fmt_items(items)},"
|
|
153
|
+
f" but did contain {self._fmt_items(found)}."
|
|
159
154
|
)
|
|
160
155
|
return self
|
|
161
156
|
|
|
@@ -191,8 +186,8 @@ class ContainsMixin:
|
|
|
191
186
|
extra.append(i)
|
|
192
187
|
if extra:
|
|
193
188
|
return self.error(
|
|
194
|
-
"Expected
|
|
195
|
-
|
|
189
|
+
f"Expected <{self.val}> to contain only {self._fmt_items(items)},"
|
|
190
|
+
f" but did contain {self._fmt_items(extra)}."
|
|
196
191
|
)
|
|
197
192
|
|
|
198
193
|
missing = []
|
|
@@ -201,8 +196,8 @@ class ContainsMixin:
|
|
|
201
196
|
missing.append(i)
|
|
202
197
|
if missing:
|
|
203
198
|
return self.error(
|
|
204
|
-
"Expected
|
|
205
|
-
|
|
199
|
+
f"Expected <{self.val}> to contain only {self._fmt_items(items)},"
|
|
200
|
+
f" but did not contain {self._fmt_items(missing)}."
|
|
206
201
|
)
|
|
207
202
|
return self
|
|
208
203
|
|
|
@@ -238,7 +233,7 @@ class ContainsMixin:
|
|
|
238
233
|
idx = self.val.find(item, pos)
|
|
239
234
|
if idx == -1:
|
|
240
235
|
return self.error(
|
|
241
|
-
"Expected
|
|
236
|
+
f"Expected <{self.val}> to contain sequence {self._fmt_items(items)}, but did not."
|
|
242
237
|
)
|
|
243
238
|
pos = idx + len(item)
|
|
244
239
|
return self
|
|
@@ -251,7 +246,7 @@ class ContainsMixin:
|
|
|
251
246
|
return self
|
|
252
247
|
except TypeError:
|
|
253
248
|
raise TypeError("val is not iterable") from None
|
|
254
|
-
return self.error("Expected
|
|
249
|
+
return self.error(f"Expected <{self.val}> to contain sequence {self._fmt_items(items)}, but did not.")
|
|
255
250
|
|
|
256
251
|
def contains_duplicates(self) -> Self:
|
|
257
252
|
"""Asserts that val is iterable and *does* contain duplicates.
|
|
@@ -274,7 +269,7 @@ class ContainsMixin:
|
|
|
274
269
|
return self
|
|
275
270
|
except TypeError:
|
|
276
271
|
raise TypeError("val is not iterable") from None
|
|
277
|
-
return self.error("Expected
|
|
272
|
+
return self.error(f"Expected <{self.val}> to contain duplicates, but did not.")
|
|
278
273
|
|
|
279
274
|
def does_not_contain_duplicates(self) -> Self:
|
|
280
275
|
"""Asserts that val is iterable and *does not* contain any duplicates.
|
|
@@ -297,7 +292,7 @@ class ContainsMixin:
|
|
|
297
292
|
return self
|
|
298
293
|
except TypeError:
|
|
299
294
|
raise TypeError("val is not iterable") from None
|
|
300
|
-
return self.error("Expected
|
|
295
|
+
return self.error(f"Expected <{self.val}> to not contain duplicates, but did.")
|
|
301
296
|
|
|
302
297
|
def is_empty(self) -> Self:
|
|
303
298
|
"""Asserts that val is empty.
|
|
@@ -319,9 +314,9 @@ class ContainsMixin:
|
|
|
319
314
|
"""
|
|
320
315
|
if len(self.val) != 0:
|
|
321
316
|
if isinstance(self.val, str):
|
|
322
|
-
return self.error("Expected
|
|
317
|
+
return self.error(f"Expected <{self.val}> to be empty string, but was not.")
|
|
323
318
|
else:
|
|
324
|
-
return self.error("Expected
|
|
319
|
+
return self.error(f"Expected <{self.val}> to be empty, but was not.")
|
|
325
320
|
return self
|
|
326
321
|
|
|
327
322
|
def is_not_empty(self) -> Self:
|
|
@@ -439,7 +434,7 @@ class ContainsMixin:
|
|
|
439
434
|
for i in items:
|
|
440
435
|
if self.val == i:
|
|
441
436
|
return self
|
|
442
|
-
return self.error("Expected
|
|
437
|
+
return self.error(f"Expected <{self.val}> to be in {self._fmt_items(items)}, but was not.")
|
|
443
438
|
|
|
444
439
|
def is_not_in(self, *items) -> Self:
|
|
445
440
|
"""Asserts that val is not equal to one of the given items.
|
|
@@ -464,5 +459,5 @@ class ContainsMixin:
|
|
|
464
459
|
else:
|
|
465
460
|
for i in items:
|
|
466
461
|
if self.val == i:
|
|
467
|
-
return self.error("Expected
|
|
462
|
+
return self.error(f"Expected <{self.val}> to not be in {self._fmt_items(items)}, but was.")
|
|
468
463
|
return self
|