assertpy2 2.19.0__tar.gz → 2.20.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (200) hide show
  1. {assertpy2-2.19.0 → assertpy2-2.20.1}/.github/workflows/ci.yml +59 -3
  2. {assertpy2-2.19.0 → assertpy2-2.20.1}/.github/workflows/codeql.yml +2 -2
  3. {assertpy2-2.19.0 → assertpy2-2.20.1}/.github/workflows/codspeed.yml +1 -1
  4. assertpy2-2.20.1/.github/workflows/mutation.yml +78 -0
  5. {assertpy2-2.19.0 → assertpy2-2.20.1}/.github/workflows/publish.yml +1 -1
  6. {assertpy2-2.19.0 → assertpy2-2.20.1}/.github/workflows/scorecard.yml +1 -1
  7. {assertpy2-2.19.0 → assertpy2-2.20.1}/.github/workflows/zizmor.yml +1 -1
  8. assertpy2-2.20.1/CONTRIBUTING.md +67 -0
  9. {assertpy2-2.19.0 → assertpy2-2.20.1}/PKG-INFO +3 -2
  10. {assertpy2-2.19.0 → assertpy2-2.20.1}/README.md +2 -1
  11. {assertpy2-2.19.0 → assertpy2-2.20.1}/assertpy2/__init__.py +25 -2
  12. {assertpy2-2.19.0 → assertpy2-2.20.1}/assertpy2/_engine/_compare.py +27 -0
  13. {assertpy2-2.19.0 → assertpy2-2.20.1}/assertpy2/_engine/_contract.py +2 -0
  14. {assertpy2-2.19.0 → assertpy2-2.20.1}/assertpy2/_engine/_diff.py +121 -84
  15. {assertpy2-2.19.0 → assertpy2-2.20.1}/assertpy2/_engine/_introspection.py +51 -2
  16. {assertpy2-2.19.0 → assertpy2-2.20.1}/assertpy2/_engine/_mixin_base.py +5 -0
  17. assertpy2-2.20.1/assertpy2/_engine/_path.py +100 -0
  18. {assertpy2-2.19.0 → assertpy2-2.20.1}/assertpy2/_engine/_typing.py +25 -3
  19. assertpy2-2.20.1/assertpy2/_hints.py +325 -0
  20. {assertpy2-2.19.0 → assertpy2-2.20.1}/assertpy2/_inline.py +7 -1
  21. {assertpy2-2.19.0 → assertpy2-2.20.1}/assertpy2/_matcher_impls.py +173 -37
  22. {assertpy2-2.19.0 → assertpy2-2.20.1}/assertpy2/_satisfies.py +58 -26
  23. {assertpy2-2.19.0 → assertpy2-2.20.1}/assertpy2/assertpy.py +347 -90
  24. {assertpy2-2.19.0 → assertpy2-2.20.1}/assertpy2/async_assertions.py +10 -4
  25. {assertpy2-2.19.0 → assertpy2-2.20.1}/assertpy2/base.py +15 -6
  26. {assertpy2-2.19.0 → assertpy2-2.20.1}/assertpy2/bytes_mixin.py +15 -6
  27. {assertpy2-2.19.0 → assertpy2-2.20.1}/assertpy2/collection.py +7 -3
  28. {assertpy2-2.19.0 → assertpy2-2.20.1}/assertpy2/contains.py +92 -49
  29. {assertpy2-2.19.0 → assertpy2-2.20.1}/assertpy2/dynamic.py +16 -2
  30. {assertpy2-2.19.0 → assertpy2-2.20.1}/assertpy2/errors.py +179 -9
  31. {assertpy2-2.19.0 → assertpy2-2.20.1}/assertpy2/extracting.py +4 -0
  32. {assertpy2-2.19.0 → assertpy2-2.20.1}/assertpy2/helpers.py +84 -17
  33. {assertpy2-2.19.0 → assertpy2-2.20.1}/assertpy2/matchers.py +80 -5
  34. assertpy2-2.20.1/assertpy2/outcome.py +118 -0
  35. {assertpy2-2.19.0 → assertpy2-2.20.1}/assertpy2/pytest_plugin.py +121 -38
  36. {assertpy2-2.19.0 → assertpy2-2.20.1}/assertpy2/snapshot.py +70 -32
  37. {assertpy2-2.19.0 → assertpy2-2.20.1}/assertpy2/string.py +19 -0
  38. {assertpy2-2.19.0 → assertpy2-2.20.1}/benchmarks/test_perf.py +42 -0
  39. assertpy2-2.20.1/docs/concepts/stability.md +63 -0
  40. {assertpy2-2.19.0 → assertpy2-2.20.1}/docs/concepts/type-safety.md +49 -4
  41. {assertpy2-2.19.0 → assertpy2-2.20.1}/docs/extending/custom-assertions.md +9 -0
  42. {assertpy2-2.19.0 → assertpy2-2.20.1}/docs/extending/integrations.md +43 -3
  43. {assertpy2-2.19.0 → assertpy2-2.20.1}/docs/getting-started/comparison.md +4 -2
  44. {assertpy2-2.19.0 → assertpy2-2.20.1}/docs/getting-started/migration.md +2 -1
  45. {assertpy2-2.19.0 → assertpy2-2.20.1}/docs/getting-started/quickstart.md +6 -2
  46. {assertpy2-2.19.0 → assertpy2-2.20.1}/docs/guides/assertions.md +26 -2
  47. {assertpy2-2.19.0 → assertpy2-2.20.1}/docs/guides/errors.md +253 -9
  48. {assertpy2-2.19.0 → assertpy2-2.20.1}/docs/guides/fluent.md +8 -3
  49. {assertpy2-2.19.0 → assertpy2-2.20.1}/docs/guides/matchers.md +78 -4
  50. {assertpy2-2.19.0 → assertpy2-2.20.1}/docs/guides/testing.md +12 -3
  51. {assertpy2-2.19.0 → assertpy2-2.20.1}/docs/reference/core.md +5 -0
  52. {assertpy2-2.19.0 → assertpy2-2.20.1}/docs/reference/entry-points.md +18 -0
  53. {assertpy2-2.19.0 → assertpy2-2.20.1}/docs/reference/errors.md +6 -0
  54. assertpy2-2.20.1/docs/reference/matchers.md +21 -0
  55. {assertpy2-2.19.0 → assertpy2-2.20.1}/mkdocs.yml +1 -0
  56. {assertpy2-2.19.0 → assertpy2-2.20.1}/pyproject.toml +63 -2
  57. assertpy2-2.20.1/scripts/mutation_report.py +142 -0
  58. assertpy2-2.20.1/tests/docs_fixtures.py +218 -0
  59. assertpy2-2.20.1/tests/pyright_baseline.py +69 -0
  60. assertpy2-2.20.1/tests/test_api_vocabulary.py +153 -0
  61. {assertpy2-2.19.0 → assertpy2-2.20.1}/tests/test_attrs_support.py +1 -1
  62. {assertpy2-2.19.0 → assertpy2-2.20.1}/tests/test_bytes.py +54 -3
  63. assertpy2-2.20.1/tests/test_check.py +121 -0
  64. {assertpy2-2.19.0 → assertpy2-2.20.1}/tests/test_collection.py +71 -1
  65. {assertpy2-2.19.0 → assertpy2-2.20.1}/tests/test_core.py +3 -1
  66. {assertpy2-2.19.0 → assertpy2-2.20.1}/tests/test_datetime.py +25 -0
  67. {assertpy2-2.19.0 → assertpy2-2.20.1}/tests/test_dict_compare.py +100 -10
  68. assertpy2-2.20.1/tests/test_docs_examples.py +91 -0
  69. assertpy2-2.20.1/tests/test_docs_typing.py +98 -0
  70. {assertpy2-2.19.0 → assertpy2-2.20.1}/tests/test_equals.py +62 -0
  71. {assertpy2-2.19.0 → assertpy2-2.20.1}/tests/test_errors.py +146 -9
  72. {assertpy2-2.19.0 → assertpy2-2.20.1}/tests/test_extensions.py +90 -13
  73. {assertpy2-2.19.0 → assertpy2-2.20.1}/tests/test_extracting.py +21 -0
  74. assertpy2-2.20.1/tests/test_hints.py +417 -0
  75. {assertpy2-2.19.0 → assertpy2-2.20.1}/tests/test_inline_record.py +58 -0
  76. assertpy2-2.20.1/tests/test_inline_snapshot.py +199 -0
  77. {assertpy2-2.19.0 → assertpy2-2.20.1}/tests/test_list.py +114 -0
  78. assertpy2-2.20.1/tests/test_match_result.py +134 -0
  79. assertpy2-2.20.1/tests/test_matcher_parity.py +157 -0
  80. {assertpy2-2.19.0 → assertpy2-2.20.1}/tests/test_matcher_registry.py +90 -1
  81. {assertpy2-2.19.0 → assertpy2-2.20.1}/tests/test_matchers.py +136 -0
  82. assertpy2-2.20.1/tests/test_message_elision.py +118 -0
  83. {assertpy2-2.19.0 → assertpy2-2.20.1}/tests/test_openapi_contract.py +136 -1
  84. assertpy2-2.20.1/tests/test_optional_integration_contracts.py +87 -0
  85. {assertpy2-2.19.0 → assertpy2-2.20.1}/tests/test_pipeline.py +15 -0
  86. {assertpy2-2.19.0 → assertpy2-2.20.1}/tests/test_property_based.py +185 -17
  87. assertpy2-2.20.1/tests/test_public_surface.py +168 -0
  88. assertpy2-2.20.1/tests/test_pyright_baseline.py +54 -0
  89. {assertpy2-2.19.0 → assertpy2-2.20.1}/tests/test_pytest_plugin.py +308 -17
  90. {assertpy2-2.19.0 → assertpy2-2.20.1}/tests/test_recursive_assertion.py +26 -17
  91. {assertpy2-2.19.0 → assertpy2-2.20.1}/tests/test_recursive_compare_config.py +92 -0
  92. {assertpy2-2.19.0 → assertpy2-2.20.1}/tests/test_rich_diff.py +211 -31
  93. assertpy2-2.20.1/tests/test_satisfy.py +288 -0
  94. {assertpy2-2.19.0 → assertpy2-2.20.1}/tests/test_soft.py +64 -1
  95. {assertpy2-2.19.0 → assertpy2-2.20.1}/tests/test_structural.py +260 -13
  96. {assertpy2-2.19.0 → assertpy2-2.20.1}/tests/test_sync_eventually.py +12 -0
  97. {assertpy2-2.19.0 → assertpy2-2.20.1}/tests/test_typing.py +44 -2
  98. assertpy2-2.20.1/tests/test_typing_claims.py +81 -0
  99. {assertpy2-2.19.0 → assertpy2-2.20.1}/tests/test_vacuity_contract.py +11 -4
  100. {assertpy2-2.19.0 → assertpy2-2.20.1}/tests/test_warn.py +6 -1
  101. {assertpy2-2.19.0 → assertpy2-2.20.1}/uv.lock +271 -934
  102. assertpy2-2.19.0/.github/workflows/mutation.yml +0 -66
  103. assertpy2-2.19.0/CONTRIBUTING.md +0 -50
  104. assertpy2-2.19.0/docs/reference/matchers.md +0 -8
  105. assertpy2-2.19.0/tests/test_docs_examples.py +0 -60
  106. assertpy2-2.19.0/tests/test_docs_typing.py +0 -77
  107. assertpy2-2.19.0/tests/test_inline_snapshot.py +0 -98
  108. assertpy2-2.19.0/tests/test_readme.py +0 -472
  109. assertpy2-2.19.0/tests/test_satisfy.py +0 -115
  110. {assertpy2-2.19.0 → assertpy2-2.20.1}/.codecov.yml +0 -0
  111. {assertpy2-2.19.0 → assertpy2-2.20.1}/.gitattributes +0 -0
  112. {assertpy2-2.19.0 → assertpy2-2.20.1}/.github/dependabot.yml +0 -0
  113. {assertpy2-2.19.0 → assertpy2-2.20.1}/.github/workflows/docs.yml +0 -0
  114. {assertpy2-2.19.0 → assertpy2-2.20.1}/.gitignore +0 -0
  115. {assertpy2-2.19.0 → assertpy2-2.20.1}/LICENSE +0 -0
  116. {assertpy2-2.19.0 → assertpy2-2.20.1}/SECURITY.md +0 -0
  117. {assertpy2-2.19.0 → assertpy2-2.20.1}/assertpy2/_engine/__init__.py +0 -0
  118. {assertpy2-2.19.0 → assertpy2-2.20.1}/assertpy2/_engine/_compat.py +0 -0
  119. {assertpy2-2.19.0 → assertpy2-2.20.1}/assertpy2/_snapshot_codec.py +0 -0
  120. {assertpy2-2.19.0 → assertpy2-2.20.1}/assertpy2/behave_matchers.py +0 -0
  121. {assertpy2-2.19.0 → assertpy2-2.20.1}/assertpy2/dataframe.py +0 -0
  122. {assertpy2-2.19.0 → assertpy2-2.20.1}/assertpy2/date.py +0 -0
  123. {assertpy2-2.19.0 → assertpy2-2.20.1}/assertpy2/dict.py +0 -0
  124. {assertpy2-2.19.0 → assertpy2-2.20.1}/assertpy2/exception.py +0 -0
  125. {assertpy2-2.19.0 → assertpy2-2.20.1}/assertpy2/file.py +0 -0
  126. {assertpy2-2.19.0 → assertpy2-2.20.1}/assertpy2/json_mixin.py +0 -0
  127. {assertpy2-2.19.0 → assertpy2-2.20.1}/assertpy2/numeric.py +0 -0
  128. {assertpy2-2.19.0 → assertpy2-2.20.1}/assertpy2/py.typed +0 -0
  129. {assertpy2-2.19.0 → assertpy2-2.20.1}/assertpy2/warning.py +0 -0
  130. {assertpy2-2.19.0 → assertpy2-2.20.1}/docs/assets/diff-equal.png +0 -0
  131. {assertpy2-2.19.0 → assertpy2-2.20.1}/docs/assets/diff-equal.svg +0 -0
  132. {assertpy2-2.19.0 → assertpy2-2.20.1}/docs/assets/diff-gallery.png +0 -0
  133. {assertpy2-2.19.0 → assertpy2-2.20.1}/docs/assets/diff-match.svg +0 -0
  134. {assertpy2-2.19.0 → assertpy2-2.20.1}/docs/assets/diff-sequence.svg +0 -0
  135. {assertpy2-2.19.0 → assertpy2-2.20.1}/docs/assets/diff-set.svg +0 -0
  136. {assertpy2-2.19.0 → assertpy2-2.20.1}/docs/assets/diff-string.svg +0 -0
  137. {assertpy2-2.19.0 → assertpy2-2.20.1}/docs/guides/data.md +0 -0
  138. {assertpy2-2.19.0 → assertpy2-2.20.1}/docs/index.md +0 -0
  139. {assertpy2-2.19.0 → assertpy2-2.20.1}/docs/logo-dark.svg +0 -0
  140. {assertpy2-2.19.0 → assertpy2-2.20.1}/docs/logo.svg +0 -0
  141. {assertpy2-2.19.0 → assertpy2-2.20.1}/docs/recipes.md +0 -0
  142. {assertpy2-2.19.0 → assertpy2-2.20.1}/docs/reference/async.md +0 -0
  143. {assertpy2-2.19.0 → assertpy2-2.20.1}/docs/reference/bytes.md +0 -0
  144. {assertpy2-2.19.0 → assertpy2-2.20.1}/docs/reference/collections.md +0 -0
  145. {assertpy2-2.19.0 → assertpy2-2.20.1}/docs/reference/containment.md +0 -0
  146. {assertpy2-2.19.0 → assertpy2-2.20.1}/docs/reference/dataframes.md +0 -0
  147. {assertpy2-2.19.0 → assertpy2-2.20.1}/docs/reference/dates.md +0 -0
  148. {assertpy2-2.19.0 → assertpy2-2.20.1}/docs/reference/dicts.md +0 -0
  149. {assertpy2-2.19.0 → assertpy2-2.20.1}/docs/reference/dynamic.md +0 -0
  150. {assertpy2-2.19.0 → assertpy2-2.20.1}/docs/reference/exceptions.md +0 -0
  151. {assertpy2-2.19.0 → assertpy2-2.20.1}/docs/reference/extracting.md +0 -0
  152. {assertpy2-2.19.0 → assertpy2-2.20.1}/docs/reference/files.md +0 -0
  153. {assertpy2-2.19.0 → assertpy2-2.20.1}/docs/reference/json.md +0 -0
  154. {assertpy2-2.19.0 → assertpy2-2.20.1}/docs/reference/numbers.md +0 -0
  155. {assertpy2-2.19.0 → assertpy2-2.20.1}/docs/reference/overview.md +0 -0
  156. {assertpy2-2.19.0 → assertpy2-2.20.1}/docs/reference/snapshots.md +0 -0
  157. {assertpy2-2.19.0 → assertpy2-2.20.1}/docs/reference/strings.md +0 -0
  158. {assertpy2-2.19.0 → assertpy2-2.20.1}/docs/reference/warnings.md +0 -0
  159. {assertpy2-2.19.0 → assertpy2-2.20.1}/docs/stylesheets/extra.css +0 -0
  160. {assertpy2-2.19.0 → assertpy2-2.20.1}/mkdocs_hooks.py +0 -0
  161. {assertpy2-2.19.0 → assertpy2-2.20.1}/tests/conftest.py +0 -0
  162. {assertpy2-2.19.0 → assertpy2-2.20.1}/tests/test_async.py +0 -0
  163. {assertpy2-2.19.0 → assertpy2-2.20.1}/tests/test_behave_matchers.py +0 -0
  164. {assertpy2-2.19.0 → assertpy2-2.20.1}/tests/test_bool.py +0 -0
  165. {assertpy2-2.19.0 → assertpy2-2.20.1}/tests/test_boundary_cases.py +0 -0
  166. {assertpy2-2.19.0 → assertpy2-2.20.1}/tests/test_callable.py +0 -0
  167. {assertpy2-2.19.0 → assertpy2-2.20.1}/tests/test_chaining.py +0 -0
  168. {assertpy2-2.19.0 → assertpy2-2.20.1}/tests/test_class.py +0 -0
  169. {assertpy2-2.19.0 → assertpy2-2.20.1}/tests/test_custom_dict.py +0 -0
  170. {assertpy2-2.19.0 → assertpy2-2.20.1}/tests/test_custom_list.py +0 -0
  171. {assertpy2-2.19.0 → assertpy2-2.20.1}/tests/test_dataframe.py +0 -0
  172. {assertpy2-2.19.0 → assertpy2-2.20.1}/tests/test_description.py +0 -0
  173. {assertpy2-2.19.0 → assertpy2-2.20.1}/tests/test_dict.py +0 -0
  174. {assertpy2-2.19.0 → assertpy2-2.20.1}/tests/test_dynamic.py +0 -0
  175. {assertpy2-2.19.0 → assertpy2-2.20.1}/tests/test_exception_context.py +0 -0
  176. {assertpy2-2.19.0 → assertpy2-2.20.1}/tests/test_expected_exception.py +0 -0
  177. {assertpy2-2.19.0 → assertpy2-2.20.1}/tests/test_expected_warning.py +0 -0
  178. {assertpy2-2.19.0 → assertpy2-2.20.1}/tests/test_fail.py +0 -0
  179. {assertpy2-2.19.0 → assertpy2-2.20.1}/tests/test_file.py +0 -0
  180. {assertpy2-2.19.0 → assertpy2-2.20.1}/tests/test_grouped_soft.py +0 -0
  181. {assertpy2-2.19.0 → assertpy2-2.20.1}/tests/test_in.py +0 -0
  182. {assertpy2-2.19.0 → assertpy2-2.20.1}/tests/test_iterable_cluster.py +0 -0
  183. {assertpy2-2.19.0 → assertpy2-2.20.1}/tests/test_json.py +0 -0
  184. {assertpy2-2.19.0 → assertpy2-2.20.1}/tests/test_namedtuple.py +0 -0
  185. {assertpy2-2.19.0 → assertpy2-2.20.1}/tests/test_none.py +0 -0
  186. {assertpy2-2.19.0 → assertpy2-2.20.1}/tests/test_not.py +0 -0
  187. {assertpy2-2.19.0 → assertpy2-2.20.1}/tests/test_numbers.py +0 -0
  188. {assertpy2-2.19.0 → assertpy2-2.20.1}/tests/test_overloads.py +0 -0
  189. {assertpy2-2.19.0 → assertpy2-2.20.1}/tests/test_property_attrs.py +0 -0
  190. {assertpy2-2.19.0 → assertpy2-2.20.1}/tests/test_protocol_parity.py +0 -0
  191. {assertpy2-2.19.0 → assertpy2-2.20.1}/tests/test_recursive_compare.py +0 -0
  192. {assertpy2-2.19.0 → assertpy2-2.20.1}/tests/test_recursive_compare_attrs.py +0 -0
  193. {assertpy2-2.19.0 → assertpy2-2.20.1}/tests/test_recursive_compare_pydantic.py +0 -0
  194. {assertpy2-2.19.0 → assertpy2-2.20.1}/tests/test_regex_groups.py +0 -0
  195. {assertpy2-2.19.0 → assertpy2-2.20.1}/tests/test_same_as.py +0 -0
  196. {assertpy2-2.19.0 → assertpy2-2.20.1}/tests/test_snapshots.py +0 -0
  197. {assertpy2-2.19.0 → assertpy2-2.20.1}/tests/test_soft_fail.py +0 -0
  198. {assertpy2-2.19.0 → assertpy2-2.20.1}/tests/test_string.py +0 -0
  199. {assertpy2-2.19.0 → assertpy2-2.20.1}/tests/test_traceback.py +0 -0
  200. {assertpy2-2.19.0 → assertpy2-2.20.1}/tests/test_type.py +0 -0
