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.
Files changed (77) hide show
  1. {assertpy2-2.1.3 → assertpy2-2.2.0}/.github/workflows/ci.yml +5 -5
  2. {assertpy2-2.1.3 → assertpy2-2.2.0}/.github/workflows/codeql.yml +1 -1
  3. {assertpy2-2.1.3 → assertpy2-2.2.0}/.github/workflows/publish.yml +18 -5
  4. {assertpy2-2.1.3 → assertpy2-2.2.0}/.github/workflows/scorecard.yml +1 -1
  5. {assertpy2-2.1.3 → assertpy2-2.2.0}/PKG-INFO +7 -7
  6. {assertpy2-2.1.3 → assertpy2-2.2.0}/README.md +6 -6
  7. {assertpy2-2.1.3 → assertpy2-2.2.0}/assertpy2/_typing.py +19 -0
  8. {assertpy2-2.1.3 → assertpy2-2.2.0}/assertpy2/assertpy.py +1 -1
  9. {assertpy2-2.1.3 → assertpy2-2.2.0}/assertpy2/base.py +138 -0
  10. {assertpy2-2.1.3 → assertpy2-2.2.0}/assertpy2/contains.py +66 -0
  11. {assertpy2-2.1.3 → assertpy2-2.2.0}/assertpy2/date.py +68 -0
  12. {assertpy2-2.1.3 → assertpy2-2.2.0}/assertpy2/exception.py +46 -6
  13. {assertpy2-2.1.3 → assertpy2-2.2.0}/assertpy2/extracting.py +1 -1
  14. {assertpy2-2.1.3 → assertpy2-2.2.0}/assertpy2/file.py +1 -1
  15. {assertpy2-2.1.3 → assertpy2-2.2.0}/assertpy2/matchers.py +117 -0
  16. {assertpy2-2.1.3 → assertpy2-2.2.0}/assertpy2/numeric.py +73 -0
  17. {assertpy2-2.1.3 → assertpy2-2.2.0}/assertpy2/string.py +106 -0
  18. {assertpy2-2.1.3 → assertpy2-2.2.0}/docs/api.md +38 -0
  19. {assertpy2-2.1.3 → assertpy2-2.2.0}/pyproject.toml +2 -2
  20. {assertpy2-2.1.3 → assertpy2-2.2.0}/tests/test_expected_exception.py +1 -1
  21. assertpy2-2.2.0/tests/test_matchers_phase3.py +266 -0
  22. {assertpy2-2.1.3 → assertpy2-2.2.0}/tests/test_numbers.py +140 -0
  23. assertpy2-2.2.0/tests/test_phase2.py +422 -0
  24. {assertpy2-2.1.3 → assertpy2-2.2.0}/tests/test_string.py +180 -0
  25. {assertpy2-2.1.3 → assertpy2-2.2.0}/uv.lock +21 -21
  26. {assertpy2-2.1.3 → assertpy2-2.2.0}/.codecov.yml +0 -0
  27. {assertpy2-2.1.3 → assertpy2-2.2.0}/.github/dependabot.yml +0 -0
  28. {assertpy2-2.1.3 → assertpy2-2.2.0}/.gitignore +0 -0
  29. {assertpy2-2.1.3 → assertpy2-2.2.0}/CONTRIBUTING.md +0 -0
  30. {assertpy2-2.1.3 → assertpy2-2.2.0}/LICENSE +0 -0
  31. {assertpy2-2.1.3 → assertpy2-2.2.0}/SECURITY.md +0 -0
  32. {assertpy2-2.1.3 → assertpy2-2.2.0}/assertpy2/__init__.py +0 -0
  33. {assertpy2-2.1.3 → assertpy2-2.2.0}/assertpy2/async_assertions.py +0 -0
  34. {assertpy2-2.1.3 → assertpy2-2.2.0}/assertpy2/collection.py +0 -0
  35. {assertpy2-2.1.3 → assertpy2-2.2.0}/assertpy2/dict.py +0 -0
  36. {assertpy2-2.1.3 → assertpy2-2.2.0}/assertpy2/dynamic.py +0 -0
  37. {assertpy2-2.1.3 → assertpy2-2.2.0}/assertpy2/errors.py +0 -0
  38. {assertpy2-2.1.3 → assertpy2-2.2.0}/assertpy2/helpers.py +0 -0
  39. {assertpy2-2.1.3 → assertpy2-2.2.0}/assertpy2/py.typed +0 -0
  40. {assertpy2-2.1.3 → assertpy2-2.2.0}/assertpy2/pytest_plugin.py +0 -0
  41. {assertpy2-2.1.3 → assertpy2-2.2.0}/assertpy2/snapshot.py +0 -0
  42. {assertpy2-2.1.3 → assertpy2-2.2.0}/docs/logo-dark.svg +0 -0
  43. {assertpy2-2.1.3 → assertpy2-2.2.0}/docs/logo.svg +0 -0
  44. {assertpy2-2.1.3 → assertpy2-2.2.0}/tests/test_async.py +0 -0
  45. {assertpy2-2.1.3 → assertpy2-2.2.0}/tests/test_bool.py +0 -0
  46. {assertpy2-2.1.3 → assertpy2-2.2.0}/tests/test_class.py +0 -0
  47. {assertpy2-2.1.3 → assertpy2-2.2.0}/tests/test_collection.py +0 -0
  48. {assertpy2-2.1.3 → assertpy2-2.2.0}/tests/test_core.py +0 -0
  49. {assertpy2-2.1.3 → assertpy2-2.2.0}/tests/test_custom_dict.py +0 -0
  50. {assertpy2-2.1.3 → assertpy2-2.2.0}/tests/test_custom_list.py +0 -0
  51. {assertpy2-2.1.3 → assertpy2-2.2.0}/tests/test_datetime.py +0 -0
  52. {assertpy2-2.1.3 → assertpy2-2.2.0}/tests/test_description.py +0 -0
  53. {assertpy2-2.1.3 → assertpy2-2.2.0}/tests/test_dict.py +0 -0
  54. {assertpy2-2.1.3 → assertpy2-2.2.0}/tests/test_dict_compare.py +0 -0
  55. {assertpy2-2.1.3 → assertpy2-2.2.0}/tests/test_dyn.py +0 -0
  56. {assertpy2-2.1.3 → assertpy2-2.2.0}/tests/test_equals.py +0 -0
  57. {assertpy2-2.1.3 → assertpy2-2.2.0}/tests/test_errors.py +0 -0
  58. {assertpy2-2.1.3 → assertpy2-2.2.0}/tests/test_extensions.py +0 -0
  59. {assertpy2-2.1.3 → assertpy2-2.2.0}/tests/test_extracting.py +0 -0
  60. {assertpy2-2.1.3 → assertpy2-2.2.0}/tests/test_fail.py +0 -0
  61. {assertpy2-2.1.3 → assertpy2-2.2.0}/tests/test_file.py +0 -0
  62. {assertpy2-2.1.3 → assertpy2-2.2.0}/tests/test_in.py +0 -0
  63. {assertpy2-2.1.3 → assertpy2-2.2.0}/tests/test_list.py +0 -0
  64. {assertpy2-2.1.3 → assertpy2-2.2.0}/tests/test_matchers.py +0 -0
  65. {assertpy2-2.1.3 → assertpy2-2.2.0}/tests/test_namedtuple.py +0 -0
  66. {assertpy2-2.1.3 → assertpy2-2.2.0}/tests/test_none.py +0 -0
  67. {assertpy2-2.1.3 → assertpy2-2.2.0}/tests/test_overloads.py +0 -0
  68. {assertpy2-2.1.3 → assertpy2-2.2.0}/tests/test_pytest_plugin.py +0 -0
  69. {assertpy2-2.1.3 → assertpy2-2.2.0}/tests/test_readme.py +0 -0
  70. {assertpy2-2.1.3 → assertpy2-2.2.0}/tests/test_same_as.py +0 -0
  71. {assertpy2-2.1.3 → assertpy2-2.2.0}/tests/test_snapshots.py +0 -0
  72. {assertpy2-2.1.3 → assertpy2-2.2.0}/tests/test_soft.py +0 -0
  73. {assertpy2-2.1.3 → assertpy2-2.2.0}/tests/test_soft_fail.py +0 -0
  74. {assertpy2-2.1.3 → assertpy2-2.2.0}/tests/test_structural.py +0 -0
  75. {assertpy2-2.1.3 → assertpy2-2.2.0}/tests/test_traceback.py +0 -0
  76. {assertpy2-2.1.3 → assertpy2-2.2.0}/tests/test_type.py +0 -0
  77. {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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
19
+ - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
20
20
 
21
21
  - name: Install uv
22
- uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
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@cddd853df119a48c5be31a973f8cd97e12e35e16 # v6.0.1
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
49
+ - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
50
50
 
51
51
  - name: Install uv
52
- uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
19
+ - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
20
20
 
21
21
  - name: Install uv
22
- uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
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 build provenance
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
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.1.3
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 is_even(self):
351
- if self.val % 2 != 0:
352
- return self.error(f'{self.val} is not even!')
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(is_even)
355
+ add_extension(is_5)
356
356
 
357
- assert_that(4).is_even()
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 is_even(self):
320
- if self.val % 2 != 0:
321
- return self.error(f'{self.val} is not even!')
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(is_even)
324
+ add_extension(is_5)
325
325
 
326
- assert_that(4).is_even()
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.3"
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): # FIXME, this does __getitem__, but doesn't check for it...
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