assertpy2 2.2.0__tar.gz → 2.3.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.2.0 → assertpy2-2.3.1}/PKG-INFO +56 -2
- {assertpy2-2.2.0 → assertpy2-2.3.1}/README.md +51 -1
- {assertpy2-2.2.0 → assertpy2-2.3.1}/assertpy2/_typing.py +13 -0
- {assertpy2-2.2.0 → assertpy2-2.3.1}/assertpy2/assertpy.py +6 -6
- {assertpy2-2.2.0 → assertpy2-2.3.1}/assertpy2/async_assertions.py +2 -2
- {assertpy2-2.2.0 → assertpy2-2.3.1}/assertpy2/base.py +19 -17
- assertpy2-2.3.1/assertpy2/behave_matchers.py +85 -0
- {assertpy2-2.2.0 → assertpy2-2.3.1}/assertpy2/collection.py +9 -19
- {assertpy2-2.2.0 → assertpy2-2.3.1}/assertpy2/contains.py +22 -27
- {assertpy2-2.2.0 → assertpy2-2.3.1}/assertpy2/date.py +20 -20
- {assertpy2-2.2.0 → assertpy2-2.3.1}/assertpy2/dict.py +8 -8
- {assertpy2-2.2.0 → assertpy2-2.3.1}/assertpy2/dynamic.py +7 -7
- {assertpy2-2.2.0 → assertpy2-2.3.1}/assertpy2/errors.py +2 -2
- {assertpy2-2.2.0 → assertpy2-2.3.1}/assertpy2/exception.py +5 -9
- {assertpy2-2.2.0 → assertpy2-2.3.1}/assertpy2/extracting.py +4 -4
- {assertpy2-2.2.0 → assertpy2-2.3.1}/assertpy2/file.py +11 -11
- {assertpy2-2.2.0 → assertpy2-2.3.1}/assertpy2/helpers.py +61 -42
- {assertpy2-2.2.0 → assertpy2-2.3.1}/assertpy2/matchers.py +40 -42
- {assertpy2-2.2.0 → assertpy2-2.3.1}/assertpy2/numeric.py +33 -41
- assertpy2-2.3.1/assertpy2/pytest_plugin.py +115 -0
- {assertpy2-2.2.0 → assertpy2-2.3.1}/assertpy2/snapshot.py +1 -1
- {assertpy2-2.2.0 → assertpy2-2.3.1}/assertpy2/string.py +17 -17
- {assertpy2-2.2.0 → assertpy2-2.3.1}/docs/api.md +152 -4
- {assertpy2-2.2.0 → assertpy2-2.3.1}/pyproject.toml +6 -2
- {assertpy2-2.2.0 → assertpy2-2.3.1}/tests/test_async.py +5 -5
- assertpy2-2.3.1/tests/test_behave_matchers.py +108 -0
- {assertpy2-2.2.0 → assertpy2-2.3.1}/tests/test_class.py +3 -3
- {assertpy2-2.2.0 → assertpy2-2.3.1}/tests/test_dyn.py +3 -3
- {assertpy2-2.2.0 → assertpy2-2.3.1}/tests/test_errors.py +29 -1
- {assertpy2-2.2.0 → assertpy2-2.3.1}/tests/test_expected_exception.py +1 -2
- {assertpy2-2.2.0 → assertpy2-2.3.1}/tests/test_extensions.py +5 -5
- {assertpy2-2.2.0 → assertpy2-2.3.1}/tests/test_extracting.py +2 -2
- {assertpy2-2.2.0 → assertpy2-2.3.1}/tests/test_numbers.py +1 -1
- assertpy2-2.3.1/tests/test_pytest_plugin.py +383 -0
- {assertpy2-2.2.0 → assertpy2-2.3.1}/tests/test_readme.py +4 -4
- {assertpy2-2.2.0 → assertpy2-2.3.1}/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.1}/.codecov.yml +0 -0
- {assertpy2-2.2.0 → assertpy2-2.3.1}/.github/dependabot.yml +0 -0
- {assertpy2-2.2.0 → assertpy2-2.3.1}/.github/workflows/ci.yml +0 -0
- {assertpy2-2.2.0 → assertpy2-2.3.1}/.github/workflows/codeql.yml +0 -0
- {assertpy2-2.2.0 → assertpy2-2.3.1}/.github/workflows/publish.yml +0 -0
- {assertpy2-2.2.0 → assertpy2-2.3.1}/.github/workflows/scorecard.yml +0 -0
- {assertpy2-2.2.0 → assertpy2-2.3.1}/.gitignore +0 -0
- {assertpy2-2.2.0 → assertpy2-2.3.1}/CONTRIBUTING.md +0 -0
- {assertpy2-2.2.0 → assertpy2-2.3.1}/LICENSE +0 -0
- {assertpy2-2.2.0 → assertpy2-2.3.1}/SECURITY.md +0 -0
- {assertpy2-2.2.0 → assertpy2-2.3.1}/assertpy2/__init__.py +0 -0
- {assertpy2-2.2.0 → assertpy2-2.3.1}/assertpy2/py.typed +0 -0
- {assertpy2-2.2.0 → assertpy2-2.3.1}/docs/logo-dark.svg +0 -0
- {assertpy2-2.2.0 → assertpy2-2.3.1}/docs/logo.svg +0 -0
- {assertpy2-2.2.0 → assertpy2-2.3.1}/tests/test_bool.py +0 -0
- {assertpy2-2.2.0 → assertpy2-2.3.1}/tests/test_collection.py +0 -0
- {assertpy2-2.2.0 → assertpy2-2.3.1}/tests/test_core.py +0 -0
- {assertpy2-2.2.0 → assertpy2-2.3.1}/tests/test_custom_dict.py +0 -0
- {assertpy2-2.2.0 → assertpy2-2.3.1}/tests/test_custom_list.py +0 -0
- {assertpy2-2.2.0 → assertpy2-2.3.1}/tests/test_datetime.py +0 -0
- {assertpy2-2.2.0 → assertpy2-2.3.1}/tests/test_description.py +0 -0
- {assertpy2-2.2.0 → assertpy2-2.3.1}/tests/test_dict.py +0 -0
- {assertpy2-2.2.0 → assertpy2-2.3.1}/tests/test_dict_compare.py +0 -0
- {assertpy2-2.2.0 → assertpy2-2.3.1}/tests/test_equals.py +0 -0
- {assertpy2-2.2.0 → assertpy2-2.3.1}/tests/test_fail.py +0 -0
- {assertpy2-2.2.0 → assertpy2-2.3.1}/tests/test_file.py +0 -0
- {assertpy2-2.2.0 → assertpy2-2.3.1}/tests/test_in.py +0 -0
- {assertpy2-2.2.0 → assertpy2-2.3.1}/tests/test_list.py +0 -0
- {assertpy2-2.2.0 → assertpy2-2.3.1}/tests/test_matchers.py +0 -0
- {assertpy2-2.2.0 → assertpy2-2.3.1}/tests/test_matchers_phase3.py +0 -0
- {assertpy2-2.2.0 → assertpy2-2.3.1}/tests/test_namedtuple.py +0 -0
- {assertpy2-2.2.0 → assertpy2-2.3.1}/tests/test_none.py +0 -0
- {assertpy2-2.2.0 → assertpy2-2.3.1}/tests/test_overloads.py +0 -0
- {assertpy2-2.2.0 → assertpy2-2.3.1}/tests/test_phase2.py +0 -0
- {assertpy2-2.2.0 → assertpy2-2.3.1}/tests/test_same_as.py +0 -0
- {assertpy2-2.2.0 → assertpy2-2.3.1}/tests/test_snapshots.py +0 -0
- {assertpy2-2.2.0 → assertpy2-2.3.1}/tests/test_soft.py +0 -0
- {assertpy2-2.2.0 → assertpy2-2.3.1}/tests/test_soft_fail.py +0 -0
- {assertpy2-2.2.0 → assertpy2-2.3.1}/tests/test_string.py +0 -0
- {assertpy2-2.2.0 → assertpy2-2.3.1}/tests/test_structural.py +0 -0
- {assertpy2-2.2.0 → assertpy2-2.3.1}/tests/test_traceback.py +0 -0
- {assertpy2-2.2.0 → assertpy2-2.3.1}/tests/test_type.py +0 -0
- {assertpy2-2.2.0 → assertpy2-2.3.1}/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.1
|
|
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:
|
|
@@ -66,6 +66,19 @@ if TYPE_CHECKING:
|
|
|
66
66
|
def contains_in_order(self, *items: object) -> Self: ...
|
|
67
67
|
def is_empty(self) -> Self: ...
|
|
68
68
|
def is_not_empty(self) -> Self: ...
|
|
69
|
+
# StringMixin - regex
|
|
70
|
+
def extracting_group(self, pattern: str, group: int | str = ...) -> Self: ...
|
|
71
|
+
def matches_with_groups(self, pattern: str) -> Self: ...
|
|
72
|
+
# FileMixin
|
|
73
|
+
def exists(self) -> Self: ...
|
|
74
|
+
def does_not_exist(self) -> Self: ...
|
|
75
|
+
def is_file(self) -> Self: ...
|
|
76
|
+
def is_directory(self) -> Self: ...
|
|
77
|
+
def is_named(self, filename: str) -> Self: ...
|
|
78
|
+
def is_child_of(self, parent: object) -> Self: ...
|
|
79
|
+
def is_readable(self) -> Self: ...
|
|
80
|
+
def is_writable(self) -> Self: ...
|
|
81
|
+
def is_executable(self) -> Self: ...
|
|
69
82
|
|
|
70
83
|
class _NumericAssertion(_CoreAssertion, Protocol):
|
|
71
84
|
"""Assertions available for ``int``, ``float``, and ``complex`` values."""
|
|
@@ -73,7 +73,7 @@ from .numeric import NumericMixin
|
|
|
73
73
|
from .snapshot import SnapshotMixin
|
|
74
74
|
from .string import StringMixin
|
|
75
75
|
|
|
76
|
-
__version__ = "2.1
|
|
76
|
+
__version__ = "2.3.1"
|
|
77
77
|
|
|
78
78
|
__tracebackhide__ = True # clean tracebacks via py.test integration
|
|
79
79
|
contextlib.__tracebackhide__ = True # monkey patch contextlib with clean py.test tracebacks
|
|
@@ -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
|
|