assertpy2 2.26.0__tar.gz → 2.27.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 (250) hide show
  1. {assertpy2-2.26.0 → assertpy2-2.27.1}/CONTRIBUTING.md +14 -0
  2. {assertpy2-2.26.0 → assertpy2-2.27.1}/PKG-INFO +2 -2
  3. {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/_clustering.py +25 -2
  4. {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/_engine/_builder_check_typing.py +0 -10
  5. {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/_engine/_capable_typing.py +37 -31
  6. {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/_engine/_check_typing.py +38 -0
  7. {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/_engine/_compare.py +184 -32
  8. {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/_engine/_diff.py +61 -16
  9. {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/_engine/_equality.py +231 -41
  10. assertpy2-2.27.1/assertpy2/_engine/_introspection.py +410 -0
  11. {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/_engine/_membership.py +117 -16
  12. {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/_engine/_mixin_base.py +1 -0
  13. {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/_engine/_negated_typing.py +501 -122
  14. {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/_engine/_ordering.py +57 -8
  15. assertpy2-2.27.1/assertpy2/_engine/_pairing.py +59 -0
  16. assertpy2-2.27.1/assertpy2/_engine/_poll_typing.py +5492 -0
  17. {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/_engine/_require.py +62 -9
  18. {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/_engine/_typing.py +91 -13
  19. {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/_hints.py +22 -10
  20. {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/_inline.py +9 -5
  21. {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/_matcher_impls.py +211 -78
  22. {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/_satisfies.py +44 -53
  23. {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/_snapshot_codec.py +122 -9
  24. {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/assertpy.py +39 -17
  25. {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/async_assertions.py +38 -9
  26. {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/base.py +26 -20
  27. {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/bytes_mixin.py +4 -2
  28. {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/collection.py +23 -29
  29. {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/contains.py +59 -28
  30. {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/dataframe.py +9 -1
  31. {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/date.py +27 -17
  32. {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/dict.py +5 -4
  33. {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/dynamic.py +7 -3
  34. {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/errors.py +35 -8
  35. {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/exception.py +82 -59
  36. {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/file.py +9 -8
  37. {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/helpers.py +39 -40
  38. {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/http_mixin.py +1 -1
  39. {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/json_mixin.py +31 -8
  40. {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/matchers.py +37 -1
  41. {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/numeric.py +71 -43
  42. {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/pytest_plugin.py +7 -2
  43. {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/snapshot.py +5 -1
  44. {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/string.py +43 -22
  45. {assertpy2-2.26.0 → assertpy2-2.27.1}/docs/concepts/stability.md +2 -2
  46. {assertpy2-2.26.0 → assertpy2-2.27.1}/docs/concepts/type-safety.md +8 -1
  47. {assertpy2-2.26.0 → assertpy2-2.27.1}/docs/extending/integrations.md +6 -2
  48. {assertpy2-2.26.0 → assertpy2-2.27.1}/docs/guides/assertions.md +6 -2
  49. {assertpy2-2.26.0 → assertpy2-2.27.1}/docs/guides/data.md +4 -1
  50. {assertpy2-2.26.0 → assertpy2-2.27.1}/docs/guides/errors.md +1 -1
  51. {assertpy2-2.26.0 → assertpy2-2.27.1}/docs/reference/async.md +8 -0
  52. {assertpy2-2.26.0 → assertpy2-2.27.1}/docs/reference/entry-points.md +18 -0
  53. {assertpy2-2.26.0 → assertpy2-2.27.1}/docs/reference/errors.md +12 -0
  54. {assertpy2-2.26.0 → assertpy2-2.27.1}/docs/reference/matchers.md +24 -0
  55. {assertpy2-2.26.0 → assertpy2-2.27.1}/mkdocs.yml +1 -1
  56. {assertpy2-2.26.0 → assertpy2-2.27.1}/pyproject.toml +22 -5
  57. {assertpy2-2.26.0 → assertpy2-2.27.1}/scripts/generate_poll_protocols.py +258 -24
  58. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/api_snapshot.json +506 -30
  59. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/poll_parity_baseline.py +6 -2
  60. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/pyright_baseline.py +17 -19
  61. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_async.py +150 -0
  62. assertpy2-2.27.1/tests/test_attrs_support.py +478 -0
  63. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_bytes.py +6 -1
  64. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_capable_protocol.py +414 -1
  65. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_clustering.py +22 -2
  66. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_complexity.py +12 -10
  67. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_core.py +49 -1
  68. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_dataframe.py +39 -0
  69. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_datetime.py +72 -10
  70. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_dynamic.py +26 -0
  71. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_equals.py +12 -0
  72. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_errors.py +31 -0
  73. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_evaluation_core.py +929 -5
  74. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_extensions.py +160 -0
  75. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_file.py +11 -0
  76. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_hints.py +55 -0
  77. assertpy2-2.27.1/tests/test_hostile_values.py +238 -0
  78. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_inline_snapshot.py +27 -0
  79. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_json.py +15 -3
  80. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_matcher_registry.py +21 -0
  81. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_negated_protocols.py +19 -3
  82. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_numbers.py +199 -0
  83. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_openapi_contract.py +87 -0
  84. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_pin_coverage.py +9 -0
  85. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_poll_protocols.py +101 -7
  86. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_property_attrs.py +43 -1
  87. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_property_based.py +159 -1
  88. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_protocol_parity.py +20 -3
  89. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_public_surface.py +57 -0
  90. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_pyright_baseline.py +1 -0
  91. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_pytest_plugin.py +29 -7
  92. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_recursive_compare_config.py +224 -1
  93. assertpy2-2.27.1/tests/test_recursive_compare_pydantic.py +466 -0
  94. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_satisfy.py +104 -0
  95. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_snapshots.py +89 -0
  96. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_soft.py +80 -1
  97. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_surface_conformance.py +102 -0
  98. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_typing.py +43 -1
  99. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_typing_conformance.py +1 -1
  100. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_typing_negative.py +21 -7
  101. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_typing_promises.py +9 -0
  102. assertpy2-2.27.1/tests/test_verdict_corpus.py +137 -0
  103. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_warn.py +15 -0
  104. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/typing_cases.py +64 -2
  105. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/typing_harness.py +25 -3
  106. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/typing_negative_baseline.py +74 -10
  107. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/typing_promises.py +89 -0
  108. assertpy2-2.27.1/tests/verdict_corpus.py +632 -0
  109. assertpy2-2.27.1/tests/verdict_golden.txt +127 -0
  110. assertpy2-2.26.0/assertpy2/_engine/_introspection.py +0 -214
  111. assertpy2-2.26.0/assertpy2/_engine/_poll_typing.py +0 -2724
  112. assertpy2-2.26.0/tests/test_attrs_support.py +0 -170
  113. assertpy2-2.26.0/tests/test_recursive_compare_pydantic.py +0 -83
  114. {assertpy2-2.26.0 → assertpy2-2.27.1}/.gitignore +0 -0
  115. {assertpy2-2.26.0 → assertpy2-2.27.1}/LICENSE +0 -0
  116. {assertpy2-2.26.0 → assertpy2-2.27.1}/README.md +0 -0
  117. {assertpy2-2.26.0 → assertpy2-2.27.1}/SECURITY.md +0 -0
  118. {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/__init__.py +0 -0
  119. {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/_dangling.py +0 -0
  120. {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/_engine/__init__.py +0 -0
  121. {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/_engine/_compat.py +0 -0
  122. {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/_engine/_contract.py +0 -0
  123. {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/_engine/_operations.py +0 -0
  124. {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/_engine/_path.py +0 -0
  125. {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/_engine/_size.py +0 -0
  126. {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/_engine/_text.py +0 -0
  127. {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/behave_matchers.py +0 -0
  128. {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/extracting.py +0 -0
  129. {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/outcome.py +0 -0
  130. {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/py.typed +0 -0
  131. {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/warning.py +0 -0
  132. {assertpy2-2.26.0 → assertpy2-2.27.1}/benchmarks/test_perf.py +0 -0
  133. {assertpy2-2.26.0 → assertpy2-2.27.1}/docs/assets/diff-equal.png +0 -0
  134. {assertpy2-2.26.0 → assertpy2-2.27.1}/docs/assets/diff-equal.svg +0 -0
  135. {assertpy2-2.26.0 → assertpy2-2.27.1}/docs/assets/diff-gallery.png +0 -0
  136. {assertpy2-2.26.0 → assertpy2-2.27.1}/docs/assets/diff-match.svg +0 -0
  137. {assertpy2-2.26.0 → assertpy2-2.27.1}/docs/assets/diff-sequence.svg +0 -0
  138. {assertpy2-2.26.0 → assertpy2-2.27.1}/docs/assets/diff-set.svg +0 -0
  139. {assertpy2-2.26.0 → assertpy2-2.27.1}/docs/assets/diff-string.svg +0 -0
  140. {assertpy2-2.26.0 → assertpy2-2.27.1}/docs/extending/custom-assertions.md +0 -0
  141. {assertpy2-2.26.0 → assertpy2-2.27.1}/docs/getting-started/comparison.md +0 -0
  142. {assertpy2-2.26.0 → assertpy2-2.27.1}/docs/getting-started/migration.md +0 -0
  143. {assertpy2-2.26.0 → assertpy2-2.27.1}/docs/getting-started/quickstart.md +0 -0
  144. {assertpy2-2.26.0 → assertpy2-2.27.1}/docs/guides/fluent.md +0 -0
  145. {assertpy2-2.26.0 → assertpy2-2.27.1}/docs/guides/matchers.md +0 -0
  146. {assertpy2-2.26.0 → assertpy2-2.27.1}/docs/guides/testing.md +0 -0
  147. {assertpy2-2.26.0 → assertpy2-2.27.1}/docs/index.md +0 -0
  148. {assertpy2-2.26.0 → assertpy2-2.27.1}/docs/logo-dark.svg +0 -0
  149. {assertpy2-2.26.0 → assertpy2-2.27.1}/docs/logo.svg +0 -0
  150. {assertpy2-2.26.0 → assertpy2-2.27.1}/docs/recipes.md +0 -0
  151. {assertpy2-2.26.0 → assertpy2-2.27.1}/docs/reference/bytes.md +0 -0
  152. {assertpy2-2.26.0 → assertpy2-2.27.1}/docs/reference/collections.md +0 -0
  153. {assertpy2-2.26.0 → assertpy2-2.27.1}/docs/reference/containment.md +0 -0
  154. {assertpy2-2.26.0 → assertpy2-2.27.1}/docs/reference/core.md +0 -0
  155. {assertpy2-2.26.0 → assertpy2-2.27.1}/docs/reference/dataframes.md +0 -0
  156. {assertpy2-2.26.0 → assertpy2-2.27.1}/docs/reference/dates.md +0 -0
  157. {assertpy2-2.26.0 → assertpy2-2.27.1}/docs/reference/dicts.md +0 -0
  158. {assertpy2-2.26.0 → assertpy2-2.27.1}/docs/reference/dynamic.md +0 -0
  159. {assertpy2-2.26.0 → assertpy2-2.27.1}/docs/reference/exceptions.md +0 -0
  160. {assertpy2-2.26.0 → assertpy2-2.27.1}/docs/reference/extracting.md +0 -0
  161. {assertpy2-2.26.0 → assertpy2-2.27.1}/docs/reference/files.md +0 -0
  162. {assertpy2-2.26.0 → assertpy2-2.27.1}/docs/reference/http.md +0 -0
  163. {assertpy2-2.26.0 → assertpy2-2.27.1}/docs/reference/json.md +0 -0
  164. {assertpy2-2.26.0 → assertpy2-2.27.1}/docs/reference/numbers.md +0 -0
  165. {assertpy2-2.26.0 → assertpy2-2.27.1}/docs/reference/overview.md +0 -0
  166. {assertpy2-2.26.0 → assertpy2-2.27.1}/docs/reference/snapshots.md +0 -0
  167. {assertpy2-2.26.0 → assertpy2-2.27.1}/docs/reference/strings.md +0 -0
  168. {assertpy2-2.26.0 → assertpy2-2.27.1}/docs/reference/warnings.md +0 -0
  169. {assertpy2-2.26.0 → assertpy2-2.27.1}/docs/stylesheets/extra.css +0 -0
  170. {assertpy2-2.26.0 → assertpy2-2.27.1}/mkdocs_hooks.py +0 -0
  171. {assertpy2-2.26.0 → assertpy2-2.27.1}/scripts/generate_check_protocols.py +0 -0
  172. {assertpy2-2.26.0 → assertpy2-2.27.1}/scripts/mutation_report.py +0 -0
  173. {assertpy2-2.26.0 → assertpy2-2.27.1}/scripts/regenerate.py +0 -0
  174. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/api_surface.py +0 -0
  175. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/conftest.py +0 -0
  176. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/docs_fixtures.py +0 -0
  177. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/group_compat.py +0 -0
  178. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/poll_parity_cases.py +0 -0
  179. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_api_compatibility.py +0 -0
  180. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_api_vocabulary.py +0 -0
  181. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_architecture_doc.py +0 -0
  182. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_async_predicates.py +0 -0
  183. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_behave_matchers.py +0 -0
  184. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_bool.py +0 -0
  185. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_boundary_cases.py +0 -0
  186. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_callable.py +0 -0
  187. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_chaining.py +0 -0
  188. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_check.py +0 -0
  189. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_check_protocols.py +0 -0
  190. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_class.py +0 -0
  191. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_collection.py +0 -0
  192. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_custom_dict.py +0 -0
  193. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_custom_list.py +0 -0
  194. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_dangling.py +0 -0
  195. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_declared_floors.py +0 -0
  196. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_description.py +0 -0
  197. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_dict.py +0 -0
  198. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_dict_compare.py +0 -0
  199. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_docs_examples.py +0 -0
  200. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_docs_typing.py +0 -0
  201. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_exception_context.py +0 -0
  202. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_expected_contract.py +0 -0
  203. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_expected_exception.py +0 -0
  204. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_expected_warning.py +0 -0
  205. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_extracting.py +0 -0
  206. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_fail.py +0 -0
  207. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_grouped_soft.py +0 -0
  208. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_http.py +0 -0
  209. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_in.py +0 -0
  210. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_inline_record.py +0 -0
  211. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_iterable_cluster.py +0 -0
  212. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_list.py +0 -0
  213. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_match_result.py +0 -0
  214. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_matcher_parity.py +0 -0
  215. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_matchers.py +0 -0
  216. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_message_elision.py +0 -0
  217. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_namedtuple.py +0 -0
  218. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_none.py +0 -0
  219. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_not.py +0 -0
  220. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_operation_contract.py +0 -0
  221. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_optional_integration_contracts.py +0 -0
  222. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_overload_order.py +0 -0
  223. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_overloads.py +0 -0
  224. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_pipeline.py +0 -0
  225. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_poll_parity.py +0 -0
  226. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_recursive_assertion.py +0 -0
  227. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_recursive_compare.py +0 -0
  228. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_recursive_compare_attrs.py +0 -0
  229. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_refusals.py +0 -0
  230. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_regex_groups.py +0 -0
  231. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_requirement.py +0 -0
  232. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_rich_diff.py +0 -0
  233. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_same_as.py +0 -0
  234. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_skip_guard.py +0 -0
  235. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_soft_fail.py +0 -0
  236. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_string.py +0 -0
  237. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_structural.py +0 -0
  238. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_sync_eventually.py +0 -0
  239. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_traceback.py +0 -0
  240. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_type.py +0 -0
  241. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_type_expression_failures.py +0 -0
  242. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_typing_claims.py +0 -0
  243. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_typing_completeness.py +0 -0
  244. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_typing_from_a_wheel.py +0 -0
  245. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_typing_http.py +0 -0
  246. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_typing_integrations.py +0 -0
  247. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_vacuity_contract.py +0 -0
  248. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/typing_http.py +0 -0
  249. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/typing_integrations.py +0 -0
  250. {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/typing_integrations_baseline.py +0 -0
@@ -114,6 +114,20 @@ change. The baseline test passes the same target itself, so it gives one answer
114
114
  - `tests/test_docs_examples.py` is run separately because it executes the snippets in `docs/`, which
115
115
  needs the `docs-examples` group and a different collection.
116
116
 
117
+ ### When a gate says an answer moved
118
+
119
+ Two gates record what the library currently decides rather than what it should. Both fail on a change
120
+ and neither is a failure to paper over: read the diff, decide whether the change is meant to ship, then
121
+ re-record it in the same commit so the diff carries the decision into review.
122
+
123
+ ```bash
124
+ ASSERTPY2_UPDATE_API=1 uv run pytest tests/test_api_compatibility.py # the public surface
125
+ ASSERTPY2_UPDATE_VERDICTS=1 uv run pytest tests/test_verdict_corpus.py # what each input decides
126
+ ```
127
+
128
+ The second one's diff is the draft of the release's Behaviour changes section, so read it before
129
+ writing that section rather than after.
130
+
117
131
  ## Commit style
118
132
 
119
133
  Use [Conventional Commits](https://www.conventionalcommits.org/): `feat:`, `fix:`, `refactor:`, `test:`, `docs:`, `chore:`, etc.
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: assertpy2
3
- Version: 2.26.0
4
- Summary: Fluent assertion library for Python with composable matchers, structural matching, and full type safety
3
+ Version: 2.27.1
4
+ Summary: Fluent, fully typed assertion library for Python: your type checker sees only the assertions that fit the value, and every failure carries a structured diff. Soft assertions, polling, inline snapshots, OpenAPI contracts.
5
5
  Project-URL: Homepage, https://github.com/Solganis/assertpy2
6
6
  Project-URL: Repository, https://github.com/Solganis/assertpy2
7
7
  Project-URL: Issues, https://github.com/Solganis/assertpy2/issues
@@ -239,6 +239,25 @@ def _shown(value: object) -> str:
239
239
  return _bounded(stable_repr(value))
240
240
 
241
241
 
242
+ class _Absent:
243
+ """A side that held nothing at all.
244
+
245
+ An absent key carries `None`, and read as a value it printed as ``None`` and keyed with every
246
+ failure whose value really is `None`.
247
+ """
248
+
249
+ def __repr__(self) -> str:
250
+ return "<absent>"
251
+
252
+
253
+ _ABSENT = _Absent()
254
+
255
+
256
+ def _side_of(entry: DiffEntry, side: str) -> object:
257
+ """One side of an entry, or the absent marker where that side held nothing."""
258
+ return _ABSENT if entry.absent == side else (entry.actual if side == "actual" else entry.expected)
259
+
260
+
242
261
  def _identity(value: object) -> str:
243
262
  """A value as a cluster key: the whole of it, in a fixed number of characters.
244
263
 
@@ -265,7 +284,9 @@ def signature(diff: DiffResult, entry: DiffEntry, label: str | None = None) -> S
265
284
  return Signature(False, diff.kind, label=label)
266
285
  if diff.kind not in _VALUES_ARE_VALUES:
267
286
  return None
268
- return Signature(False, diff.kind, values=(_identity(entry.actual), _identity(entry.expected)))
287
+ return Signature(
288
+ False, diff.kind, values=(_identity(_side_of(entry, "actual")), _identity(_side_of(entry, "expected")))
289
+ )
269
290
 
270
291
 
271
292
  def is_well_formed(key: Signature) -> bool:
@@ -305,7 +326,9 @@ def observations_of(diff: DiffResult | None, label: str | None = None) -> list[O
305
326
  for entry in diff.entries:
306
327
  key = signature(diff, entry, label)
307
328
  if key is not None:
308
- seen.setdefault(key, Observation(key, _shown(entry.actual), _shown(entry.expected)))
329
+ seen.setdefault(
330
+ key, Observation(key, _shown(_side_of(entry, "actual")), _shown(_side_of(entry, "expected")))
331
+ )
309
332
  return list(seen.values())
310
333
 
311
334
 
@@ -27,7 +27,6 @@ if TYPE_CHECKING:
27
27
  from ..matchers import Matcher
28
28
  from ..outcome import AssertionOutcome
29
29
  from ._capable_typing import (
30
- _Callable,
31
30
  _Indexed,
32
31
  _Keyed,
33
32
  _KeyedWithItems,
@@ -1136,15 +1135,6 @@ if TYPE_CHECKING:
1136
1135
  **options: Any,
1137
1136
  ) -> AssertionOutcome: ...
1138
1137
 
1139
- @overload
1140
- def when_called_with(
1141
- self: _CheckAnyValue[Callable[..., _P]], *some_args: object, **some_kwargs: object
1142
- ) -> AssertionOutcome: ...
1143
- @overload
1144
- def when_called_with(
1145
- self: _CheckAnyValue[_Callable], *some_args: object, **some_kwargs: object
1146
- ) -> AssertionOutcome: ...
1147
-
1148
1138
  @overload
1149
1139
  def is_not_none(self: _CheckAnyValue[str | None]) -> AssertionOutcome: ...
1150
1140
  @overload
@@ -35,8 +35,18 @@ if TYPE_CHECKING:
35
35
  from ..matchers import Matcher
36
36
  from ._builder_check_typing import _CheckAnyValue
37
37
  from ._compat import Self
38
- from ._poll_typing import _AsyncPoll, _SyncPoll
39
- from ._typing import _E, _K, _R, _U, _V
38
+ from ._poll_typing import _AsyncPoll, _NoExpectationOnAChain, _SyncPoll
39
+ from ._typing import (
40
+ _E,
41
+ _K,
42
+ _R,
43
+ _U,
44
+ _V,
45
+ _ExpectedCompletionAssertion,
46
+ _ExpectedRaiseAssertion,
47
+ _ExpectedWarningAssertion,
48
+ _ListAssertion,
49
+ )
40
50
 
41
51
  # covariant: the façade only ever hands the subject back, through `value`
42
52
  _CapableT_co = TypeVar("_CapableT_co", covariant=True)
@@ -194,6 +204,18 @@ if TYPE_CHECKING:
194
204
  ignoring: type[Exception] | tuple[type[Exception], ...] = ...,
195
205
  trace: bool = ...,
196
206
  ) -> _SyncPoll[Any]: ...
207
+ # where the callable view lands: flattened, the runtime's `-> Self` kept a caught message typed as the callable
208
+ def raises(self: _CapableAssertion[_Callable], ex: type) -> _ExpectedRaiseAssertion[Any]: ...
209
+ def does_not_raise(self: _CapableAssertion[_Callable], ex: type) -> _ExpectedCompletionAssertion[Any]: ...
210
+ def warns(
211
+ self: _CapableAssertion[_Callable], warning: type[Warning] = ...
212
+ ) -> _ExpectedWarningAssertion[Any]: ...
213
+ def does_not_warn(
214
+ self: _CapableAssertion[_Callable], warning: type[Warning] = ...
215
+ ) -> _ExpectedCompletionAssertion[Any]: ...
216
+
217
+ when_called_with: _NoExpectationOnAChain
218
+
197
219
  # the builder's own helpers: left to `__getattr__`, `builder()` read as this facade over the value already here
198
220
  def builder(
199
221
  self,
@@ -220,8 +242,8 @@ if TYPE_CHECKING:
220
242
  ) -> Self: ...
221
243
  def all_satisfy(self, matcher: Matcher[Any] | Callable[..., bool], *, allow_empty: bool = ...) -> Self: ...
222
244
  def any_satisfy(self, matcher: Matcher[Any] | Callable[..., bool]) -> Self: ...
223
- def at_json_path(self, path: str) -> Self: ...
224
- def caused_by(self, ex: type) -> Self: ...
245
+ def at_json_path(self, path: str) -> AssertionBuilder[Any]: ...
246
+ def caused_by(self, ex: type) -> AssertionBuilder[Any]: ...
225
247
  def conforms_to_openapi(
226
248
  self,
227
249
  spec: dict[str, Any],
@@ -255,9 +277,7 @@ if TYPE_CHECKING:
255
277
  def contains_value(
256
278
  self: _CapableAssertion[_KeyedWithValues], *values: object
257
279
  ) -> _CapableAssertion[_CapableT_co]: ...
258
- def decoded_as(
259
- self: _CapableAssertion[bytes | bytearray], encoding: str = ...
260
- ) -> _CapableAssertion[_CapableT_co]: ...
280
+ def decoded_as(self: _CapableAssertion[bytes | bytearray], encoding: str = ...) -> AssertionBuilder[Any]: ...
261
281
  def decoded_as_json(self) -> AssertionBuilder[object]: ...
262
282
  def described_as(self, description: str) -> Self: ...
263
283
  def does_not_contain(self, *items: object) -> Self: ...
@@ -273,28 +293,24 @@ if TYPE_CHECKING:
273
293
  def does_not_exist(self: _CapableAssertion[_PathLike]) -> _CapableAssertion[_CapableT_co]: ...
274
294
  def does_not_have_json_path(self, path: str) -> Self: ...
275
295
  def does_not_match(self: _CapableAssertion[str], pattern: str) -> _CapableAssertion[_CapableT_co]: ...
276
- def does_not_raise(self: _CapableAssertion[_Callable], ex: type) -> _CapableAssertion[_CapableT_co]: ...
277
- def does_not_warn(
278
- self: _CapableAssertion[_Callable], warning: type[Warning] = ...
279
- ) -> _CapableAssertion[_CapableT_co]: ...
280
296
  def each(self, matcher: Matcher[Any] | Callable[..., bool], *, allow_empty: bool = ...) -> Self: ...
281
297
  def ends_with(self, suffix: object) -> Self: ...
282
298
  def ends_with_ignoring_case(self: _CapableAssertion[str], suffix: str) -> _CapableAssertion[_CapableT_co]: ...
283
- def error_of(self, ex: type) -> Self: ...
284
- def errors(self) -> Self: ...
299
+ def error_of(self, ex: type) -> AssertionBuilder[Any]: ...
300
+ def errors(self) -> AssertionBuilder[Any]: ...
285
301
  def exists(self: _CapableAssertion[_PathLike]) -> _CapableAssertion[_CapableT_co]: ...
286
- def extracting(self, *names: object, **kwargs: object) -> Self: ...
302
+ def extracting(self, *names: object, **kwargs: object) -> _ListAssertion[Any]: ...
287
303
  def extracting_group(
288
304
  self: _CapableAssertion[str], pattern: str, group: int | str = ...
289
- ) -> _CapableAssertion[_CapableT_co]: ...
290
- def filtered_on(self, predicate: Matcher[Any] | Callable[[Any], bool]) -> Self: ...
291
- def flat_mapped(self, func: Callable[[Any], Iterable[Any]]) -> Self: ...
305
+ ) -> AssertionBuilder[Any]: ...
306
+ def filtered_on(self, predicate: Matcher[Any] | Callable[[Any], bool]) -> _ListAssertion[Any]: ...
307
+ def flat_mapped(self, func: Callable[[Any], Iterable[Any]]) -> _ListAssertion[Any]: ...
292
308
  def has_byte_at(
293
309
  self: _CapableAssertion[bytes | bytearray], index: int, expected: int
294
310
  ) -> _CapableAssertion[_CapableT_co]: ...
295
311
  def has_json_path(self, path: str) -> Self: ...
296
312
  def has_no_none_fields(self, *, allow_empty: bool = ...) -> Self: ...
297
- def has_root_cause(self, ex: type) -> Self: ...
313
+ def has_root_cause(self, ex: type) -> AssertionBuilder[Any]: ...
298
314
  def has_same_size_as(self, other: Sized) -> Self: ...
299
315
  def has_size_between(self, low: int, high: int) -> Self: ...
300
316
  def has_size_greater_than(self, size: int) -> Self: ...
@@ -420,11 +436,10 @@ if TYPE_CHECKING:
420
436
  def matches_json_schema(self, schema: dict[str, Any]) -> Self: ...
421
437
  def matches_json_schema_from_file(self, path: str | Path) -> Self: ...
422
438
  def matches_structure(self, spec: dict[Any, Any]) -> Self: ...
423
- def matches_with_groups(self: _CapableAssertion[str], pattern: str) -> _CapableAssertion[_CapableT_co]: ...
439
+ def matches_with_groups(self: _CapableAssertion[str], pattern: str) -> AssertionBuilder[Any]: ...
424
440
  def none_satisfy(self, matcher: Matcher[Any] | Callable[..., bool]) -> Self: ...
425
- def raised(self) -> Self: ...
426
- def raises(self: _CapableAssertion[_Callable], ex: type) -> _CapableAssertion[_CapableT_co]: ...
427
- def returned(self) -> Self: ...
441
+ def raised(self) -> AssertionBuilder[Any]: ...
442
+ def returned(self) -> AssertionBuilder[Any]: ...
428
443
  def satisfies_exactly(self, *matchers: Matcher[Any] | Callable[..., bool]) -> Self: ...
429
444
  def satisfies_exactly_in_any_order(self, *matchers: Matcher[Any] | Callable[..., bool]) -> Self: ...
430
445
  def snapshot(
@@ -443,12 +458,6 @@ if TYPE_CHECKING:
443
458
  self: _CapableAssertion[bytes | bytearray], prefix: bytes | bytearray
444
459
  ) -> _CapableAssertion[_CapableT_co]: ...
445
460
  def starts_with_ignoring_case(self: _CapableAssertion[str], prefix: str) -> _CapableAssertion[_CapableT_co]: ...
446
- def warns(
447
- self: _CapableAssertion[_Callable], warning: type[Warning] = ...
448
- ) -> _CapableAssertion[_CapableT_co]: ...
449
- def when_called_with(
450
- self: _CapableAssertion[_Callable], *some_args: object, **some_kwargs: object
451
- ) -> _CapableAssertion[_CapableT_co]: ...
452
461
  def zip_satisfies(
453
462
  self, other: Iterable[object], predicate: Callable[..., bool], *, allow_empty: bool = ...
454
463
  ) -> Self: ...
@@ -721,9 +730,6 @@ if TYPE_CHECKING:
721
730
  def starts_with_ignoring_case(
722
731
  self: _NegatedCapableAssertion[str], prefix: str
723
732
  ) -> _CapableAssertion[_CapableT_co]: ...
724
- def when_called_with(
725
- self: _NegatedCapableAssertion[_Callable], *some_args: object, **some_kwargs: object
726
- ) -> _CapableAssertion[_CapableT_co]: ...
727
733
  def zip_satisfies(
728
734
  self, other: Iterable[object], predicate: Callable[..., bool], *, allow_empty: bool = ...
729
735
  ) -> _CapableAssertion[_CapableT_co]: ...
@@ -150,6 +150,12 @@ if TYPE_CHECKING:
150
150
  @property
151
151
  def not_(self) -> Self: ...
152
152
 
153
+ class _CheckReturningAssertion(Protocol):
154
+ """The verdict twin of `_ReturningAssertion`."""
155
+
156
+ @property
157
+ def not_(self) -> Self: ...
158
+
153
159
  class _CheckZeroAssertion(Protocol):
154
160
  """The verdict twin of `_ZeroAssertion`."""
155
161
 
@@ -679,9 +685,41 @@ if TYPE_CHECKING:
679
685
  @property
680
686
  def not_(self) -> Self: ...
681
687
 
688
+ class _CheckWarnedAssertion(_CheckTextAssertion, _CheckReturningAssertion, Protocol):
689
+ """The verdict twin of `_WarnedAssertion`."""
690
+
691
+ @property
692
+ def not_(self) -> Self: ...
693
+
682
694
  class _CheckCallableAssertion(_CheckCoreAssertion, Protocol[_P_co]):
683
695
  """The verdict twin of `_CallableAssertion`."""
684
696
 
697
+ @property
698
+ def not_(self) -> Self: ...
699
+
700
+ class _CheckCompletedAssertion(_CheckCallableAssertion[_P_co], _CheckReturningAssertion, Protocol[_P_co]):
701
+ """The verdict twin of `_CompletedAssertion`."""
702
+
703
+ @property
704
+ def not_(self) -> Self: ...
705
+
706
+ class _CheckExpectedRaiseAssertion(_CheckCallableAssertion[_P_co], Protocol[_P_co]):
707
+ """The verdict twin of `_ExpectedRaiseAssertion`."""
708
+
709
+ def when_called_with(self, *some_args: object, **some_kwargs: object) -> AssertionOutcome: ...
710
+ @property
711
+ def not_(self) -> Self: ...
712
+
713
+ class _CheckExpectedWarningAssertion(_CheckCallableAssertion[_P_co], Protocol[_P_co]):
714
+ """The verdict twin of `_ExpectedWarningAssertion`."""
715
+
716
+ def when_called_with(self, *some_args: object, **some_kwargs: object) -> AssertionOutcome: ...
717
+ @property
718
+ def not_(self) -> Self: ...
719
+
720
+ class _CheckExpectedCompletionAssertion(_CheckCallableAssertion[_P_co], Protocol[_P_co]):
721
+ """The verdict twin of `_ExpectedCompletionAssertion`."""
722
+
685
723
  def when_called_with(self, *some_args: object, **some_kwargs: object) -> AssertionOutcome: ...
686
724
  @property
687
725
  def not_(self) -> Self: ...
@@ -16,6 +16,8 @@ from __future__ import annotations
16
16
  import dataclasses
17
17
  import datetime
18
18
  import decimal
19
+ import fractions
20
+ import inspect
19
21
  import math
20
22
  import numbers
21
23
  import pathlib
@@ -24,11 +26,21 @@ import uuid
24
26
  from dataclasses import dataclass
25
27
  from typing import TYPE_CHECKING, Any
26
28
 
27
- from ._introspection import is_mapping_like, is_model_dump_object
28
- from ._require import verdict
29
+ from ._introspection import (
30
+ KeyedValue,
31
+ as_held,
32
+ eq_keyed,
33
+ is_attrs_instance,
34
+ is_mapping_like,
35
+ is_model_dump_object,
36
+ kind_of,
37
+ model_field_values,
38
+ )
39
+ from ._ordering import nan_operand
40
+ from ._require import raised_inside, verdict
29
41
 
30
42
  if TYPE_CHECKING:
31
- from collections.abc import Callable
43
+ from collections.abc import Callable, Iterable
32
44
 
33
45
 
34
46
  _EQ_ATOMIC = frozenset(
@@ -161,33 +173,56 @@ def _find_ambiguous_operand(actual, expected, _seen=None):
161
173
  operand = _ambiguous_array_operand(actual, expected)
162
174
  if operand is not None:
163
175
  return operand
176
+ for left, right in _members_compared(actual, expected):
177
+ found = _find_ambiguous_operand(left, right, _seen)
178
+ if found is not None:
179
+ return found
180
+ return None
181
+
182
+
183
+ def _members_compared(actual: Any, expected: Any) -> Iterable[tuple[Any, Any]]:
184
+ """The pairs ``==`` walks into for two values of one container kind, and none for any other pair."""
164
185
  if is_mapping_like(actual) and is_mapping_like(expected):
165
186
  expected_keys = set(expected)
166
- for key in actual:
167
- if key in expected_keys:
168
- found = _find_ambiguous_operand(actual[key], expected[key], _seen)
169
- if found is not None:
170
- return found
171
- return None
187
+ return ((actual[key], expected[key]) for key in actual if key in expected_keys)
172
188
  if (
173
189
  dataclasses.is_dataclass(actual)
174
190
  and not isinstance(actual, type)
175
191
  and dataclasses.is_dataclass(expected)
176
192
  and not isinstance(expected, type)
177
193
  ):
178
- for field in dataclasses.fields(actual):
179
- found = _find_ambiguous_operand(getattr(actual, field.name), getattr(expected, field.name, None), _seen)
180
- if found is not None:
181
- return found
182
- return None
194
+ return (
195
+ (getattr(actual, one.name), getattr(expected, one.name, None))
196
+ for one in dataclasses.fields(actual)
197
+ if one.compare
198
+ )
199
+ if is_attrs_instance(actual) and is_attrs_instance(expected):
200
+ return _keyed_members(actual, expected)
183
201
  if is_model_dump_object(actual) and is_model_dump_object(expected):
184
- return _find_ambiguous_operand(actual.model_dump(), expected.model_dump(), _seen)
202
+ return ((model_field_values(actual), model_field_values(expected)),)
185
203
  if isinstance(actual, (list, tuple)) and isinstance(expected, (list, tuple)):
186
- for actual_item, expected_item in zip(actual, expected, strict=False):
187
- found = _find_ambiguous_operand(actual_item, expected_item, _seen)
188
- if found is not None:
189
- return found
190
- return None
204
+ return zip(actual, expected, strict=False)
205
+ return ()
206
+
207
+
208
+ def _keyed_members(actual: Any, expected: Any) -> tuple[tuple[Any, Any], ...]:
209
+ """The attrs fields ``==`` compares, each read through its key as ``==`` reads it, or none.
210
+
211
+ Only what ``==`` compares can have broken it. attrs reads every key before it compares a field, so a key
212
+ that raises is the error being explained and no array is to blame: read again here, it raised a second
213
+ error while the first was handled, and skipped, it let a later array take the blame.
214
+ """
215
+ try:
216
+ return tuple(
217
+ (
218
+ eq_keyed(one, getattr(actual, one.name)),
219
+ eq_keyed(one, getattr(expected, one.name)) if hasattr(expected, one.name) else None,
220
+ )
221
+ for one in actual.__attrs_attrs__
222
+ if one.eq is not False
223
+ )
224
+ except (TypeError, ValueError): # the two errors `_guarded_not_equal` hands to this search
225
+ return ()
191
226
 
192
227
 
193
228
  def _guarded_not_equal(actual, expected, *, method="is_equal_to") -> bool:
@@ -225,16 +260,73 @@ def _is_real_number(value) -> bool:
225
260
  def _within_tolerance(actual, expected, tolerance) -> bool:
226
261
  """Return whether two real numbers are within ``tolerance`` (absolute); ``NaN`` is never within.
227
262
 
228
- Only actual ``float`` operands are checked for ``NaN`` (other reals cannot be ``NaN``), which also keeps
229
- ``math.isnan`` off arbitrary-precision ``int`` values that would overflow it.
263
+ Checked by type rather than through `math.isnan`, which overflows on an arbitrary-precision ``int``
264
+ and signals on a `Decimal` NaN.
265
+
266
+ The subtraction is tried as written, so two floats keep the arithmetic they always had. A `Decimal`
267
+ against a ``float`` refuses to subtract at all and a bignum ``int`` overflows one, and those two pairs
268
+ are measured exactly instead, through `fractions.Fraction`.
230
269
  """
231
- if isinstance(actual, float) and math.isnan(actual):
232
- return False
233
- if isinstance(expected, float) and math.isnan(expected):
270
+ if nan_operand(actual) or nan_operand(expected):
234
271
  return False
235
272
  if actual == expected: # equal values (including inf == inf) are within any tolerance
236
273
  return True
237
- return abs(actual - expected) <= tolerance
274
+ if _is_infinite(actual) or _is_infinite(expected):
275
+ return False # unequal, and no distance from an infinity is within a finite tolerance
276
+ try:
277
+ return abs(actual - expected) <= tolerance
278
+ except (TypeError, OverflowError) as error:
279
+ if raised_inside(error): # their own `__sub__` or `__abs__` raised: a bug in the value, not a refusal
280
+ raise
281
+ return abs(fractions.Fraction(actual) - fractions.Fraction(expected)) <= fractions.Fraction(tolerance)
282
+
283
+
284
+ def _is_infinite(value) -> bool:
285
+ """A `float` or `Decimal` infinity, asked by type so nothing else is converted to a float to answer.
286
+
287
+ Through `Decimal`'s own method rather than the value's: a subclass overriding `is_infinite` would
288
+ otherwise decide this, and run its code before the arithmetic it is being asked about.
289
+ """
290
+ if isinstance(value, float):
291
+ return math.isinf(value)
292
+ return isinstance(value, decimal.Decimal) and decimal.Decimal.is_infinite(value)
293
+
294
+
295
+ class WindowRefusedError(TypeError):
296
+ """The two operands form no window: neither the arithmetic nor an exact conversion takes them.
297
+
298
+ A `TypeError` still, so a caller that let the operand's own refusal out keeps letting this one out,
299
+ and a matcher, which may not raise, can tell it from a `__sub__` of somebody's own that raised.
300
+ """
301
+
302
+
303
+ def tolerance_window(middle: Any, tolerance: Any) -> tuple[Any, Any]:
304
+ """The closed interval ``middle`` plus and minus ``tolerance``.
305
+
306
+ Typed as `Any` because the pairing is a run-time fact: the assertion has refused every combination
307
+ but two before it asks, a number against a number and a datetime against a timedelta, and a checker
308
+ reading the public signature alone is right to refuse the arithmetic. The matcher asks without
309
+ refusing anything first, which is what `WindowRefusedError` is for: it hands back the operands' own
310
+ refusal as something a matcher may answer "no match" to.
311
+ """
312
+ if _is_infinite(middle) and _is_real_number(tolerance):
313
+ # its own window, the way a float infinity already gets one: no distance from it is finite. The
314
+ # tolerance is still read, or an infinity paired with anything at all would answer "close enough"
315
+ return middle, middle
316
+ try:
317
+ return middle - tolerance, middle + tolerance
318
+ except TypeError as refusal:
319
+ if raised_inside(refusal): # their own `__sub__` raised: that is a bug in the value
320
+ raise
321
+ if not all(isinstance(operand, numbers.Number) for operand in (middle, tolerance)):
322
+ raise WindowRefusedError(str(refusal)) from None
323
+ # a `Decimal` refuses arithmetic with a `float` or a `Fraction`, which both convert exactly, while
324
+ # an infinite or NaN one does not convert at all
325
+ try:
326
+ exact, span = fractions.Fraction(middle), fractions.Fraction(tolerance)
327
+ except (TypeError, ValueError, OverflowError) as failed:
328
+ raise WindowRefusedError(str(failed)) from None
329
+ return exact - span, exact + span
238
330
 
239
331
 
240
332
  def _resolve_comparator(actual, config: _CompareConfig, *, field):
@@ -246,6 +338,7 @@ def _resolve_comparator(actual, config: _CompareConfig, *, field):
246
338
  comparators = config.comparators
247
339
  if comparators is None:
248
340
  return None
341
+ actual = as_held(actual)
249
342
  if field is not None and not isinstance(field, type) and field in comparators:
250
343
  return comparators[field]
251
344
  if type(actual) in comparators:
@@ -266,7 +359,7 @@ def _types_differ(actual, expected) -> bool:
266
359
  ``_is_matcher`` is imported here rather than at module scope because ``_matcher_impls`` imports
267
360
  ``_guarded_not_equal`` from this module, so the module-level import would be a cycle.
268
361
  """
269
- if type(actual) is type(expected):
362
+ if kind_of(actual) is kind_of(expected):
270
363
  return False
271
364
  from .._matcher_impls import _is_matcher
272
365
 
@@ -293,6 +386,51 @@ def _keyed_types_differ(actual, expected) -> bool:
293
386
  return False
294
387
 
295
388
 
389
+ def _kinds_never_equal(actual, expected) -> bool:
390
+ """Whether ``==`` rejects the pair by its kind alone, which a walk over the parts cannot see.
391
+
392
+ Under a compare config the walker decides by parts, so ``[1.0]`` against ``(1.0,)``, or two dataclasses
393
+ of different classes holding the same fields, had no differing part and passed, while ``==`` rejects
394
+ both outright. Asked only once ``==`` has already said no, so it never fails a pair ``==`` accepts.
395
+
396
+ The kind alone is not the answer: a list subclass may define ``__eq__`` to accept a tuple. So a pair
397
+ of different kinds counts only when both sides' own ``__eq__`` decline it, which leaves ``==`` at
398
+ identity whatever the parts hold. A pydantic model answers ``False`` rather than declining, so which of
399
+ class or fields decided cannot be told apart, and models are left to the walk.
400
+ """
401
+ if isinstance(actual, (list, tuple)) and isinstance(expected, (list, tuple)):
402
+ kinds_differ = isinstance(actual, list) is not isinstance(expected, list)
403
+ else:
404
+ both_dataclasses = all(
405
+ dataclasses.is_dataclass(side) and not isinstance(side, type) for side in (actual, expected)
406
+ )
407
+ both_attrs = is_attrs_instance(actual) and is_attrs_instance(expected)
408
+ kinds_differ = (both_dataclasses or both_attrs) and type(actual) is not type(expected)
409
+ return kinds_differ and _both_decline(actual, expected)
410
+
411
+
412
+ def _declines(value: Any, other: Any) -> bool:
413
+ """Whether *value*'s own ``__eq__`` answers ``NotImplemented`` for *other*, asked as ``==`` asks it.
414
+
415
+ Read statically off the type, since `type(x).__eq__` is an ordinary read that a metaclass answers,
416
+ and bound through the descriptor protocol, since that is what turns a function, a `staticmethod` or
417
+ anything else on the class into the callable the operator uses.
418
+ """
419
+ held: Any = inspect.getattr_static(type(value), "__eq__", None)
420
+ bind: Any = getattr(type(held), "__get__", None)
421
+ # measured against `==` itself: a callable that binds to nothing is called with the other side alone
422
+ asking = held if bind is None else bind(held, value, type(value))
423
+ return asking(other) is NotImplemented
424
+
425
+
426
+ def _both_decline(actual: Any, expected: Any) -> bool:
427
+ """Whether each side's own ``__eq__`` answers ``NotImplemented`` for the other."""
428
+ try:
429
+ return _declines(actual, expected) and _declines(expected, actual)
430
+ except Exception: # an `__eq__` that raises decides nothing here, and the walk goes on as before
431
+ return False
432
+
433
+
296
434
  def _node_decision(actual, expected, config: _CompareConfig | None, *, field=None, at_root: bool = False) -> str:
297
435
  """Classify a node as ``"equal"``, ``"leaf"``, ``"recurse"`` or ``"strict"``.
298
436
 
@@ -307,11 +445,12 @@ def _node_decision(actual, expected, config: _CompareConfig | None, *, field=Non
307
445
  `assertpy2._engine._diff._child_entries()` is the single place to know.
308
446
  """
309
447
  if config is not None:
310
- if config.ignore_null and field is not None and expected is None:
448
+ if config.ignore_null and field is not None and as_held(expected) is None:
311
449
  return "equal" # a named field the expected side leaves None is not compared
312
450
  comparator = _resolve_comparator(actual, config, field=field)
313
451
  if comparator is not None:
314
- return "equal" if verdict(comparator(actual, expected), subject="the comparator") else "leaf"
452
+ agreed = verdict(comparator(as_held(actual), as_held(expected)), subject="the comparator")
453
+ return "equal" if agreed else "leaf"
315
454
  if config.strict_types:
316
455
  if actual is expected and not at_root:
317
456
  # identity, free from `PyObject_RichCompareBool`. Not at the root, where it made `strict_types` weaker
@@ -325,9 +464,22 @@ def _node_decision(actual, expected, config: _CompareConfig | None, *, field=Non
325
464
  if type(actual) not in _EQ_ATOMIC and not _guarded_not_equal(actual, expected):
326
465
  # `[True] == [1]`: a container says nothing about the types inside it, so the walk keeps going
327
466
  return "strict"
328
- if config.tolerance is not None and _is_real_number(actual) and _is_real_number(expected):
329
- return "equal" if _within_tolerance(actual, expected, config.tolerance) else "leaf"
330
- return "recurse" if _guarded_not_equal(actual, expected) else "equal"
467
+ if config.tolerance is not None and _is_real_number(as_held(actual)) and _is_real_number(as_held(expected)):
468
+ # a keyed field's key still holds equal what the tolerance would not: it only ever loosens `==`
469
+ within = _within_tolerance(as_held(actual), as_held(expected), config.tolerance)
470
+ return "equal" if within or (type(actual) is KeyedValue and actual == expected) else "leaf"
471
+ return _plain_decision(actual, expected, config, at_root=at_root)
472
+
473
+
474
+ def _plain_decision(actual, expected, config: _CompareConfig | None, *, at_root: bool = False) -> str:
475
+ """``==``'s answer as a decision, where under a config a pair ruled out by its kind alone is a leaf."""
476
+ if actual is expected and not at_root:
477
+ # the same rule a container's own `==` applies to its members, and the verdict came from that `==`:
478
+ # without it the diff listed a NaN both sides hold as differing, and the hint blamed it
479
+ return "equal"
480
+ if not _guarded_not_equal(actual, expected):
481
+ return "equal"
482
+ return "leaf" if config is not None and _kinds_never_equal(actual, expected) else "recurse"
331
483
 
332
484
 
333
485
  def _spec_matches(key, value, specs) -> bool: