assertpy2 2.1.3__tar.gz → 2.2.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.1.3 → assertpy2-2.2.0}/.github/workflows/ci.yml +5 -5
- {assertpy2-2.1.3 → assertpy2-2.2.0}/.github/workflows/codeql.yml +1 -1
- {assertpy2-2.1.3 → assertpy2-2.2.0}/.github/workflows/publish.yml +18 -5
- {assertpy2-2.1.3 → assertpy2-2.2.0}/.github/workflows/scorecard.yml +1 -1
- {assertpy2-2.1.3 → assertpy2-2.2.0}/PKG-INFO +7 -7
- {assertpy2-2.1.3 → assertpy2-2.2.0}/README.md +6 -6
- {assertpy2-2.1.3 → assertpy2-2.2.0}/assertpy2/_typing.py +19 -0
- {assertpy2-2.1.3 → assertpy2-2.2.0}/assertpy2/assertpy.py +1 -1
- {assertpy2-2.1.3 → assertpy2-2.2.0}/assertpy2/base.py +138 -0
- {assertpy2-2.1.3 → assertpy2-2.2.0}/assertpy2/contains.py +66 -0
- {assertpy2-2.1.3 → assertpy2-2.2.0}/assertpy2/date.py +68 -0
- {assertpy2-2.1.3 → assertpy2-2.2.0}/assertpy2/exception.py +46 -6
- {assertpy2-2.1.3 → assertpy2-2.2.0}/assertpy2/extracting.py +1 -1
- {assertpy2-2.1.3 → assertpy2-2.2.0}/assertpy2/file.py +1 -1
- {assertpy2-2.1.3 → assertpy2-2.2.0}/assertpy2/matchers.py +117 -0
- {assertpy2-2.1.3 → assertpy2-2.2.0}/assertpy2/numeric.py +73 -0
- {assertpy2-2.1.3 → assertpy2-2.2.0}/assertpy2/string.py +106 -0
- {assertpy2-2.1.3 → assertpy2-2.2.0}/docs/api.md +38 -0
- {assertpy2-2.1.3 → assertpy2-2.2.0}/pyproject.toml +2 -2
- {assertpy2-2.1.3 → assertpy2-2.2.0}/tests/test_expected_exception.py +1 -1
- assertpy2-2.2.0/tests/test_matchers_phase3.py +266 -0
- {assertpy2-2.1.3 → assertpy2-2.2.0}/tests/test_numbers.py +140 -0
- assertpy2-2.2.0/tests/test_phase2.py +422 -0
- {assertpy2-2.1.3 → assertpy2-2.2.0}/tests/test_string.py +180 -0
- {assertpy2-2.1.3 → assertpy2-2.2.0}/uv.lock +21 -21
- {assertpy2-2.1.3 → assertpy2-2.2.0}/.codecov.yml +0 -0
- {assertpy2-2.1.3 → assertpy2-2.2.0}/.github/dependabot.yml +0 -0
- {assertpy2-2.1.3 → assertpy2-2.2.0}/.gitignore +0 -0
- {assertpy2-2.1.3 → assertpy2-2.2.0}/CONTRIBUTING.md +0 -0
- {assertpy2-2.1.3 → assertpy2-2.2.0}/LICENSE +0 -0
- {assertpy2-2.1.3 → assertpy2-2.2.0}/SECURITY.md +0 -0
- {assertpy2-2.1.3 → assertpy2-2.2.0}/assertpy2/__init__.py +0 -0
- {assertpy2-2.1.3 → assertpy2-2.2.0}/assertpy2/async_assertions.py +0 -0
- {assertpy2-2.1.3 → assertpy2-2.2.0}/assertpy2/collection.py +0 -0
- {assertpy2-2.1.3 → assertpy2-2.2.0}/assertpy2/dict.py +0 -0
- {assertpy2-2.1.3 → assertpy2-2.2.0}/assertpy2/dynamic.py +0 -0
- {assertpy2-2.1.3 → assertpy2-2.2.0}/assertpy2/errors.py +0 -0
- {assertpy2-2.1.3 → assertpy2-2.2.0}/assertpy2/helpers.py +0 -0
- {assertpy2-2.1.3 → assertpy2-2.2.0}/assertpy2/py.typed +0 -0
- {assertpy2-2.1.3 → assertpy2-2.2.0}/assertpy2/pytest_plugin.py +0 -0
- {assertpy2-2.1.3 → assertpy2-2.2.0}/assertpy2/snapshot.py +0 -0
- {assertpy2-2.1.3 → assertpy2-2.2.0}/docs/logo-dark.svg +0 -0
- {assertpy2-2.1.3 → assertpy2-2.2.0}/docs/logo.svg +0 -0
- {assertpy2-2.1.3 → assertpy2-2.2.0}/tests/test_async.py +0 -0
- {assertpy2-2.1.3 → assertpy2-2.2.0}/tests/test_bool.py +0 -0
- {assertpy2-2.1.3 → assertpy2-2.2.0}/tests/test_class.py +0 -0
- {assertpy2-2.1.3 → assertpy2-2.2.0}/tests/test_collection.py +0 -0
- {assertpy2-2.1.3 → assertpy2-2.2.0}/tests/test_core.py +0 -0
- {assertpy2-2.1.3 → assertpy2-2.2.0}/tests/test_custom_dict.py +0 -0
- {assertpy2-2.1.3 → assertpy2-2.2.0}/tests/test_custom_list.py +0 -0
- {assertpy2-2.1.3 → assertpy2-2.2.0}/tests/test_datetime.py +0 -0
- {assertpy2-2.1.3 → assertpy2-2.2.0}/tests/test_description.py +0 -0
- {assertpy2-2.1.3 → assertpy2-2.2.0}/tests/test_dict.py +0 -0
- {assertpy2-2.1.3 → assertpy2-2.2.0}/tests/test_dict_compare.py +0 -0
- {assertpy2-2.1.3 → assertpy2-2.2.0}/tests/test_dyn.py +0 -0
- {assertpy2-2.1.3 → assertpy2-2.2.0}/tests/test_equals.py +0 -0
- {assertpy2-2.1.3 → assertpy2-2.2.0}/tests/test_errors.py +0 -0
- {assertpy2-2.1.3 → assertpy2-2.2.0}/tests/test_extensions.py +0 -0
- {assertpy2-2.1.3 → assertpy2-2.2.0}/tests/test_extracting.py +0 -0
- {assertpy2-2.1.3 → assertpy2-2.2.0}/tests/test_fail.py +0 -0
- {assertpy2-2.1.3 → assertpy2-2.2.0}/tests/test_file.py +0 -0
- {assertpy2-2.1.3 → assertpy2-2.2.0}/tests/test_in.py +0 -0
- {assertpy2-2.1.3 → assertpy2-2.2.0}/tests/test_list.py +0 -0
- {assertpy2-2.1.3 → assertpy2-2.2.0}/tests/test_matchers.py +0 -0
- {assertpy2-2.1.3 → assertpy2-2.2.0}/tests/test_namedtuple.py +0 -0
- {assertpy2-2.1.3 → assertpy2-2.2.0}/tests/test_none.py +0 -0
- {assertpy2-2.1.3 → assertpy2-2.2.0}/tests/test_overloads.py +0 -0
- {assertpy2-2.1.3 → assertpy2-2.2.0}/tests/test_pytest_plugin.py +0 -0
- {assertpy2-2.1.3 → assertpy2-2.2.0}/tests/test_readme.py +0 -0
- {assertpy2-2.1.3 → assertpy2-2.2.0}/tests/test_same_as.py +0 -0
- {assertpy2-2.1.3 → assertpy2-2.2.0}/tests/test_snapshots.py +0 -0
- {assertpy2-2.1.3 → assertpy2-2.2.0}/tests/test_soft.py +0 -0
- {assertpy2-2.1.3 → assertpy2-2.2.0}/tests/test_soft_fail.py +0 -0
- {assertpy2-2.1.3 → assertpy2-2.2.0}/tests/test_structural.py +0 -0
- {assertpy2-2.1.3 → assertpy2-2.2.0}/tests/test_traceback.py +0 -0
- {assertpy2-2.1.3 → assertpy2-2.2.0}/tests/test_type.py +0 -0
- {assertpy2-2.1.3 → assertpy2-2.2.0}/tests/test_warn.py +0 -0
|
@@ -16,10 +16,10 @@ jobs:
|
|
|
16
16
|
matrix:
|
|
17
17
|
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14", "3.15"]
|
|
18
18
|
steps:
|
|
19
|
-
- uses: actions/checkout@
|
|
19
|
+
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
|
20
20
|
|
|
21
21
|
- name: Install uv
|
|
22
|
-
uses: astral-sh/setup-uv@
|
|
22
|
+
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
|
23
23
|
|
|
24
24
|
- name: Set up Python ${{ matrix.python-version }}
|
|
25
25
|
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
|
@@ -38,7 +38,7 @@ jobs:
|
|
|
38
38
|
|
|
39
39
|
- name: Upload coverage to Codecov
|
|
40
40
|
if: matrix.python-version == '3.14'
|
|
41
|
-
uses: codecov/codecov-action@
|
|
41
|
+
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
|
|
42
42
|
with:
|
|
43
43
|
token: ${{ secrets.CODECOV_TOKEN }}
|
|
44
44
|
files: coverage.xml
|
|
@@ -46,10 +46,10 @@ jobs:
|
|
|
46
46
|
lint:
|
|
47
47
|
runs-on: ubuntu-latest
|
|
48
48
|
steps:
|
|
49
|
-
- uses: actions/checkout@
|
|
49
|
+
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
|
50
50
|
|
|
51
51
|
- name: Install uv
|
|
52
|
-
uses: astral-sh/setup-uv@
|
|
52
|
+
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
|
53
53
|
|
|
54
54
|
- name: Set up Python
|
|
55
55
|
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
|
@@ -17,7 +17,7 @@ jobs:
|
|
|
17
17
|
permissions:
|
|
18
18
|
security-events: write
|
|
19
19
|
steps:
|
|
20
|
-
- uses: actions/checkout@
|
|
20
|
+
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
|
21
21
|
- uses: github/codeql-action/init@f52b05f4acaaa234e44466e66d29050e135ea9ef # v4.36.0
|
|
22
22
|
with:
|
|
23
23
|
languages: python
|
|
@@ -16,10 +16,10 @@ jobs:
|
|
|
16
16
|
contents: write
|
|
17
17
|
attestations: write
|
|
18
18
|
steps:
|
|
19
|
-
- uses: actions/checkout@
|
|
19
|
+
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
|
20
20
|
|
|
21
21
|
- name: Install uv
|
|
22
|
-
uses: astral-sh/setup-uv@
|
|
22
|
+
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
|
23
23
|
|
|
24
24
|
- name: Set up Python
|
|
25
25
|
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
|
@@ -30,15 +30,28 @@ jobs:
|
|
|
30
30
|
- name: Build
|
|
31
31
|
run: uv build
|
|
32
32
|
|
|
33
|
-
- name: Attest
|
|
33
|
+
- name: Attest wheel
|
|
34
|
+
id: attest-wheel
|
|
34
35
|
uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0
|
|
35
36
|
with:
|
|
36
|
-
subject-path: dist
|
|
37
|
+
subject-path: dist/*.whl
|
|
38
|
+
|
|
39
|
+
- name: Attest sdist
|
|
40
|
+
id: attest-sdist
|
|
41
|
+
uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0
|
|
42
|
+
with:
|
|
43
|
+
subject-path: dist/*.tar.gz
|
|
44
|
+
|
|
45
|
+
- name: Prepare provenance bundles
|
|
46
|
+
run: |
|
|
47
|
+
mkdir -p provenance
|
|
48
|
+
cp "${{ steps.attest-wheel.outputs.bundle-path }}" "provenance/$(basename dist/*.whl).sigstore.json"
|
|
49
|
+
cp "${{ steps.attest-sdist.outputs.bundle-path }}" "provenance/$(basename dist/*.tar.gz).sigstore.json"
|
|
37
50
|
|
|
38
51
|
- name: Publish to PyPI
|
|
39
52
|
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
|
|
40
53
|
|
|
41
54
|
- name: Upload to GitHub Release
|
|
42
|
-
run: gh release upload "${{ github.event.release.tag_name }}" dist/*
|
|
55
|
+
run: gh release upload "${{ github.event.release.tag_name }}" dist/* provenance/*
|
|
43
56
|
env:
|
|
44
57
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
@@ -16,7 +16,7 @@ jobs:
|
|
|
16
16
|
security-events: write
|
|
17
17
|
id-token: write
|
|
18
18
|
steps:
|
|
19
|
-
- uses: actions/checkout@
|
|
19
|
+
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
|
20
20
|
with:
|
|
21
21
|
persist-credentials: false
|
|
22
22
|
- uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: assertpy2
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.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
|
|
@@ -194,7 +194,7 @@ assert_that("hello").satisfies(~match.equal_to("world"))
|
|
|
194
194
|
assert_that(150).satisfies(match.is_negative() | match.greater_than(100))
|
|
195
195
|
```
|
|
196
196
|
|
|
197
|
-
Available matchers: `equal_to`, `greater_than`, `greater_than_or_equal_to`, `less_than`, `less_than_or_equal_to`, `between`, `close_to`, `is_none`, `is_not_none`, `is_instance_of`, `has_length`, `is_empty`, `is_not_empty`, `is_positive`, `is_negative`, `contains_string`, `matches_regex`, `is_uuid`, `is_non_empty_string`, `ignore`, `each_item`, `structure`.
|
|
197
|
+
Available matchers: `equal_to`, `greater_than`, `greater_than_or_equal_to`, `less_than`, `less_than_or_equal_to`, `between`, `close_to`, `is_none`, `is_not_none`, `is_instance_of`, `has_length`, `is_empty`, `is_not_empty`, `is_positive`, `is_negative`, `is_zero`, `is_even`, `is_odd`, `is_divisible_by`, `is_callable`, `is_in`, `has_property`, `contains_string`, `matches_regex`, `is_uuid`, `is_non_empty_string`, `ignore`, `each_item`, `structure`.
|
|
198
198
|
|
|
199
199
|
|
|
200
200
|
## Structural matching
|
|
@@ -347,14 +347,14 @@ assert_that({"a": 1, "b": 2, "c": 3}).snapshot()
|
|
|
347
347
|
```py
|
|
348
348
|
from assertpy2 import add_extension
|
|
349
349
|
|
|
350
|
-
def
|
|
351
|
-
if self.val
|
|
352
|
-
return self.error(f'{self.val} is
|
|
350
|
+
def is_5(self):
|
|
351
|
+
if self.val != 5:
|
|
352
|
+
return self.error(f'{self.val} is NOT 5!')
|
|
353
353
|
return self
|
|
354
354
|
|
|
355
|
-
add_extension(
|
|
355
|
+
add_extension(is_5)
|
|
356
356
|
|
|
357
|
-
assert_that(
|
|
357
|
+
assert_that(5).is_5()
|
|
358
358
|
```
|
|
359
359
|
|
|
360
360
|
See the [full API reference](docs/api.md) for all assertion methods, examples, and advanced features.
|
|
@@ -163,7 +163,7 @@ assert_that("hello").satisfies(~match.equal_to("world"))
|
|
|
163
163
|
assert_that(150).satisfies(match.is_negative() | match.greater_than(100))
|
|
164
164
|
```
|
|
165
165
|
|
|
166
|
-
Available matchers: `equal_to`, `greater_than`, `greater_than_or_equal_to`, `less_than`, `less_than_or_equal_to`, `between`, `close_to`, `is_none`, `is_not_none`, `is_instance_of`, `has_length`, `is_empty`, `is_not_empty`, `is_positive`, `is_negative`, `contains_string`, `matches_regex`, `is_uuid`, `is_non_empty_string`, `ignore`, `each_item`, `structure`.
|
|
166
|
+
Available matchers: `equal_to`, `greater_than`, `greater_than_or_equal_to`, `less_than`, `less_than_or_equal_to`, `between`, `close_to`, `is_none`, `is_not_none`, `is_instance_of`, `has_length`, `is_empty`, `is_not_empty`, `is_positive`, `is_negative`, `is_zero`, `is_even`, `is_odd`, `is_divisible_by`, `is_callable`, `is_in`, `has_property`, `contains_string`, `matches_regex`, `is_uuid`, `is_non_empty_string`, `ignore`, `each_item`, `structure`.
|
|
167
167
|
|
|
168
168
|
|
|
169
169
|
## Structural matching
|
|
@@ -316,14 +316,14 @@ assert_that({"a": 1, "b": 2, "c": 3}).snapshot()
|
|
|
316
316
|
```py
|
|
317
317
|
from assertpy2 import add_extension
|
|
318
318
|
|
|
319
|
-
def
|
|
320
|
-
if self.val
|
|
321
|
-
return self.error(f'{self.val} is
|
|
319
|
+
def is_5(self):
|
|
320
|
+
if self.val != 5:
|
|
321
|
+
return self.error(f'{self.val} is NOT 5!')
|
|
322
322
|
return self
|
|
323
323
|
|
|
324
|
-
add_extension(
|
|
324
|
+
add_extension(is_5)
|
|
325
325
|
|
|
326
|
-
assert_that(
|
|
326
|
+
assert_that(5).is_5()
|
|
327
327
|
```
|
|
328
328
|
|
|
329
329
|
See the [full API reference](docs/api.md) for all assertion methods, examples, and advanced features.
|
|
@@ -27,6 +27,8 @@ if TYPE_CHECKING:
|
|
|
27
27
|
def is_type_of(self, some_type: type) -> Self: ...
|
|
28
28
|
def is_instance_of(self, some_class: type) -> Self: ...
|
|
29
29
|
def is_length(self, length: int) -> Self: ...
|
|
30
|
+
def is_callable(self) -> Self: ...
|
|
31
|
+
def is_not_callable(self) -> Self: ...
|
|
30
32
|
def satisfies(self, matcher: Matcher | Callable[..., bool]) -> Self: ...
|
|
31
33
|
# ContainsMixin - universal
|
|
32
34
|
def is_in(self, *items: object) -> Self: ...
|
|
@@ -48,6 +50,10 @@ if TYPE_CHECKING:
|
|
|
48
50
|
def is_digit(self) -> Self: ...
|
|
49
51
|
def is_lower(self) -> Self: ...
|
|
50
52
|
def is_upper(self) -> Self: ...
|
|
53
|
+
def is_alphanumeric(self) -> Self: ...
|
|
54
|
+
def is_whitespace(self) -> Self: ...
|
|
55
|
+
def contains_any_of(self, *items: str) -> Self: ...
|
|
56
|
+
def contains_none_of(self, *items: str) -> Self: ...
|
|
51
57
|
def is_unicode(self) -> Self: ...
|
|
52
58
|
# ContainsMixin
|
|
53
59
|
def contains(self, *items: object) -> Self: ...
|
|
@@ -56,6 +62,8 @@ if TYPE_CHECKING:
|
|
|
56
62
|
def contains_sequence(self, *items: object) -> Self: ...
|
|
57
63
|
def contains_duplicates(self) -> Self: ...
|
|
58
64
|
def does_not_contain_duplicates(self) -> Self: ...
|
|
65
|
+
def contains_exactly(self, *items: object) -> Self: ...
|
|
66
|
+
def contains_in_order(self, *items: object) -> Self: ...
|
|
59
67
|
def is_empty(self) -> Self: ...
|
|
60
68
|
def is_not_empty(self) -> Self: ...
|
|
61
69
|
|
|
@@ -74,6 +82,9 @@ if TYPE_CHECKING:
|
|
|
74
82
|
def is_less_than_or_equal_to(self, other: object) -> Self: ...
|
|
75
83
|
def is_positive(self) -> Self: ...
|
|
76
84
|
def is_negative(self) -> Self: ...
|
|
85
|
+
def is_even(self) -> Self: ...
|
|
86
|
+
def is_odd(self) -> Self: ...
|
|
87
|
+
def is_divisible_by(self, divisor: int) -> Self: ...
|
|
77
88
|
def is_between(self, low: object, high: object) -> Self: ...
|
|
78
89
|
def is_not_between(self, low: object, high: object) -> Self: ...
|
|
79
90
|
def is_close_to(self, other: object, tolerance: object) -> Self: ...
|
|
@@ -89,6 +100,8 @@ if TYPE_CHECKING:
|
|
|
89
100
|
def contains_sequence(self, *items: object) -> Self: ...
|
|
90
101
|
def contains_duplicates(self) -> Self: ...
|
|
91
102
|
def does_not_contain_duplicates(self) -> Self: ...
|
|
103
|
+
def contains_exactly(self, *items: object) -> Self: ...
|
|
104
|
+
def contains_in_order(self, *items: object) -> Self: ...
|
|
92
105
|
def is_empty(self) -> Self: ...
|
|
93
106
|
def is_not_empty(self) -> Self: ...
|
|
94
107
|
# CollectionMixin
|
|
@@ -100,6 +113,9 @@ if TYPE_CHECKING:
|
|
|
100
113
|
def extracting(self, *names: object, **kwargs: object) -> Self: ...
|
|
101
114
|
# BaseMixin
|
|
102
115
|
def each(self, matcher: Matcher | Callable[..., bool]) -> Self: ...
|
|
116
|
+
def any_satisfy(self, matcher: Matcher | Callable[..., bool]) -> Self: ...
|
|
117
|
+
def all_satisfy(self, matcher: Matcher | Callable[..., bool]) -> Self: ...
|
|
118
|
+
def none_satisfy(self, matcher: Matcher | Callable[..., bool]) -> Self: ...
|
|
103
119
|
|
|
104
120
|
class _DictAssertion(_CoreAssertion, Protocol):
|
|
105
121
|
"""Assertions available for ``dict`` values."""
|
|
@@ -127,6 +143,8 @@ if TYPE_CHECKING:
|
|
|
127
143
|
|
|
128
144
|
def is_before(self, other: object) -> Self: ...
|
|
129
145
|
def is_after(self, other: object) -> Self: ...
|
|
146
|
+
def is_before_or_equal_to(self, other: object) -> Self: ...
|
|
147
|
+
def is_after_or_equal_to(self, other: object) -> Self: ...
|
|
130
148
|
def is_equal_to_ignoring_milliseconds(self, other: object) -> Self: ...
|
|
131
149
|
def is_equal_to_ignoring_seconds(self, other: object) -> Self: ...
|
|
132
150
|
def is_equal_to_ignoring_time(self, other: object) -> Self: ...
|
|
@@ -148,5 +166,6 @@ if TYPE_CHECKING:
|
|
|
148
166
|
"""Assertions available for callable values."""
|
|
149
167
|
|
|
150
168
|
def raises(self, ex: type) -> Self: ...
|
|
169
|
+
def does_not_raise(self, ex: type) -> Self: ...
|
|
151
170
|
def when_called_with(self, *some_args: object, **some_kwargs: object) -> Self: ...
|
|
152
171
|
def eventually(self, *, timeout: float = ..., interval: float = ...) -> AsyncAssertionBuilder: ...
|
|
@@ -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.1.4"
|
|
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
|
|
@@ -260,6 +260,144 @@ class BaseMixin:
|
|
|
260
260
|
)
|
|
261
261
|
return self
|
|
262
262
|
|
|
263
|
+
def is_callable(self) -> Self:
|
|
264
|
+
"""Asserts that val is callable.
|
|
265
|
+
|
|
266
|
+
Examples:
|
|
267
|
+
Usage::
|
|
268
|
+
|
|
269
|
+
assert_that(lambda: None).is_callable()
|
|
270
|
+
assert_that(print).is_callable()
|
|
271
|
+
|
|
272
|
+
Returns:
|
|
273
|
+
AssertionBuilder: returns this instance to chain to the next assertion
|
|
274
|
+
|
|
275
|
+
Raises:
|
|
276
|
+
AssertionError: if val is **not** callable
|
|
277
|
+
"""
|
|
278
|
+
if not callable(self.val):
|
|
279
|
+
return self.error(f"Expected <{self.val}> to be callable, but was not.")
|
|
280
|
+
return self
|
|
281
|
+
|
|
282
|
+
def is_not_callable(self) -> Self:
|
|
283
|
+
"""Asserts that val is not callable.
|
|
284
|
+
|
|
285
|
+
Examples:
|
|
286
|
+
Usage::
|
|
287
|
+
|
|
288
|
+
assert_that(42).is_not_callable()
|
|
289
|
+
assert_that('foo').is_not_callable()
|
|
290
|
+
|
|
291
|
+
Returns:
|
|
292
|
+
AssertionBuilder: returns this instance to chain to the next assertion
|
|
293
|
+
|
|
294
|
+
Raises:
|
|
295
|
+
AssertionError: if val **is** callable
|
|
296
|
+
"""
|
|
297
|
+
if callable(self.val):
|
|
298
|
+
return self.error(f"Expected <{self.val}> to not be callable, but was.")
|
|
299
|
+
return self
|
|
300
|
+
|
|
301
|
+
def any_satisfy(self, matcher) -> Self:
|
|
302
|
+
"""Asserts that at least one item in val satisfies the given matcher.
|
|
303
|
+
|
|
304
|
+
Args:
|
|
305
|
+
matcher: a :class:`~assertpy2.matchers.Matcher` instance, or a callable that takes
|
|
306
|
+
a value and returns a bool
|
|
307
|
+
|
|
308
|
+
Examples:
|
|
309
|
+
Usage with matchers::
|
|
310
|
+
|
|
311
|
+
from assertpy2 import match
|
|
312
|
+
|
|
313
|
+
assert_that([1, -2, 3]).any_satisfy(match.is_negative())
|
|
314
|
+
|
|
315
|
+
Usage with callables::
|
|
316
|
+
|
|
317
|
+
assert_that([1, 2, 3]).any_satisfy(lambda x: x > 2)
|
|
318
|
+
|
|
319
|
+
Returns:
|
|
320
|
+
AssertionBuilder: returns this instance to chain to the next assertion
|
|
321
|
+
|
|
322
|
+
Raises:
|
|
323
|
+
AssertionError: if no item satisfies the matcher
|
|
324
|
+
"""
|
|
325
|
+
if not isinstance(self.val, collections.abc.Iterable):
|
|
326
|
+
raise TypeError("val is not iterable")
|
|
327
|
+
if isinstance(matcher, Matcher):
|
|
328
|
+
if not any(matcher.matches(item) for item in self.val):
|
|
329
|
+
return self.error(f"Expected any item to satisfy {matcher.describe()}, but none did.")
|
|
330
|
+
elif callable(matcher):
|
|
331
|
+
if not any(matcher(item) for item in self.val):
|
|
332
|
+
return self.error(f"Expected any item to satisfy <{matcher}>, but none did.")
|
|
333
|
+
else:
|
|
334
|
+
raise TypeError("given arg must be a Matcher or callable")
|
|
335
|
+
return self
|
|
336
|
+
|
|
337
|
+
def all_satisfy(self, matcher) -> Self:
|
|
338
|
+
"""Asserts that all items in val satisfy the given matcher.
|
|
339
|
+
|
|
340
|
+
Semantic alias for :meth:`each`.
|
|
341
|
+
|
|
342
|
+
Args:
|
|
343
|
+
matcher: a :class:`~assertpy2.matchers.Matcher` instance, or a callable that takes
|
|
344
|
+
a value and returns a bool
|
|
345
|
+
|
|
346
|
+
Examples:
|
|
347
|
+
Usage with matchers::
|
|
348
|
+
|
|
349
|
+
from assertpy2 import match
|
|
350
|
+
|
|
351
|
+
assert_that([1, 2, 3]).all_satisfy(match.is_positive())
|
|
352
|
+
|
|
353
|
+
Returns:
|
|
354
|
+
AssertionBuilder: returns this instance to chain to the next assertion
|
|
355
|
+
|
|
356
|
+
Raises:
|
|
357
|
+
AssertionError: if any item does **not** satisfy the matcher
|
|
358
|
+
"""
|
|
359
|
+
return self.each(matcher)
|
|
360
|
+
|
|
361
|
+
def none_satisfy(self, matcher) -> Self:
|
|
362
|
+
"""Asserts that no item in val satisfies the given matcher.
|
|
363
|
+
|
|
364
|
+
Args:
|
|
365
|
+
matcher: a :class:`~assertpy2.matchers.Matcher` instance, or a callable that takes
|
|
366
|
+
a value and returns a bool
|
|
367
|
+
|
|
368
|
+
Examples:
|
|
369
|
+
Usage with matchers::
|
|
370
|
+
|
|
371
|
+
from assertpy2 import match
|
|
372
|
+
|
|
373
|
+
assert_that([1, 2, 3]).none_satisfy(match.is_negative())
|
|
374
|
+
|
|
375
|
+
Usage with callables::
|
|
376
|
+
|
|
377
|
+
assert_that([1, 2, 3]).none_satisfy(lambda x: x < 0)
|
|
378
|
+
|
|
379
|
+
Returns:
|
|
380
|
+
AssertionBuilder: returns this instance to chain to the next assertion
|
|
381
|
+
|
|
382
|
+
Raises:
|
|
383
|
+
AssertionError: if any item satisfies the matcher
|
|
384
|
+
"""
|
|
385
|
+
if not isinstance(self.val, collections.abc.Iterable):
|
|
386
|
+
raise TypeError("val is not iterable")
|
|
387
|
+
if isinstance(matcher, Matcher):
|
|
388
|
+
for i, item in enumerate(self.val):
|
|
389
|
+
if matcher.matches(item):
|
|
390
|
+
return self.error(
|
|
391
|
+
f"Expected no item to satisfy {matcher.describe()}, but item at index {i} <{item}> did."
|
|
392
|
+
)
|
|
393
|
+
elif callable(matcher):
|
|
394
|
+
for i, item in enumerate(self.val):
|
|
395
|
+
if matcher(item):
|
|
396
|
+
return self.error(f"Expected no item to satisfy <{matcher}>, but item at index {i} <{item}> did.")
|
|
397
|
+
else:
|
|
398
|
+
raise TypeError("given arg must be a Matcher or callable")
|
|
399
|
+
return self
|
|
400
|
+
|
|
263
401
|
def is_not_equal_to(self, other) -> Self:
|
|
264
402
|
"""Asserts that val is not equal to other.
|
|
265
403
|
|
|
@@ -349,6 +349,72 @@ class ContainsMixin:
|
|
|
349
349
|
return self.error("Expected not empty, but was empty.")
|
|
350
350
|
return self
|
|
351
351
|
|
|
352
|
+
def contains_exactly(self, *items) -> Self:
|
|
353
|
+
"""Asserts that val contains exactly the given items in the given order.
|
|
354
|
+
|
|
355
|
+
Unlike :meth:`contains_only` (which ignores order) and :meth:`contains_sequence`
|
|
356
|
+
(which allows extra items), this method requires exact count, items, and order.
|
|
357
|
+
|
|
358
|
+
Args:
|
|
359
|
+
*items: the items expected, in exact order
|
|
360
|
+
|
|
361
|
+
Examples:
|
|
362
|
+
Usage::
|
|
363
|
+
|
|
364
|
+
assert_that([1, 2, 3]).contains_exactly(1, 2, 3)
|
|
365
|
+
assert_that(['a', 'b']).contains_exactly('a', 'b')
|
|
366
|
+
|
|
367
|
+
Returns:
|
|
368
|
+
AssertionBuilder: returns this instance to chain to the next assertion
|
|
369
|
+
|
|
370
|
+
Raises:
|
|
371
|
+
AssertionError: if val does **not** contain exactly the given items in order
|
|
372
|
+
"""
|
|
373
|
+
if len(items) == 0:
|
|
374
|
+
raise ValueError("one or more args must be given")
|
|
375
|
+
try:
|
|
376
|
+
val_list = list(self.val)
|
|
377
|
+
except TypeError:
|
|
378
|
+
raise TypeError("val is not iterable") from None
|
|
379
|
+
if val_list != list(items):
|
|
380
|
+
return self.error(f"Expected <{self.val}> to contain exactly {self._fmt_items(items)}, but did not.")
|
|
381
|
+
return self
|
|
382
|
+
|
|
383
|
+
def contains_in_order(self, *items) -> Self:
|
|
384
|
+
"""Asserts that val contains the given items in the given order (as a subsequence).
|
|
385
|
+
|
|
386
|
+
Items must appear in the given order but do not need to be contiguous.
|
|
387
|
+
Unlike :meth:`contains_sequence` which requires contiguous items.
|
|
388
|
+
|
|
389
|
+
Args:
|
|
390
|
+
*items: the items expected, in order (but not necessarily contiguous)
|
|
391
|
+
|
|
392
|
+
Examples:
|
|
393
|
+
Usage::
|
|
394
|
+
|
|
395
|
+
assert_that([1, 5, 2, 8, 3]).contains_in_order(1, 2, 3)
|
|
396
|
+
assert_that(['a', 'x', 'b', 'y', 'c']).contains_in_order('a', 'b', 'c')
|
|
397
|
+
|
|
398
|
+
Returns:
|
|
399
|
+
AssertionBuilder: returns this instance to chain to the next assertion
|
|
400
|
+
|
|
401
|
+
Raises:
|
|
402
|
+
AssertionError: if val does **not** contain items in the given order
|
|
403
|
+
"""
|
|
404
|
+
if len(items) == 0:
|
|
405
|
+
raise ValueError("one or more args must be given")
|
|
406
|
+
try:
|
|
407
|
+
val_list = list(self.val)
|
|
408
|
+
except TypeError:
|
|
409
|
+
raise TypeError("val is not iterable") from None
|
|
410
|
+
item_idx = 0
|
|
411
|
+
for element in val_list:
|
|
412
|
+
if item_idx < len(items) and element == items[item_idx]:
|
|
413
|
+
item_idx += 1
|
|
414
|
+
if item_idx != len(items):
|
|
415
|
+
return self.error(f"Expected <{self.val}> to contain {self._fmt_items(items)} in order, but did not.")
|
|
416
|
+
return self
|
|
417
|
+
|
|
352
418
|
def is_in(self, *items) -> Self:
|
|
353
419
|
"""Asserts that val is equal to one of the given items.
|
|
354
420
|
|
|
@@ -114,6 +114,74 @@ class DateMixin:
|
|
|
114
114
|
)
|
|
115
115
|
return self
|
|
116
116
|
|
|
117
|
+
def is_before_or_equal_to(self, other) -> Self:
|
|
118
|
+
"""Asserts that val is a date and is before or equal to other date.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
other: the other date, expected to be after or equal to val
|
|
122
|
+
|
|
123
|
+
Examples:
|
|
124
|
+
Usage::
|
|
125
|
+
|
|
126
|
+
import datetime
|
|
127
|
+
|
|
128
|
+
today = datetime.datetime.now()
|
|
129
|
+
yesterday = today - datetime.timedelta(days=1)
|
|
130
|
+
|
|
131
|
+
assert_that(yesterday).is_before_or_equal_to(today)
|
|
132
|
+
assert_that(today).is_before_or_equal_to(today)
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
AssertionBuilder: returns this instance to chain to the next assertion
|
|
136
|
+
|
|
137
|
+
Raises:
|
|
138
|
+
AssertionError: if val is **not** before or equal to the given date
|
|
139
|
+
"""
|
|
140
|
+
if type(self.val) is not datetime.datetime:
|
|
141
|
+
raise TypeError(f"val must be datetime, but was type <{type(self.val).__name__}>")
|
|
142
|
+
if type(other) is not datetime.datetime:
|
|
143
|
+
raise TypeError(f"given arg must be datetime, but was type <{type(other).__name__}>")
|
|
144
|
+
if self.val > other:
|
|
145
|
+
return self.error(
|
|
146
|
+
f"Expected <{self.val.strftime('%Y-%m-%d %H:%M:%S')}> to be before or equal to"
|
|
147
|
+
f" <{other.strftime('%Y-%m-%d %H:%M:%S')}>, but was not."
|
|
148
|
+
)
|
|
149
|
+
return self
|
|
150
|
+
|
|
151
|
+
def is_after_or_equal_to(self, other) -> Self:
|
|
152
|
+
"""Asserts that val is a date and is after or equal to other date.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
other: the other date, expected to be before or equal to val
|
|
156
|
+
|
|
157
|
+
Examples:
|
|
158
|
+
Usage::
|
|
159
|
+
|
|
160
|
+
import datetime
|
|
161
|
+
|
|
162
|
+
today = datetime.datetime.now()
|
|
163
|
+
yesterday = today - datetime.timedelta(days=1)
|
|
164
|
+
|
|
165
|
+
assert_that(today).is_after_or_equal_to(yesterday)
|
|
166
|
+
assert_that(today).is_after_or_equal_to(today)
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
AssertionBuilder: returns this instance to chain to the next assertion
|
|
170
|
+
|
|
171
|
+
Raises:
|
|
172
|
+
AssertionError: if val is **not** after or equal to the given date
|
|
173
|
+
"""
|
|
174
|
+
if type(self.val) is not datetime.datetime:
|
|
175
|
+
raise TypeError(f"val must be datetime, but was type <{type(self.val).__name__}>")
|
|
176
|
+
if type(other) is not datetime.datetime:
|
|
177
|
+
raise TypeError(f"given arg must be datetime, but was type <{type(other).__name__}>")
|
|
178
|
+
if self.val < other:
|
|
179
|
+
return self.error(
|
|
180
|
+
f"Expected <{self.val.strftime('%Y-%m-%d %H:%M:%S')}> to be after or equal to"
|
|
181
|
+
f" <{other.strftime('%Y-%m-%d %H:%M:%S')}>, but was not."
|
|
182
|
+
)
|
|
183
|
+
return self
|
|
184
|
+
|
|
117
185
|
def is_equal_to_ignoring_milliseconds(self, other) -> Self:
|
|
118
186
|
"""Asserts that val is a date and is equal to other date to the second.
|
|
119
187
|
|
|
@@ -78,7 +78,7 @@ class ExceptionMixin:
|
|
|
78
78
|
"""Asserts that val, when invoked with the given args and kwargs, raises the expected exception.
|
|
79
79
|
|
|
80
80
|
Invokes ``val()`` with the given args and kwargs. You must first set the expected
|
|
81
|
-
exception with :meth:`~raises`.
|
|
81
|
+
exception with :meth:`~raises` or :meth:`~does_not_raise`.
|
|
82
82
|
|
|
83
83
|
Args:
|
|
84
84
|
*some_args: the args to call ``val()``
|
|
@@ -97,18 +97,20 @@ class ExceptionMixin:
|
|
|
97
97
|
|
|
98
98
|
Raises:
|
|
99
99
|
AssertionError: if val does **not** raise the expected exception
|
|
100
|
-
TypeError: if expected exception not set via :meth:`raises`
|
|
100
|
+
TypeError: if expected exception not set via :meth:`raises` or :meth:`does_not_raise`
|
|
101
101
|
"""
|
|
102
102
|
if not self.expected:
|
|
103
|
-
raise TypeError("expected exception not set, raises() must be called first")
|
|
103
|
+
raise TypeError("expected exception not set, raises() or does_not_raise() must be called first")
|
|
104
|
+
|
|
105
|
+
if getattr(self, "_not_expected", False):
|
|
106
|
+
return self._when_called_with_not_expected(*some_args, **some_kwargs)
|
|
107
|
+
|
|
104
108
|
try:
|
|
105
109
|
self.val(*some_args, **some_kwargs)
|
|
106
110
|
except BaseException as e:
|
|
107
111
|
if issubclass(type(e), self.expected):
|
|
108
|
-
# chain on with error message
|
|
109
112
|
return self.builder(str(e), self.description, self.kind, logger=self.logger)
|
|
110
113
|
else:
|
|
111
|
-
# got exception, but wrong type, so raise
|
|
112
114
|
self.error(
|
|
113
115
|
"Expected <%s> to raise <%s> when called with (%s), but raised <%s>."
|
|
114
116
|
% (
|
|
@@ -120,9 +122,47 @@ class ExceptionMixin:
|
|
|
120
122
|
)
|
|
121
123
|
return _InertBuilder()
|
|
122
124
|
|
|
123
|
-
# didn't fail as expected, so raise
|
|
124
125
|
self.error(
|
|
125
126
|
"Expected <%s> to raise <%s> when called with (%s)."
|
|
126
127
|
% (self.val.__name__, self.expected.__name__, self._fmt_args_kwargs(*some_args, **some_kwargs))
|
|
127
128
|
)
|
|
128
129
|
return _InertBuilder()
|
|
130
|
+
|
|
131
|
+
def _when_called_with_not_expected(self, *some_args, **some_kwargs) -> Self:
|
|
132
|
+
try:
|
|
133
|
+
self.val(*some_args, **some_kwargs)
|
|
134
|
+
except BaseException as e:
|
|
135
|
+
if issubclass(type(e), self.expected):
|
|
136
|
+
self.error(
|
|
137
|
+
f"Expected <{self.val.__name__}> to not raise <{self.expected.__name__}>"
|
|
138
|
+
f" when called with ({self._fmt_args_kwargs(*some_args, **some_kwargs)}),"
|
|
139
|
+
f" but did raise <{type(e).__name__}>."
|
|
140
|
+
)
|
|
141
|
+
return _InertBuilder()
|
|
142
|
+
return self
|
|
143
|
+
|
|
144
|
+
def does_not_raise(self, ex) -> Self:
|
|
145
|
+
"""Asserts that val is callable and sets the not-expected exception.
|
|
146
|
+
|
|
147
|
+
Just sets the not-expected exception, but never calls val. You must
|
|
148
|
+
chain to :meth:`~when_called_with` to invoke ``val()``.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
ex: the exception that should **not** be raised
|
|
152
|
+
|
|
153
|
+
Examples:
|
|
154
|
+
Usage::
|
|
155
|
+
|
|
156
|
+
assert_that(some_func).does_not_raise(RuntimeError).when_called_with('foo')
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
AssertionBuilder: returns a new instance to chain to the next assertion
|
|
160
|
+
"""
|
|
161
|
+
if not callable(self.val):
|
|
162
|
+
raise TypeError("val must be callable")
|
|
163
|
+
if not issubclass(ex, BaseException):
|
|
164
|
+
raise TypeError("given arg must be exception")
|
|
165
|
+
|
|
166
|
+
new_builder = self.builder(self.val, self.description, self.kind, ex, self.logger)
|
|
167
|
+
new_builder._not_expected = True
|
|
168
|
+
return new_builder
|
|
@@ -180,7 +180,7 @@ class ExtractingMixin:
|
|
|
180
180
|
return getattr(x, name)
|
|
181
181
|
else: # val has no attribute <foo>
|
|
182
182
|
raise ValueError("item attributes %s did no contain attribute <%s>" % (x._fields, name))
|
|
183
|
-
elif isinstance(x, collections.abc.Iterable):
|
|
183
|
+
elif isinstance(x, collections.abc.Iterable):
|
|
184
184
|
self._check_iterable(x, name="item")
|
|
185
185
|
return x[name]
|
|
186
186
|
elif hasattr(x, name):
|
|
@@ -77,7 +77,7 @@ def contents_of(file, encoding="utf-8"):
|
|
|
77
77
|
try:
|
|
78
78
|
return contents.decode(encoding, "replace")
|
|
79
79
|
except AttributeError:
|
|
80
|
-
pass
|
|
80
|
+
pass # contents is already a str, no decode needed
|
|
81
81
|
# if all else fails, just return the contents "as is"
|
|
82
82
|
return contents
|
|
83
83
|
|