@@ -17,6 +17,10 @@ jobs:
17
17
  test:
18
18
  timeout-minutes: 15
19
19
  runs-on: ${{ matrix.os }}
20
+ # mapped at job level so the upload step can ask whether it has a token before it runs: the secrets
21
+ # context is not readable from a step `if`, and step-level env is not in scope for its own condition
22
+ env:
23
+ CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
20
24
  strategy:
21
25
  matrix:
22
26
  os: [ubuntu-latest]
@@ -72,11 +76,53 @@ jobs:
72
76
  run: uv run pytest tests/test_docs_examples.py
73
77
 
74
78
  - name: Upload coverage to Codecov
75
- if: matrix.python-version == '3.14' && matrix.os == 'ubuntu-latest'
79
+ # Only the full-deps cell. The bare 3.14/ubuntu cell skips every integration test and used
80
+ # to upload too: Codecov merges reports so the number stayed right, but one of the two was
81
+ # always a partial report sent at a 100% target.
82
+ # A run without repository secrets gets an empty token, and Codecov rejects an untokened upload
83
+ # to a protected branch, so the job went red on the upload while every test passed. The gate is
84
+ # the token rather than who opened the pull request: dependabot pushes its branches into this
85
+ # repository, so a fork test reads false for it and it still runs without secrets.
86
+ if: matrix.full-deps && env.CODECOV_TOKEN != ''
76
87
  uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
77
88
  with:
78
89
  token: ${{ secrets.CODECOV_TOKEN }}
79
90
  files: coverage.xml
91
+ # A failed upload is a broken report, not a passing build. The 100% gate above is the hard
92
+ # guard either way, so this cannot turn a real coverage drop into a green run.
93
+ fail_ci_if_error: true
94
+
95
+ optional-integrations:
96
+ # allure and behave get their own job because installing them is incompatible with the coverage
97
+ # gate: with allure present the `except ImportError` fallback in pytest_plugin.py never runs, and
98
+ # without it the import itself never runs. The gating cell keeps the second state, this one holds
99
+ # the first. Everywhere else the two are tested against mocks, which accept any call and would
100
+ # wave through the renamed parameter or moved attribute a Dependabot bump can bring.
101
+ timeout-minutes: 10
102
+ runs-on: ubuntu-latest
103
+ steps:
104
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
105
+ with:
106
+ persist-credentials: false
107
+
108
+ - name: Install uv
109
+ uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
110
+
111
+ - name: Set up Python
112
+ uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
113
+ with:
114
+ python-version: "3.14"
115
+
116
+ - name: Install dependencies
117
+ run: uv sync --extra json --extra allure --extra behave
118
+
119
+ - name: Contract tests against the real libraries
120
+ run: uv run pytest -v tests/test_optional_integration_contracts.py
121
+
122
+ - name: The rest of the suite must also pass with both installed
123
+ # Installing allure flips `_HAS_ALLURE`, so every failing assertion in the suite takes the
124
+ # real attach path. That must not change any other test's outcome.
125
+ run: uv run pytest -q --ignore=tests/test_docs_examples.py tests
80
126
 
81
127
  lint:
82
128
  timeout-minutes: 10
@@ -112,14 +158,24 @@ jobs:
112
158
  - name: Type check (pyright, public typing surface)
113
159
  run: uv run pyright tests/test_typing.py
114
160
 
161
+ # the package itself carries diagnostics that are not defects (values typed `object`, guarded
162
+ # optional imports, deliberate overloads). They are recorded per rule with a reason, so a new
163
+ # one fails here instead of hiding in a count nobody reads.
164
+ - name: Type check (pyright, package baseline)
165
+ run: uv run pytest tests/test_pyright_baseline.py -q
166
+
115
167
  ci-ok:
116
168
  timeout-minutes: 5
117
- needs: [test, lint]
169
+ needs: [test, optional-integrations, lint]
118
170
  if: always()
119
171
  runs-on: ubuntu-latest
120
172
  steps:
121
173
  - name: Verify required jobs succeeded
122
174
  env:
123
175
  TEST_RESULT: ${{ needs.test.result }}
176
+ INTEGRATIONS_RESULT: ${{ needs.optional-integrations.result }}
124
177
  LINT_RESULT: ${{ needs.lint.result }}
125
- run: '[ "$TEST_RESULT" = "success" ] && [ "$LINT_RESULT" = "success" ]'
178
+ run: |
179
+ [ "$TEST_RESULT" = "success" ] &&
180
+ [ "$INTEGRATIONS_RESULT" = "success" ] &&
181
+ [ "$LINT_RESULT" = "success" ]
@@ -21,7 +21,7 @@ jobs:
21
21
  - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
22
22
  with:
23
23
  persist-credentials: false
24
- - uses: github/codeql-action/init@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3
24
+ - uses: github/codeql-action/init@d1ba80a13dd99fba24a470575428917156a28b43 # v4.37.5
25
25
  with:
26
26
  languages: python
27
- - uses: github/codeql-action/analyze@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3
27
+ - uses: github/codeql-action/analyze@d1ba80a13dd99fba24a470575428917156a28b43 # v4.37.5
@@ -35,7 +35,7 @@ jobs:
35
35
  uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
36
36
 
37
37
  - name: Run benchmarks under CodSpeed
38
- uses: CodSpeedHQ/action@f22792bfac16f3e14eb9fbea76f4a48e9cc22b93 # v4.19.1
38
+ uses: CodSpeedHQ/action@0ca9cbbf4623b599a6c3ed4fc8a922942705d9f1 # v5.0.2
39
39
  with:
40
40
  mode: simulation
41
41
  run: uv run --locked --group benchmark pytest benchmarks/ --codspeed --no-cov
@@ -0,0 +1,78 @@
1
+ name: Mutation
2
+
3
+ # Coverage says a line ran; mutation testing asks whether anything would have NOTICED had it been
4
+ # wrong. It is a review tool, not a gate: most survivors are equivalent mutants (message text no test
5
+ # asserts on, formatting, `__repr__`), so the job stays green on survivors and publishes the list to
6
+ # triage. Weekly, not nightly - the score only moves when assertpy2/ or tests/ move, and a recurring
7
+ # red that means nothing is a red people learn to ignore.
8
+ #
9
+ # It does go red when the RUN itself is broken. mutmut runs the unmutated suite once to learn which
10
+ # tests cover which function, and exits 1 if that fails, so a test the trampoline breaks stops the
11
+ # whole run instead of quietly leaving mutants unjudged. That red is never routine: it means the
12
+ # config rotted, which is the failure the previous cosmic-ray setup hid for five weeks behind a
13
+ # stale module path.
14
+ #
15
+ # Linux only: mutmut forks and calls os.wait, and does not run on Windows.
16
+
17
+ on:
18
+ schedule:
19
+ - cron: "0 4 * * 1" # weekly, Monday 04:00 UTC
20
+ workflow_dispatch: {}
21
+
22
+ permissions:
23
+ contents: read
24
+
25
+ concurrency:
26
+ group: ${{ github.workflow }}
27
+ cancel-in-progress: true
28
+
29
+ jobs:
30
+ mutation:
31
+ runs-on: ubuntu-latest
32
+ # 10 060 mutants over the whole package. Measured on four pinned cores with `--max-children 4`,
33
+ # which is this runner's shape: 41m15s. Slower runner cores put the real figure somewhere
34
+ # around an hour, so this leaves comfortable headroom without letting a hang burn two hours.
35
+ timeout-minutes: 120
36
+ steps:
37
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
38
+ with:
39
+ persist-credentials: false
40
+
41
+ - name: Install uv
42
+ uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
43
+
44
+ - name: Set up Python
45
+ uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
46
+ with:
47
+ python-version: "3.14"
48
+
49
+ - name: Install dependencies
50
+ # Every optional integration, matching the full-deps cell of ci.yml. A skipped test cannot
51
+ # kill a mutant, so mutating dataframe.py or json_mixin.py without pandas/polars/jsonschema
52
+ # installed reports its whole surface as untested.
53
+ run: uv sync --extra json --extra data --extra inline --group integrations --group mutation
54
+
55
+ - name: Run the mutants
56
+ # Exits non-zero when it cannot collect stats, which is what a baseline failure looks like.
57
+ # Survivors alone do not fail it.
58
+ #
59
+ # `--no-sync` on every `uv run` below: without it uv re-resolves the environment to the
60
+ # project's default groups and uninstalls the extras the step above just installed, so the
61
+ # mutants would be judged by a suite that skips pandas, polars and jsonschema.
62
+ run: uv run --no-sync mutmut run
63
+
64
+ - name: Report
65
+ if: always()
66
+ run: |
67
+ uv run --no-sync python scripts/mutation_report.py >> "$GITHUB_STEP_SUMMARY"
68
+ uv run --no-sync mutmut results > not-killed.txt
69
+
70
+ - name: Upload the mutants that lived
71
+ # `mutmut results` lists every mutant that was not killed, which is survivors plus timeouts
72
+ # plus the ones no test covered. Each line carries its own status, so the file is named for
73
+ # what it holds rather than for survivors alone - reading it as a survivor list overcounts.
74
+ if: always()
75
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
76
+ with:
77
+ name: mutants-not-killed
78
+ path: not-killed.txt
@@ -66,7 +66,7 @@ jobs:
66
66
  run: syft scan dir:dist -o "cyclonedx-json=provenance/assertpy2.sbom.cdx.json"
