assertpy2 2.12.0__tar.gz → 2.13.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 (155) hide show
  1. assertpy2-2.13.0/.github/dependabot.yml +31 -0
  2. {assertpy2-2.12.0 → assertpy2-2.13.0}/.github/workflows/ci.yml +22 -4
  3. {assertpy2-2.12.0 → assertpy2-2.13.0}/.github/workflows/codeql.yml +1 -1
  4. {assertpy2-2.12.0 → assertpy2-2.13.0}/.github/workflows/docs.yml +2 -2
  5. {assertpy2-2.12.0 → assertpy2-2.13.0}/.github/workflows/mutation.yml +2 -2
  6. {assertpy2-2.12.0 → assertpy2-2.13.0}/.github/workflows/publish.yml +2 -2
  7. {assertpy2-2.12.0 → assertpy2-2.13.0}/.github/workflows/scorecard.yml +1 -1
  8. {assertpy2-2.12.0 → assertpy2-2.13.0}/.github/workflows/zizmor.yml +2 -2
  9. {assertpy2-2.12.0 → assertpy2-2.13.0}/PKG-INFO +2 -2
  10. {assertpy2-2.12.0 → assertpy2-2.13.0}/assertpy2/__init__.py +2 -0
  11. {assertpy2-2.12.0 → assertpy2-2.13.0}/assertpy2/_compare.py +87 -1
  12. {assertpy2-2.12.0 → assertpy2-2.13.0}/assertpy2/_typing.py +17 -2
  13. {assertpy2-2.12.0 → assertpy2-2.13.0}/assertpy2/assertpy.py +26 -4
  14. {assertpy2-2.12.0 → assertpy2-2.13.0}/assertpy2/async_assertions.py +35 -4
  15. {assertpy2-2.12.0 → assertpy2-2.13.0}/assertpy2/base.py +13 -34
  16. {assertpy2-2.12.0 → assertpy2-2.13.0}/assertpy2/helpers.py +3 -3
  17. {assertpy2-2.12.0 → assertpy2-2.13.0}/assertpy2/matchers.py +10 -5
  18. {assertpy2-2.12.0 → assertpy2-2.13.0}/assertpy2/snapshot.py +62 -3
  19. {assertpy2-2.12.0 → assertpy2-2.13.0}/docs/assertions.md +5 -1
  20. {assertpy2-2.12.0 → assertpy2-2.13.0}/docs/integrations.md +2 -1
  21. {assertpy2-2.12.0 → assertpy2-2.13.0}/docs/matchers.md +6 -1
  22. assertpy2-2.13.0/docs/reference/snapshots.md +11 -0
  23. {assertpy2-2.12.0 → assertpy2-2.13.0}/docs/testing.md +35 -6
  24. {assertpy2-2.12.0 → assertpy2-2.13.0}/mkdocs.yml +1 -1
  25. assertpy2-2.13.0/pyproject.toml +158 -0
  26. {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_async.py +107 -0
  27. {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_equals.py +129 -0
  28. {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_matchers.py +24 -0
  29. {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_snapshots.py +77 -1
  30. {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_structural.py +25 -0
  31. {assertpy2-2.12.0 → assertpy2-2.13.0}/uv.lock +237 -69
  32. assertpy2-2.12.0/.github/dependabot.yml +0 -20
  33. assertpy2-2.12.0/docs/reference/snapshots.md +0 -5
  34. assertpy2-2.12.0/pyproject.toml +0 -104
  35. {assertpy2-2.12.0 → assertpy2-2.13.0}/.codecov.yml +0 -0
  36. {assertpy2-2.12.0 → assertpy2-2.13.0}/.gitattributes +0 -0
  37. {assertpy2-2.12.0 → assertpy2-2.13.0}/.gitignore +0 -0
  38. {assertpy2-2.12.0 → assertpy2-2.13.0}/CONTRIBUTING.md +0 -0
  39. {assertpy2-2.12.0 → assertpy2-2.13.0}/LICENSE +0 -0
  40. {assertpy2-2.12.0 → assertpy2-2.13.0}/README.md +0 -0
  41. {assertpy2-2.12.0 → assertpy2-2.13.0}/SECURITY.md +0 -0
  42. {assertpy2-2.12.0 → assertpy2-2.13.0}/assertpy2/_compat.py +0 -0
  43. {assertpy2-2.12.0 → assertpy2-2.13.0}/assertpy2/_introspection.py +0 -0
  44. {assertpy2-2.12.0 → assertpy2-2.13.0}/assertpy2/_mixin_base.py +0 -0
  45. {assertpy2-2.12.0 → assertpy2-2.13.0}/assertpy2/behave_matchers.py +0 -0
  46. {assertpy2-2.12.0 → assertpy2-2.13.0}/assertpy2/bytes_mixin.py +0 -0
  47. {assertpy2-2.12.0 → assertpy2-2.13.0}/assertpy2/collection.py +0 -0
  48. {assertpy2-2.12.0 → assertpy2-2.13.0}/assertpy2/contains.py +0 -0
  49. {assertpy2-2.12.0 → assertpy2-2.13.0}/assertpy2/dataframe.py +0 -0
  50. {assertpy2-2.12.0 → assertpy2-2.13.0}/assertpy2/date.py +0 -0
  51. {assertpy2-2.12.0 → assertpy2-2.13.0}/assertpy2/dict.py +0 -0
  52. {assertpy2-2.12.0 → assertpy2-2.13.0}/assertpy2/dynamic.py +0 -0
  53. {assertpy2-2.12.0 → assertpy2-2.13.0}/assertpy2/errors.py +0 -0
  54. {assertpy2-2.12.0 → assertpy2-2.13.0}/assertpy2/exception.py +0 -0
  55. {assertpy2-2.12.0 → assertpy2-2.13.0}/assertpy2/extracting.py +0 -0
  56. {assertpy2-2.12.0 → assertpy2-2.13.0}/assertpy2/file.py +0 -0
  57. {assertpy2-2.12.0 → assertpy2-2.13.0}/assertpy2/json_mixin.py +0 -0
  58. {assertpy2-2.12.0 → assertpy2-2.13.0}/assertpy2/numeric.py +0 -0
  59. {assertpy2-2.12.0 → assertpy2-2.13.0}/assertpy2/py.typed +0 -0
  60. {assertpy2-2.12.0 → assertpy2-2.13.0}/assertpy2/pytest_plugin.py +0 -0
  61. {assertpy2-2.12.0 → assertpy2-2.13.0}/assertpy2/string.py +0 -0
  62. {assertpy2-2.12.0 → assertpy2-2.13.0}/assertpy2/warning.py +0 -0
  63. {assertpy2-2.12.0 → assertpy2-2.13.0}/docs/assets/diff-equal.png +0 -0
  64. {assertpy2-2.12.0 → assertpy2-2.13.0}/docs/assets/diff-equal.svg +0 -0
  65. {assertpy2-2.12.0 → assertpy2-2.13.0}/docs/assets/diff-gallery.png +0 -0
  66. {assertpy2-2.12.0 → assertpy2-2.13.0}/docs/assets/diff-match.svg +0 -0
  67. {assertpy2-2.12.0 → assertpy2-2.13.0}/docs/assets/diff-sequence.svg +0 -0
  68. {assertpy2-2.12.0 → assertpy2-2.13.0}/docs/assets/diff-set.svg +0 -0
  69. {assertpy2-2.12.0 → assertpy2-2.13.0}/docs/comparison.md +0 -0
  70. {assertpy2-2.12.0 → assertpy2-2.13.0}/docs/data.md +0 -0
  71. {assertpy2-2.12.0 → assertpy2-2.13.0}/docs/errors.md +0 -0
  72. {assertpy2-2.12.0 → assertpy2-2.13.0}/docs/extending.md +0 -0
  73. {assertpy2-2.12.0 → assertpy2-2.13.0}/docs/fluent.md +0 -0
  74. {assertpy2-2.12.0 → assertpy2-2.13.0}/docs/getting-started.md +0 -0
  75. {assertpy2-2.12.0 → assertpy2-2.13.0}/docs/index.md +0 -0
  76. {assertpy2-2.12.0 → assertpy2-2.13.0}/docs/logo-dark.svg +0 -0
  77. {assertpy2-2.12.0 → assertpy2-2.13.0}/docs/logo.svg +0 -0
  78. {assertpy2-2.12.0 → assertpy2-2.13.0}/docs/migration.md +0 -0
  79. {assertpy2-2.12.0 → assertpy2-2.13.0}/docs/reference/async.md +0 -0
  80. {assertpy2-2.12.0 → assertpy2-2.13.0}/docs/reference/bytes.md +0 -0
  81. {assertpy2-2.12.0 → assertpy2-2.13.0}/docs/reference/collections.md +0 -0
  82. {assertpy2-2.12.0 → assertpy2-2.13.0}/docs/reference/containment.md +0 -0
  83. {assertpy2-2.12.0 → assertpy2-2.13.0}/docs/reference/core.md +0 -0
  84. {assertpy2-2.12.0 → assertpy2-2.13.0}/docs/reference/dataframes.md +0 -0
  85. {assertpy2-2.12.0 → assertpy2-2.13.0}/docs/reference/dates.md +0 -0
  86. {assertpy2-2.12.0 → assertpy2-2.13.0}/docs/reference/dicts.md +0 -0
  87. {assertpy2-2.12.0 → assertpy2-2.13.0}/docs/reference/dynamic.md +0 -0
  88. {assertpy2-2.12.0 → assertpy2-2.13.0}/docs/reference/entry-points.md +0 -0
  89. {assertpy2-2.12.0 → assertpy2-2.13.0}/docs/reference/errors.md +0 -0
  90. {assertpy2-2.12.0 → assertpy2-2.13.0}/docs/reference/exceptions.md +0 -0
  91. {assertpy2-2.12.0 → assertpy2-2.13.0}/docs/reference/extracting.md +0 -0
  92. {assertpy2-2.12.0 → assertpy2-2.13.0}/docs/reference/files.md +0 -0
  93. {assertpy2-2.12.0 → assertpy2-2.13.0}/docs/reference/json.md +0 -0
  94. {assertpy2-2.12.0 → assertpy2-2.13.0}/docs/reference/matchers.md +0 -0
  95. {assertpy2-2.12.0 → assertpy2-2.13.0}/docs/reference/numbers.md +0 -0
  96. {assertpy2-2.12.0 → assertpy2-2.13.0}/docs/reference/overview.md +0 -0
  97. {assertpy2-2.12.0 → assertpy2-2.13.0}/docs/reference/strings.md +0 -0
  98. {assertpy2-2.12.0 → assertpy2-2.13.0}/docs/reference/warnings.md +0 -0
  99. {assertpy2-2.12.0 → assertpy2-2.13.0}/docs/stylesheets/extra.css +0 -0
  100. {assertpy2-2.12.0 → assertpy2-2.13.0}/docs/type-safety.md +0 -0
  101. {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_behave_matchers.py +0 -0
  102. {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_bool.py +0 -0
  103. {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_bytes.py +0 -0
  104. {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_callable.py +0 -0
  105. {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_chaining.py +0 -0
  106. {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_class.py +0 -0
  107. {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_collection.py +0 -0
  108. {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_core.py +0 -0
  109. {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_custom_dict.py +0 -0
  110. {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_custom_list.py +0 -0
  111. {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_dataframe.py +0 -0
  112. {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_datetime.py +0 -0
  113. {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_description.py +0 -0
  114. {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_dict.py +0 -0
  115. {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_dict_compare.py +0 -0
  116. {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_dynamic.py +0 -0
  117. {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_errors.py +0 -0
  118. {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_expected_exception.py +0 -0
  119. {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_expected_warning.py +0 -0
  120. {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_extensions.py +0 -0
  121. {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_extracting.py +0 -0
  122. {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_fail.py +0 -0
  123. {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_file.py +0 -0
  124. {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_grouped_soft.py +0 -0
  125. {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_in.py +0 -0
  126. {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_iterable_cluster.py +0 -0
  127. {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_json.py +0 -0
  128. {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_list.py +0 -0
  129. {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_matcher_registry.py +0 -0
  130. {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_mutation_hardening.py +0 -0
  131. {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_namedtuple.py +0 -0
  132. {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_none.py +0 -0
  133. {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_not.py +0 -0
  134. {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_numbers.py +0 -0
  135. {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_overloads.py +0 -0
  136. {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_pipeline.py +0 -0
  137. {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_property_based.py +0 -0
  138. {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_pytest_plugin.py +0 -0
  139. {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_readme.py +0 -0
  140. {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_recursive_assertion.py +0 -0
  141. {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_recursive_compare.py +0 -0
  142. {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_recursive_compare_attrs.py +0 -0
  143. {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_recursive_compare_config.py +0 -0
  144. {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_recursive_compare_pydantic.py +0 -0
  145. {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_regex_groups.py +0 -0
  146. {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_rich_diff.py +0 -0
  147. {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_same_as.py +0 -0
  148. {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_satisfy.py +0 -0
  149. {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_soft.py +0 -0
  150. {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_soft_fail.py +0 -0
  151. {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_string.py +0 -0
  152. {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_traceback.py +0 -0
  153. {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_type.py +0 -0
  154. {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_typing.py +0 -0
  155. {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_warn.py +0 -0
@@ -0,0 +1,31 @@
1
+ version: 2
2
+ updates:
3
+ # Python dependencies via uv: the native `uv` ecosystem updates pyproject.toml
4
+ # AND keeps uv.lock in sync (the older `pip` ecosystem left the lock untouched).
5
+ - package-ecosystem: uv
6
+ directory: /
7
+ schedule:
8
+ interval: weekly
9
+ cooldown:
10
+ default-days: 7
11
+ semver-major-days: 7
12
+ commit-message:
13
+ prefix: "chore(deps)"
14
+ groups:
15
+ # One PR/week for all minor+patch bumps; majors stay individual for review.
16
+ python-minor-patch:
17
+ patterns: ["*"]
18
+ update-types: ["minor", "patch"]
19
+
20
+ - package-ecosystem: github-actions
21
+ directory: /
22
+ schedule:
23
+ interval: weekly
24
+ cooldown:
25
+ default-days: 7
26
+ commit-message:
27
+ prefix: "ci(deps)"
28
+ groups:
29
+ actions-minor-patch:
30
+ patterns: ["*"]
31
+ update-types: ["minor", "patch"]
@@ -21,12 +21,17 @@ jobs:
21
21
  os: [ubuntu-latest]
22
22
  python-version: ["3.10", "3.11", "3.12", "3.13", "3.14", "3.15"]
23
23
  include:
24
+ # the Codecov cell also installs every optional integration (pandas/polars/numpy extras plus
25
+ # pydantic/attrs/requests), so the tests the other minimal-env cells skip run here for real
26
+ - os: ubuntu-latest
27
+ python-version: "3.14"
28
+ full-deps: true
24
29
  - os: windows-latest
25
30
  python-version: "3.14"
26
31
  - os: macos-latest
27
32
  python-version: "3.14"
28
33
  steps:
29
- - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
34
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
30
35
  with:
31
36
  persist-credentials: false
32
37
 
@@ -34,14 +39,19 @@ jobs:
34
39
  uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
35
40
 
36
41
  - name: Set up Python ${{ matrix.python-version }}
37
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
42
+ uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
38
43
  with:
39
44
  python-version: ${{ matrix.python-version }}
40
45
  allow-prereleases: true
41
46
 
42
47
  - name: Install dependencies
48
+ if: '!matrix.full-deps'
43
49
  run: uv sync --extra json
44
50
 
51
+ - name: Install dependencies (all integrations)
52
+ if: matrix.full-deps
53
+ run: uv sync --extra json --extra data --group integrations
54
+
45
55
  - name: Test with coverage
46
56
  run: uv run pytest -v --cov=assertpy2 --cov-report=term-missing --cov-report=xml tests
47
57
 
@@ -55,7 +65,7 @@ jobs:
55
65
  lint:
56
66
  runs-on: ubuntu-latest
57
67
  steps:
58
- - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
68
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
59
69
  with:
60
70
  persist-credentials: false
61
71
 
@@ -63,7 +73,7 @@ jobs:
63
73
  uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
64
74
 
65
75
  - name: Set up Python
66
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
76
+ uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
67
77
  with:
68
78
  python-version: "3.14"
69
79
 
@@ -84,3 +94,11 @@ jobs:
84
94
 
85
95
  - name: Type check (pyright, public typing surface)
86
96
  run: uv run pyright tests/test_typing.py
97
+
98
+ ci-ok:
99
+ needs: [test, lint]
100
+ if: always()
101
+ runs-on: ubuntu-latest
102
+ steps:
103
+ - name: Verify required jobs succeeded
104
+ run: '[ "${{ needs.test.result }}" = "success" ] && [ "${{ needs.lint.result }}" = "success" ]'
@@ -17,7 +17,7 @@ jobs:
17
17
  permissions:
18
18
  security-events: write
19
19
  steps:
20
- - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
20
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
21
21
  with:
22
22
  persist-credentials: false
23
23
  - uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
@@ -17,7 +17,7 @@ jobs:
17
17
  build:
18
18
  runs-on: ubuntu-latest
19
19
  steps:
20
- - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
20
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
21
21
  with:
22
22
  persist-credentials: false
23
23
 
@@ -25,7 +25,7 @@ jobs:
25
25
  uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
26
26
 
27
27
  - name: Set up Python
28
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
28
+ uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
29
29
  with:
30
30
  python-version: "3.14"
31
31
 
@@ -21,7 +21,7 @@ jobs:
21
21
  matrix:
22
22
  module: [base, numeric, string, contains, collection, dict, date, matchers, helpers, _compare, dataframe, json_mixin, snapshot, bytes_mixin, dynamic, extracting, warning, exception, async_assertions, file, behave_matchers]
23
23
  steps:
24
- - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
24
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
25
25
  with:
26
26
  persist-credentials: false
27
27
 
@@ -29,7 +29,7 @@ jobs:
29
29
  uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
30
30
 
31
31
  - name: Set up Python
32
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
32
+ uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
33
33
  with:
34
34
  python-version: "3.14"
35
35
 
@@ -16,7 +16,7 @@ jobs:
16
16
  contents: write
17
17
  attestations: write
18
18
  steps:
19
- - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
19
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
20
20
  with:
21
21
  persist-credentials: false
22
22
 
@@ -26,7 +26,7 @@ jobs:
26
26
  enable-cache: false
27
27
 
28
28
  - name: Set up Python
29
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
29
+ uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
30
30
  with:
31
31
  python-version: "3.15"
32
32
  allow-prereleases: true
@@ -16,7 +16,7 @@ jobs:
16
16
  security-events: write
17
17
  id-token: write
18
18
  steps:
19
- - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
19
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
20
20
  with:
21
21
  persist-credentials: false
22
22
  - uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3
@@ -19,7 +19,7 @@ jobs:
19
19
  permissions:
20
20
  security-events: write
21
21
  steps:
22
- - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
22
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
23
23
  with:
24
24
  persist-credentials: false
25
- - uses: zizmorcore/zizmor-action@5f14fd08f7cf1cb1609c1e344975f152c7ee938d # v0.5.6
25
+ - uses: zizmorcore/zizmor-action@192e21d79ab29983730a13d1382995c2307fbcaa # v0.5.7
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: assertpy2
3
- Version: 2.12.0
3
+ Version: 2.13.0
4
4
  Summary: Fluent assertion library for Python with composable matchers, structural matching, and full type safety
5
5
  Project-URL: Homepage, https://github.com/Solganis/assertpy2
6
6
  Project-URL: Repository, https://github.com/Solganis/assertpy2
@@ -9,7 +9,7 @@ Author-email: Justin Shacklette <justin@saturnboy.com>
9
9
  Maintainer-email: Solganis <solganis.dev@gmail.com>
10
10
  License-Expression: BSD-3-Clause
11
11
  License-File: LICENSE
12
- Keywords: assert,assert_that,assertion,assertthat,pytest,test,testing,unittest
12
+ Keywords: assert,assert_that,assertion,assertpy,assertthat,fluent,matchers,pytest,test,testing,type-safe,unittest
13
13
  Classifier: Development Status :: 5 - Production/Stable
14
14
  Classifier: Intended Audience :: Developers
15
15
  Classifier: License :: OSI Approved :: BSD License
@@ -16,6 +16,7 @@ from .async_assertions import AsyncAssertionBuilder
16
16
  from .errors import AssertionFailure, DiffEntry, DiffResult
17
17
  from .file import contents_of
18
18
  from .matchers import Matcher, clear_custom_matchers, match, register_matcher, unregister_matcher
19
+ from .snapshot import SnapshotCreatedWarning
19
20
 
20
21
  __all__ = [
21
22
  "AssertionFailure",
@@ -24,6 +25,7 @@ __all__ = [
24
25
  "DiffResult",
25
26
  "Matcher",
26
27
  "NegatedBuilder",
28
+ "SnapshotCreatedWarning",
27
29
  "SoftAssertionCollector",
28
30
  "WarningLoggingAdapter",
29
31
  "__version__",
@@ -13,12 +13,15 @@ express to the type checker.
13
13
 
14
14
  from __future__ import annotations
15
15
 
16
+ import dataclasses
16
17
  import math
17
18
  import numbers
18
19
  import re
19
20
  from dataclasses import dataclass
20
21
  from typing import TYPE_CHECKING
21
22
 
23
+ from ._introspection import is_mapping_like, is_model_dump_object
24
+
22
25
  if TYPE_CHECKING:
23
26
  from collections.abc import Callable
24
27
 
@@ -59,6 +62,89 @@ def _build_compare_config(tolerance, comparators) -> _CompareConfig | None:
59
62
  return _CompareConfig(tolerance=tolerance, comparators=comparators)
60
63
 
61
64
 
65
+ def _ambiguous_array_operand(value: object, other: object) -> object | None:
66
+ """Return the array/frame-like operand whose ``==`` has no single truth value, else ``None``.
67
+
68
+ numpy/pandas/polars containers expose ``__array__`` and compare element-wise, so ``bool(a == b)``
69
+ raises rather than yielding one bool (and a ``DataFrame`` also quacks dict-like, which would otherwise
70
+ mis-dispatch the comparison). The ``__array__`` gate keeps the extra comparison off the hot path; the
71
+ truth test is actually attempted, so 0-d / scalar array values (which *are* truth-testable) pass
72
+ through unchanged.
73
+ """
74
+ for candidate, counterpart in ((value, other), (other, value)):
75
+ if hasattr(candidate, "__array__"):
76
+ try:
77
+ bool(candidate == counterpart)
78
+ except (ValueError, TypeError):
79
+ return candidate
80
+ return None
81
+
82
+
83
+ def _array_equality_error(method: str, operand: object) -> TypeError:
84
+ """Build the actionable error raised when ``method`` is given an element-wise array/frame-like."""
85
+ return TypeError(
86
+ f"{method}() cannot directly compare <{type(operand).__name__}>: its '==' is element-wise and has"
87
+ " no single truth value. Compare the value's own equality (e.g."
88
+ " assert_that(actual.equals(expected)).is_true()), assert on extracted scalars (columns, shape,"
89
+ " length), or use satisfies(...) with an explicit predicate."
90
+ )
91
+
92
+
93
+ def _find_ambiguous_operand(actual, expected, _seen=None):
94
+ """Locate the array/frame-like member that broke a comparison, walking the diff engine's containers.
95
+
96
+ Cold error-path only; ``None`` means the error was not array-caused and must be re-raised unchanged.
97
+ """
98
+ if _seen is None:
99
+ _seen = set()
100
+ pair = (id(actual), id(expected))
101
+ if pair in _seen:
102
+ return None
103
+ _seen = _seen | {pair}
104
+ operand = _ambiguous_array_operand(actual, expected)
105
+ if operand is not None:
106
+ return operand
107
+ if is_mapping_like(actual) and is_mapping_like(expected):
108
+ expected_keys = set(expected)
109
+ for key in actual:
110
+ if key in expected_keys:
111
+ found = _find_ambiguous_operand(actual[key], expected[key], _seen)
112
+ if found is not None:
113
+ return found
114
+ return None
115
+ if (
116
+ dataclasses.is_dataclass(actual)
117
+ and not isinstance(actual, type)
118
+ and dataclasses.is_dataclass(expected)
119
+ and not isinstance(expected, type)
120
+ ):
121
+ for field in dataclasses.fields(actual):
122
+ found = _find_ambiguous_operand(getattr(actual, field.name), getattr(expected, field.name, None), _seen)
123
+ if found is not None:
124
+ return found
125
+ return None
126
+ if is_model_dump_object(actual) and is_model_dump_object(expected):
127
+ return _find_ambiguous_operand(actual.model_dump(), expected.model_dump(), _seen)
128
+ if isinstance(actual, (list, tuple)) and isinstance(expected, (list, tuple)):
129
+ for actual_item, expected_item in zip(actual, expected, strict=False):
130
+ found = _find_ambiguous_operand(actual_item, expected_item, _seen)
131
+ if found is not None:
132
+ return found
133
+ return None
134
+
135
+
136
+ def _guarded_not_equal(actual, expected) -> bool:
137
+ """``bool(actual != expected)``, converting the ambiguity raised from *inside* a container's ``==``
138
+ (where the top-level operand gate cannot see the array member) into the actionable ``TypeError``."""
139
+ try:
140
+ return bool(actual != expected)
141
+ except (ValueError, TypeError) as error:
142
+ operand = _find_ambiguous_operand(actual, expected)
143
+ if operand is None:
144
+ raise
145
+ raise _array_equality_error("is_equal_to", operand) from error
146
+
147
+
62
148
  def _is_real_number(value) -> bool:
63
149
  """Return whether ``value`` is a real number eligible for tolerance (excludes ``bool`` and ``complex``).
64
150
 
@@ -114,7 +200,7 @@ def _node_decision(actual, expected, config: _CompareConfig | None, *, field=Non
114
200
  return "equal" if comparator(actual, expected) else "leaf"
115
201
  if config.tolerance is not None and _is_real_number(actual) and _is_real_number(expected):
116
202
  return "equal" if _within_tolerance(actual, expected, config.tolerance) else "leaf"
117
- return "recurse" if actual != expected else "equal"
203
+ return "recurse" if _guarded_not_equal(actual, expected) else "equal"
118
204
 
119
205
 
120
206
  def _spec_matches(key, value, specs) -> bool:
@@ -48,7 +48,16 @@ if TYPE_CHECKING:
48
48
  def is_in(self, *items: object) -> Self: ...
49
49
  def is_not_in(self, *items: object) -> Self: ...
50
50
  # SnapshotMixin
51
- def snapshot(self, id: str | None = ..., path: str = ...) -> Self: ... # noqa: A002 # mirrors public snapshot() parameter
51
+ def snapshot(
52
+ self,
53
+ id: str | None = ..., # noqa: A002 # mirrors public snapshot() parameter
54
+ path: str = ...,
55
+ *,
56
+ ignore: _KeySpecs | None = ...,
57
+ include: _KeySpecs | None = ...,
58
+ tolerance: float | None = ...,
59
+ comparators: dict[object, Callable[..., bool]] | None = ...,
60
+ ) -> Self: ...
52
61
  # NegatedBuilder
53
62
  @property
54
63
  def not_(self) -> NegatedBuilder: ...
@@ -258,4 +267,10 @@ if TYPE_CHECKING:
258
267
  def does_not_warn(self, warning: type[Warning] = ...) -> Self: ...
259
268
  # captured value (exception or warning message) is a str; returned() pivots to the call result
260
269
  def when_called_with(self, *some_args: object, **some_kwargs: object) -> _InvokedAssertion: ...
261
- def eventually(self, *, timeout: float = ..., interval: float = ...) -> AsyncAssertionBuilder: ...
270
+ def eventually(
271
+ self,
272
+ *,
273
+ timeout: float = ...,
274
+ interval: float = ...,
275
+ ignoring: type[Exception] | tuple[type[Exception], ...] = ...,
276
+ ) -> AsyncAssertionBuilder: ...
@@ -29,7 +29,7 @@ if TYPE_CHECKING:
29
29
  _StringAssertion,
30
30
  )
31
31
 
32
- from .async_assertions import AsyncAssertionBuilder
32
+ from .async_assertions import AsyncAssertionBuilder, _normalize_ignoring
33
33
  from .base import BaseMixin
34
34
  from .bytes_mixin import BytesMixin
35
35
  from .collection import CollectionMixin
@@ -49,7 +49,7 @@ from .snapshot import SnapshotMixin
49
49
  from .string import StringMixin
50
50
  from .warning import WarningMixin
51
51
 
52
- __version__ = "2.12.0"
52
+ __version__ = "2.13.0"
53
53
 
54
54
  __tracebackhide__ = True # clean tracebacks via py.test integration
55
55
  contextlib.__tracebackhide__ = True # ty: ignore[unresolved-attribute] # pytest monkey-patch
@@ -631,7 +631,13 @@ class AssertionBuilder(
631
631
  raise AssertionFailure(out, actual=actual, expected=expected, diff=diff)
632
632
  raise AssertionError(out)
633
633
 
634
- def eventually(self, *, timeout: float = 5.0, interval: float = 0.5) -> AsyncAssertionBuilder:
634
+ def eventually(
635
+ self,
636
+ *,
637
+ timeout: float = 5.0,
638
+ interval: float = 0.5,
639
+ ignoring: type[Exception] | tuple[type[Exception], ...] = (),
640
+ ) -> AsyncAssertionBuilder:
635
641
  """Switch to async polling mode for eventual-consistency assertions.
636
642
 
637
643
  The current ``val`` must be a callable (sync or async). Returns an
@@ -639,9 +645,16 @@ class AssertionBuilder(
639
645
  methods are coroutines that poll ``val()`` until the assertion passes or
640
646
  ``timeout`` expires.
641
647
 
648
+ By default only a failing assertion is retried: any exception raised by ``val()`` itself
649
+ propagates immediately. A probe that signals "not ready yet" by raising (a connection refused
650
+ while a service boots, a record not yet visible) can be retried too by listing those exception
651
+ types in ``ignoring``.
652
+
642
653
  Args:
643
654
  timeout: maximum seconds to keep retrying (default ``5.0``)
644
655
  interval: seconds between retries (default ``0.5``)
656
+ ignoring: an ``Exception`` subclass (or tuple of them) the polling loop retries instead of
657
+ propagating (default: none)
645
658
 
646
659
  Examples:
647
660
  Usage:
@@ -659,11 +672,19 @@ class AssertionBuilder(
659
672
  assert_that(get_count).eventually(timeout=2).is_equal_to(3)
660
673
  )
661
674
 
675
+ Retry a probe that raises while the system under test is not ready yet:
676
+
677
+ await assert_that(get_order).eventually(timeout=10, ignoring=ConnectionError).has_status("PAID")
678
+
679
+ # or configure fluently on the returned builder
680
+ await assert_that(get_order).eventually().within(10).ignoring(ConnectionError).has_status("PAID")
681
+
662
682
  Returns:
663
683
  AsyncAssertionBuilder: an async builder whose assertion methods are awaitable
664
684
 
665
685
  Raises:
666
- TypeError: if ``val`` is not callable
686
+ TypeError: if ``val`` is not callable, or if ``ignoring`` contains anything that is not an
687
+ ``Exception`` subclass
667
688
  """
668
689
  if not callable(self.val):
669
690
  raise TypeError("val must be callable when using eventually()")
@@ -673,4 +694,5 @@ class AssertionBuilder(
673
694
  description=self.description,
674
695
  timeout=timeout,
675
696
  interval=interval,
697
+ ignoring=_normalize_ignoring(ignoring),
676
698
  )
@@ -12,6 +12,19 @@ if TYPE_CHECKING:
12
12
  __tracebackhide__ = True
13
13
 
14
14
 
15
+ def _normalize_ignoring(ignoring) -> tuple[type[Exception], ...]:
16
+ """Normalize an ``ignoring`` spec (one exception type or a tuple of them) to a validated tuple.
17
+
18
+ Only ``Exception`` subclasses are accepted, so ``BaseException``-only classes such as
19
+ ``KeyboardInterrupt`` can never be swallowed by the polling loop.
20
+ """
21
+ exceptions = ignoring if isinstance(ignoring, tuple) else (ignoring,)
22
+ for exception_type in exceptions:
23
+ if not (isinstance(exception_type, type) and issubclass(exception_type, Exception)):
24
+ raise TypeError("given ignoring arg must be an Exception subclass or a tuple of Exception subclasses")
25
+ return exceptions
26
+
27
+
15
28
  class AsyncAssertionBuilder:
16
29
  """Async assertion builder that polls a callable until an assertion passes or timeout expires.
17
30
 
@@ -23,6 +36,7 @@ class AsyncAssertionBuilder:
23
36
  description: optional error description forwarded to the builder
24
37
  timeout: maximum seconds to keep retrying
25
38
  interval: seconds between retries
39
+ ignoring: exception types the polling loop retries instead of propagating
26
40
  """
27
41
 
28
42
  def __init__(
@@ -33,12 +47,14 @@ class AsyncAssertionBuilder:
33
47
  description: str = "",
34
48
  timeout: float = 5.0,
35
49
  interval: float = 0.5,
50
+ ignoring: tuple[type[Exception], ...] = (),
36
51
  ):
37
52
  self._func = func
38
53
  self._builder_func = builder_func
39
54
  self._description = description
40
55
  self._timeout = timeout
41
56
  self._interval = interval
57
+ self._ignoring = ignoring
42
58
 
43
59
  def within(self, timeout: float) -> Self:
44
60
  """Override the timeout (in seconds)."""
@@ -50,6 +66,20 @@ class AsyncAssertionBuilder:
50
66
  self._interval = interval
51
67
  return self
52
68
 
69
+ def ignoring(self, *exceptions: type[Exception]) -> Self:
70
+ """Replace the exception types the polling loop retries instead of propagating.
71
+
72
+ Examples:
73
+ Usage:
74
+
75
+ await assert_that(get_order).eventually().within(10).ignoring(ConnectionError).has_status("PAID")
76
+
77
+ Raises:
78
+ TypeError: if any argument is not an ``Exception`` subclass
79
+ """
80
+ self._ignoring = _normalize_ignoring(exceptions)
81
+ return self
82
+
53
83
  def __getattr__(self, name: str):
54
84
  if name.startswith("_"):
55
85
  raise AttributeError(name)
@@ -58,7 +88,7 @@ class AsyncAssertionBuilder:
58
88
  async def _poll():
59
89
  loop = asyncio.get_running_loop()
60
90
  deadline = loop.time() + self._timeout
61
- last_error: AssertionError | None = None
91
+ last_error: Exception | None = None
62
92
  while True:
63
93
  try:
64
94
  val = self._func()
@@ -68,12 +98,13 @@ class AsyncAssertionBuilder:
68
98
  method = getattr(builder, name)
69
99
  method(*args, **kwargs)
70
100
  return builder
71
- except AssertionError as exc: # noqa: PERF203 # retry-on-failure needs the try/except per poll iteration
101
+ except (AssertionError, *self._ignoring) as exc: # noqa: PERF203 # retry-on-failure needs the try/except per poll iteration
72
102
  last_error = exc
73
103
  if loop.time() >= deadline:
104
+ # repr for ignored exceptions: their type name is the diagnostic, str() may be empty
105
+ failure = str(last_error) if isinstance(last_error, AssertionError) else repr(last_error)
74
106
  raise AssertionError(
75
- f"Expected condition not met after {self._timeout:.1f} seconds."
76
- f" Last failure: {last_error}"
107
+ f"Expected condition not met after {self._timeout:.1f} seconds. Last failure: {failure}"
77
108
  ) from last_error
78
109
  await asyncio.sleep(self._interval)
79
110
 
@@ -4,7 +4,13 @@ import collections.abc
4
4
  import dataclasses
5
5
  from typing import TYPE_CHECKING, Any, Final
6
6
 
7
- from ._compare import _build_compare_config, _node_decision
7
+ from ._compare import (
8
+ _ambiguous_array_operand,
9
+ _array_equality_error,
10
+ _build_compare_config,
11
+ _guarded_not_equal,
12
+ _node_decision,
13
+ )
8
14
  from ._introspection import is_mapping_like, is_model_dump_object, is_namedtuple
9
15
  from ._mixin_base import _MixinBase
10
16
  from .errors import DiffEntry, DiffResult
@@ -20,34 +26,6 @@ __tracebackhide__ = True
20
26
  _SENTINEL: Final = object()
21
27
 
22
28
 
23
- def _ambiguous_array_operand(value: object, other: object) -> object | None:
24
- """Return the array/frame-like operand whose ``==`` has no single truth value, else ``None``.
25
-
26
- numpy/pandas/polars containers expose ``__array__`` and compare element-wise, so ``bool(a == b)``
27
- raises rather than yielding one bool (and a ``DataFrame`` also quacks dict-like, which would otherwise
28
- mis-dispatch the comparison). The ``__array__`` gate keeps the extra comparison off the hot path; the
29
- truth test is actually attempted, so 0-d / scalar array values (which *are* truth-testable) pass
30
- through unchanged.
31
- """
32
- for candidate, counterpart in ((value, other), (other, value)):
33
- if hasattr(candidate, "__array__"):
34
- try:
35
- bool(candidate == counterpart)
36
- except (ValueError, TypeError):
37
- return candidate
38
- return None
39
-
40
-
41
- def _array_equality_error(method: str, operand: object) -> TypeError:
42
- """Build the actionable error raised when ``method`` is given an element-wise array/frame-like."""
43
- return TypeError(
44
- f"{method}() cannot directly compare <{type(operand).__name__}>: its '==' is element-wise and has"
45
- " no single truth value. Compare the value's own equality (e.g."
46
- " assert_that(actual.equals(expected)).is_true()), assert on extracted scalars (columns, shape,"
47
- " length), or use satisfies(...) with an explicit predicate."
48
- )
49
-
50
-
51
29
  class BaseMixin(_MixinBase):
52
30
  """Base mixin."""
53
31
 
@@ -149,7 +127,7 @@ class BaseMixin(_MixinBase):
149
127
 
150
128
  # by type, or by field name (field name wins over type)
151
129
  assert_that(actual).is_equal_to(expected, comparators={float: lambda a, e: round(a, 2) == round(e, 2)})
152
- assert_that(actual).is_equal_to(expected, comparators={"created_at": lambda a, e: True})
130
+ assert_that(actual).is_equal_to(expected, comparators={"name": lambda a, e: a.lower() == e.lower()})
153
131
 
154
132
  Ignore fields by regex or by type:
155
133
 
@@ -170,9 +148,10 @@ class BaseMixin(_MixinBase):
170
148
  AssertionError: if actual is **not** equal to expected
171
149
  TypeError: if ``ignore``/``include`` is a one-shot or otherwise unsupported iterable, or is
172
150
  used on a value that is neither dict-like nor has introspectable fields; if ``tolerance`` is
173
- not a real number or ``comparators`` is not a dict of callables; or if val or other is an
174
- element-wise array/frame-like (numpy/pandas/polars) whose ``==`` has no single truth value
175
- (compare the value's own equality, e.g. ``actual.equals(expected)``, instead)
151
+ not a real number or ``comparators`` is not a dict of callables; or if val or other is (or
152
+ contains, at any nesting depth) an element-wise array/frame-like (numpy/pandas/polars) whose
153
+ ``==`` has no single truth value (compare the value's own equality, e.g.
154
+ ``actual.equals(expected)``, instead)
176
155
  ValueError: if ``tolerance`` is ``NaN`` or negative
177
156
 
178
157
  Tip:
@@ -219,7 +198,7 @@ class BaseMixin(_MixinBase):
219
198
  diff=diff,
220
199
  )
221
200
  else:
222
- if self.val != other:
201
+ if _guarded_not_equal(self.val, other):
223
202
  diff = self._build_equality_diff(self.val, other)
224
203
  return self.error(
225
204
  f"Expected <{self.val}> to be equal to <{other}>, but was not.",
@@ -7,7 +7,7 @@ import re
7
7
 
8
8
  from assertpy2.errors import DiffResult
9
9
 
10
- from ._compare import _CompareConfig, _node_decision, _spec_matches
10
+ from ._compare import _CompareConfig, _guarded_not_equal, _node_decision, _spec_matches
11
11
  from ._introspection import is_attrs_instance, is_model_dump_object, is_namedtuple
12
12
  from ._mixin_base import _MixinBase
13
13
 
@@ -138,7 +138,7 @@ class HelpersMixin(_MixinBase):
138
138
  _seen = _seen | {pair}
139
139
 
140
140
  if not (ignore or include or config is not None):
141
- return val != other
141
+ return _guarded_not_equal(val, other)
142
142
 
143
143
  ignores = [] # bound for the nested-recursion use below; only read when ``ignore`` is set
144
144
  if ignore or include:
@@ -210,7 +210,7 @@ class HelpersMixin(_MixinBase):
210
210
  )
211
211
  if subdicts_not_equal:
212
212
  return True
213
- elif val[key] != other[key]:
213
+ elif _guarded_not_equal(val[key], other[key]):
214
214
  return True
215
215
  return False
216
216