67
67
 
68
68
  - name: Publish to PyPI
69
- uses: pypa/gh-action-pypi-publish@ba38be9e461d3875417946c167d0b5f3d385a247 # v1.14.1
69
+ uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
70
70
 
71
71
  - name: Upload to GitHub Release
72
72
  run: gh release upload "$TAG_NAME" dist/* provenance/*
@@ -30,6 +30,6 @@ jobs:
30
30
  name: SARIF file
31
31
  path: results.sarif
32
32
  retention-days: 5
33
- - uses: github/codeql-action/upload-sarif@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3
33
+ - uses: github/codeql-action/upload-sarif@d1ba80a13dd99fba24a470575428917156a28b43 # v4.37.5
34
34
  with:
35
35
  sarif_file: results.sarif
@@ -23,4 +23,4 @@ jobs:
23
23
  - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
24
24
  with:
25
25
  persist-credentials: false
26
- - uses: zizmorcore/zizmor-action@6fc4b006235f201fdab3722e17240ab420d580e5 # v0.6.1
26
+ - uses: zizmorcore/zizmor-action@3dc1ecc9bcb9e94e9b2c709687979e1298497054 # v0.6.2
@@ -0,0 +1,67 @@
1
+ # Contributing
2
+
3
+ Contributions of docs, tests, or code are welcome.
4
+
5
+ ## Workflow
6
+
7
+ 1. For a new assertion or matcher, open an issue first. The API grows from demand, and a working
8
+ implementation on its own is not enough to land one
9
+ 2. Fork the repo
10
+ 3. Clone your fork (`git clone <your_fork_url>`)
11
+ 4. Create a branch (`git checkout -b my_branch`)
12
+ 5. Install dependencies: `uv sync`
13
+ 6. Make your changes
14
+ 7. Run the [verification pipeline](#verification-pipeline) and fix any issues
15
+ 8. Commit using [Conventional Commits](#commit-style)
16
+ 9. Push your branch (`git push origin my_branch`)
17
+ 10. Open a [Pull Request](http://github.com/Solganis/assertpy2/pulls)
18
+
19
+ Read more about how pulls work on GitHub's [About pull requests](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-requests) page.
20
+
21
+ ## Requirements
22
+
23
+ - Python 3.10+
24
+ - [uv](https://docs.astral.sh/uv/) as the package manager
25
+
26
+ ## Verification pipeline
27
+
28
+ Run all checks before submitting a PR. Every step must pass.
29
+
30
+ ```bash
31
+ # lint
32
+ uv run ruff check assertpy2/ tests/
33
+
34
+ # format
35
+ uv run ruff format --check assertpy2/ tests/
36
+
37
+ # type check
38
+ uv run ty check
39
+
40
+ # tests with coverage (must be 100%)
41
+ uv run pytest tests/ -v --cov=assertpy2 --cov-report=term-missing
42
+ ```
43
+
44
+ CI requires 100% code coverage.
45
+
46
+ ## Commit style
47
+
48
+ Use [Conventional Commits](https://www.conventionalcommits.org/): `feat:`, `fix:`, `refactor:`, `test:`, `docs:`, `chore:`, etc.
49
+
50
+ ## Tests
51
+
52
+ Write tests for every new feature or bug fix. Use `assertpy2` assertions in tests.
53
+
54
+ ## Documentation examples
55
+
56
+ Guide code blocks are executed (`tests/test_docs_examples.py`) and type-checked
57
+ (`tests/test_docs_typing.py`). To skip one, put an HTML comment above the fence:
58
+
59
+ | Marker | Executed | Type-checked | Use it for |
60
+ |---|---|---|---|
61
+ | `<!-- docs-guard: skip -->` | no | no | pseudo-context, neither runnable nor checkable |
62
+ | `<!-- docs-guard: untyped -->` | yes | no | a dynamic assertion (`has_<attr>()`) |
63
+ | `<!-- docs-guard: raises -->` | no | yes | a block showing what a failure looks like |
64
+ | `<!-- docs-guard: type-error -->` | no | yes, and it **must** fail | a counter-example the page presents as rejected |
65
+
66
+ Setup a page assumes (a domain class, a repository, an HTTP response) goes in
67
+ `tests/docs_fixtures.py`, not into an extra block on the page.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: assertpy2
3
- Version: 2.19.0
3
+ Version: 2.20.1
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
@@ -223,13 +223,14 @@ data = assert_conforms(response.json(), OrderModel).value # data: OrderModel
223
223
  - [**Polling assertions**](https://solganis.github.io/assertpy2/guides/testing/#async-assertions): `eventually()` (async) / `eventually_sync()` (blocking) retry for eventual consistency, with a convergence trace on timeout.
224
224
  - [**Expected exceptions**](https://solganis.github.io/assertpy2/guides/errors/#expected-exceptions): `raises().when_called_with()`, walk the cause chain (`caused_by()`, `has_root_cause()`), match `ExceptionGroup` (`contains_error()`), or pivot to the object (`raised()`).
225
225
  - [**Structured errors**](https://solganis.github.io/assertpy2/guides/errors/#structured-errors): `AssertionFailure` carries `.actual`, `.expected`, `.diff`, and the diff renders into the message itself, so it shows off pytest too.
226
+ - [**Assertions as values**](https://solganis.github.io/assertpy2/guides/errors/#asking-instead-of-asserting): `check()` runs the next assertion for its verdict instead of raising, returning an `AssertionOutcome` that is truthy when it held and carries the message, values and diff when it did not.
226
227
  - [**Rich pytest diffs**](https://solganis.github.io/assertpy2/guides/errors/#rich-pytest-diffs): recursive structural diffs across containers, dataclasses, attrs, and Pydantic models, with intra-line carets for strings and circular-reference protection.
227
228
  - [**Snapshot testing**](https://solganis.github.io/assertpy2/guides/testing/#snapshot-testing): three modes under one typed API, all updated with `--assertpy2-snapshot-update`: `snapshot()` (external JSON file), [`matches_inline()`](https://solganis.github.io/assertpy2/guides/testing/#inline-snapshots) (recorded into the test source), and [`matches_contract_snapshot()`](https://solganis.github.io/assertpy2/guides/testing/#contract-snapshots) (value-tolerant structural regressions).
228
229
  - [**OpenAPI response contracts**](https://solganis.github.io/assertpy2/reference/json/#assertpy2.json_mixin.JsonMixin.conforms_to_openapi): `conforms_to_openapi(spec, path, method)` validates a JSON response body against an operation's response schema (OpenAPI 3.0/3.1 and Swagger 2.0), reporting every violation with its JSON path.
229
230
 
230
231
  **Extensibility**
231
232
 
232
- - [**Custom matchers**](https://solganis.github.io/assertpy2/guides/matchers/#custom-matchers): `register_matcher()` for domain-specific matchers, composable with `&`, `|`, `~`.
233
+ - [**Custom matchers**](https://solganis.github.io/assertpy2/guides/matchers/#custom-matchers): `register_matcher()` to compose existing ones, or subclass `BaseMatcher` for a rule that needs its own predicate. Both compose with `&`, `|`, `~`.
233
234
  - [**Regex group extraction**](https://solganis.github.io/assertpy2/guides/data/#regex-group-extraction): `extracting_group()` and `matches_with_groups()` for regex captures.
234
235
  - [**Extensions**](https://solganis.github.io/assertpy2/extending/custom-assertions/): `add_extension()` for custom assertion methods.
235
236
 
@@ -171,13 +171,14 @@ data = assert_conforms(response.json(), OrderModel).value # data: OrderModel
171
171
  - [**Polling assertions**](https://solganis.github.io/assertpy2/guides/testing/#async-assertions): `eventually()` (async) / `eventually_sync()` (blocking) retry for eventual consistency, with a convergence trace on timeout.
172
172
  - [**Expected exceptions**](https://solganis.github.io/assertpy2/guides/errors/#expected-exceptions): `raises().when_called_with()`, walk the cause chain (`caused_by()`, `has_root_cause()`), match `ExceptionGroup` (`contains_error()`), or pivot to the object (`raised()`).
173
173
  - [**Structured errors**](https://solganis.github.io/assertpy2/guides/errors/#structured-errors): `AssertionFailure` carries `.actual`, `.expected`, `.diff`, and the diff renders into the message itself, so it shows off pytest too.
174
+ - [**Assertions as values**](https://solganis.github.io/assertpy2/guides/errors/#asking-instead-of-asserting): `check()` runs the next assertion for its verdict instead of raising, returning an `AssertionOutcome` that is truthy when it held and carries the message, values and diff when it did not.
174
175
  - [**Rich pytest diffs**](https://solganis.github.io/assertpy2/guides/errors/#rich-pytest-diffs): recursive structural diffs across containers, dataclasses, attrs, and Pydantic models, with intra-line carets for strings and circular-reference protection.
175
176
  - [**Snapshot testing**](https://solganis.github.io/assertpy2/guides/testing/#snapshot-testing): three modes under one typed API, all updated with `--assertpy2-snapshot-update`: `snapshot()` (external JSON file), [`matches_inline()`](https://solganis.github.io/assertpy2/guides/testing/#inline-snapshots) (recorded into the test source), and [`matches_contract_snapshot()`](https://solganis.github.io/assertpy2/guides/testing/#contract-snapshots) (value-tolerant structural regressions).
176
177
  - [**OpenAPI response contracts**](https://solganis.github.io/assertpy2/reference/json/#assertpy2.json_mixin.JsonMixin.conforms_to_openapi): `conforms_to_openapi(spec, path, method)` validates a JSON response body against an operation's response schema (OpenAPI 3.0/3.1 and Swagger 2.0), reporting every violation with its JSON path.
177
178
 
178
179
  **Extensibility**
179
180
 
180
- - [**Custom matchers**](https://solganis.github.io/assertpy2/guides/matchers/#custom-matchers): `register_matcher()` for domain-specific matchers, composable with `&`, `|`, `~`.
181
+ - [**Custom matchers**](https://solganis.github.io/assertpy2/guides/matchers/#custom-matchers): `register_matcher()` to compose existing ones, or subclass `BaseMatcher` for a rule that needs its own predicate. Both compose with `&`, `|`, `~`.
181
182
  - [**Regex group extraction**](https://solganis.github.io/assertpy2/guides/data/#regex-group-extraction): `extracting_group()` and `matches_with_groups()` for regex captures.
182
183
  - [**Extensions**](https://solganis.github.io/assertpy2/extending/custom-assertions/): `add_extension()` for custom assertion methods.
183
184
 
@@ -1,4 +1,5 @@
1
1
  from .assertpy import (
2
+ CheckBuilder,
2
3
  NegatedBuilder,
3
4
  SoftAssertionCollector,
4
5
  WarningLoggingAdapter,
@@ -14,9 +15,26 @@ from .assertpy import (
14
15
  soft_fail,
15
16
  )
16
17
  from .async_assertions import AsyncAssertionBuilder, SyncAssertionBuilder
17
- from .errors import AssertionFailure, DiffEntry, DiffResult, PollSample, PollTrace, VacuousAssertionWarning
18
+ from .errors import (
19
+ AssertionFailure,
20
+ DiffEntry,
21
+ DiffResult,
22
+ PollSample,
23
+ PollTrace,
24
+ Step,
25
+ VacuousAssertionWarning,
26
+ )
18
27
  from .file import contents_of
19
- from .matchers import Matcher, clear_custom_matchers, match, register_matcher, unregister_matcher
28
+ from .matchers import (
29
+ BaseMatcher,
30
+ Matcher,
31
+ MatchResult,
32
+ clear_custom_matchers,
33
+ match,
34
+ register_matcher,
35
+ unregister_matcher,
36
+ )
37
+ from .outcome import AssertionOutcome
20
38
  from .snapshot import (
21
39
  SnapshotCreatedWarning,
22
40
  SnapshotKeyReusedWarning,
@@ -26,9 +44,13 @@ from .snapshot import (
26
44
 
27
45
  __all__ = [
28
46
  "AssertionFailure",
47
+ "AssertionOutcome",
29
48
  "AsyncAssertionBuilder",
49
+ "BaseMatcher",
50
+ "CheckBuilder",
30
51
  "DiffEntry",
31
52
  "DiffResult",
53
+ "MatchResult",
32
54
  "Matcher",
33
55
  "NegatedBuilder",
34
56
  "PollSample",
@@ -37,6 +59,7 @@ __all__ = [
37
59
  "SnapshotKeyReusedWarning",
38
60
  "SnapshotUpdatedWarning",
39
61
  "SoftAssertionCollector",
62
+ "Step",
40
63
  "SyncAssertionBuilder",
41
64
  "VacuousAssertionWarning",
42
65
  "WarningLoggingAdapter",
@@ -331,3 +331,30 @@ def _spec_matches(key, value, specs) -> bool:
331
331
  elif spec == key:
332
332
  return True
333
333
  return False
334
+
335
+
336
+ def _config_note(config: _CompareConfig | None) -> str:
337
+ """A newline plus an echo of the comparison settings that were in force, or ``""`` when none were.
338
+
339
+ ``is_equal_to`` already names ``ignore`` and ``include`` inside its sentence, but nothing reports
340
+ the rest, and they are what a reader is questioning when a field they thought was tolerated still
341
+ failed. Rendered only for a non-default config: `_build_compare_config()` returns ``None`` when
342
+ every setting is at its default, so the check costs nothing and the line never appears on the
343
+ ordinary failure.
344
+
345
+ It goes on its own line rather than into the sentence, so the original message stays a prefix and
346
+ a `match=` or ``startswith`` written against it keeps working.
347
+ """
348
+ if config is None:
349
+ return ""
350
+ parts = []
351
+ if config.tolerance is not None:
352
+ parts.append(f"tolerance={config.tolerance!r}")
353
+ if config.comparators:
354
+ keys = ", ".join(sorted(key.__name__ if isinstance(key, type) else str(key) for key in config.comparators))
355
+ parts.append(f"comparators for {keys}")
356
+ if config.ignore_null:
357
+ parts.append("ignore_null=True")
358
+ if config.strict_types:
359
+ parts.append("strict_types=True")
360
+ return "\ncompared with " + ", ".join(parts) if parts else ""
@@ -17,6 +17,8 @@ def _submodel(annotation: object) -> type | None:
17
17
  """The nested model class for an annotation, peeling ``Optional`` / ``list`` / ``tuple``; else ``None``."""
18
18
  origin = typing.get_origin(annotation)
19
19
  args = typing.get_args(annotation)
20
+ # both names are needed below 3.14, where `Optional[X]` and `X | None` had distinct origins; from
21
+ # 3.14 they are the same object and the second test is redundant on the interpreters we gate on
20
22
  if origin is typing.Union or origin is types.UnionType:
21
23
  non_none = [arg for arg in args if arg is not type(None)]
22
24
  return _submodel(non_none[0]) if len(non_none) == 1 else None