assertpy2 2.26.0__tar.gz → 2.27.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 (244) hide show
  1. {assertpy2-2.26.0 → assertpy2-2.27.0}/PKG-INFO +1 -1
  2. {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/_clustering.py +25 -2
  3. {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/_engine/_builder_check_typing.py +0 -10
  4. {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/_engine/_check_typing.py +38 -0
  5. {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/_engine/_compare.py +125 -9
  6. {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/_engine/_diff.py +15 -3
  7. {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/_engine/_equality.py +186 -40
  8. {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/_engine/_introspection.py +54 -3
  9. {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/_engine/_membership.py +117 -16
  10. {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/_engine/_mixin_base.py +1 -0
  11. {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/_engine/_negated_typing.py +501 -122
  12. {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/_engine/_ordering.py +57 -8
  13. assertpy2-2.27.0/assertpy2/_engine/_pairing.py +59 -0
  14. assertpy2-2.27.0/assertpy2/_engine/_poll_typing.py +5492 -0
  15. {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/_engine/_require.py +62 -9
  16. {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/_engine/_typing.py +91 -13
  17. {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/_hints.py +8 -1
  18. {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/_inline.py +9 -5
  19. {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/_matcher_impls.py +211 -78
  20. {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/_satisfies.py +40 -51
  21. {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/_snapshot_codec.py +23 -4
  22. {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/assertpy.py +39 -17
  23. {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/async_assertions.py +38 -9
  24. {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/base.py +26 -20
  25. {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/bytes_mixin.py +4 -2
  26. {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/collection.py +23 -29
  27. {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/contains.py +59 -28
  28. {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/dataframe.py +9 -1
  29. {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/date.py +27 -17
  30. {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/dict.py +5 -4
  31. {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/dynamic.py +7 -3
  32. {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/errors.py +35 -8
  33. {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/exception.py +82 -59
  34. {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/file.py +9 -8
  35. {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/helpers.py +23 -39
  36. {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/http_mixin.py +1 -1
  37. {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/json_mixin.py +31 -8
  38. {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/matchers.py +37 -1
  39. {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/numeric.py +37 -28
  40. {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/pytest_plugin.py +2 -0
  41. {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/snapshot.py +5 -1
  42. {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/string.py +43 -22
  43. {assertpy2-2.26.0 → assertpy2-2.27.0}/docs/concepts/stability.md +2 -2
  44. {assertpy2-2.26.0 → assertpy2-2.27.0}/docs/concepts/type-safety.md +8 -1
  45. {assertpy2-2.26.0 → assertpy2-2.27.0}/docs/extending/integrations.md +6 -2
  46. {assertpy2-2.26.0 → assertpy2-2.27.0}/docs/guides/assertions.md +3 -1
  47. {assertpy2-2.26.0 → assertpy2-2.27.0}/docs/guides/data.md +4 -1
  48. {assertpy2-2.26.0 → assertpy2-2.27.0}/docs/reference/async.md +8 -0
  49. {assertpy2-2.26.0 → assertpy2-2.27.0}/docs/reference/entry-points.md +18 -0
  50. {assertpy2-2.26.0 → assertpy2-2.27.0}/docs/reference/errors.md +12 -0
  51. {assertpy2-2.26.0 → assertpy2-2.27.0}/docs/reference/matchers.md +24 -0
  52. {assertpy2-2.26.0 → assertpy2-2.27.0}/pyproject.toml +21 -4
  53. {assertpy2-2.26.0 → assertpy2-2.27.0}/scripts/generate_poll_protocols.py +200 -20
  54. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/api_snapshot.json +489 -9
  55. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/poll_parity_baseline.py +6 -2
  56. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/pyright_baseline.py +17 -19
  57. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_async.py +150 -0
  58. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_bytes.py +6 -1
  59. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_capable_protocol.py +1 -1
  60. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_clustering.py +22 -2
  61. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_complexity.py +10 -7
  62. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_core.py +49 -1
  63. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_dataframe.py +39 -0
  64. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_datetime.py +72 -10
  65. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_dynamic.py +26 -0
  66. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_errors.py +31 -0
  67. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_evaluation_core.py +929 -5
  68. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_extensions.py +160 -0
  69. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_file.py +11 -0
  70. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_hints.py +35 -0
  71. assertpy2-2.27.0/tests/test_hostile_values.py +238 -0
  72. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_inline_snapshot.py +27 -0
  73. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_json.py +15 -3
  74. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_matcher_registry.py +21 -0
  75. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_negated_protocols.py +19 -3
  76. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_numbers.py +116 -0
  77. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_openapi_contract.py +87 -0
  78. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_pin_coverage.py +5 -0
  79. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_poll_protocols.py +101 -7
  80. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_property_based.py +130 -1
  81. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_protocol_parity.py +20 -3
  82. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_public_surface.py +57 -0
  83. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_pyright_baseline.py +1 -0
  84. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_pytest_plugin.py +21 -2
  85. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_recursive_compare_config.py +189 -1
  86. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_satisfy.py +104 -0
  87. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_snapshots.py +89 -0
  88. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_soft.py +80 -1
  89. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_surface_conformance.py +102 -0
  90. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_typing.py +25 -1
  91. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_typing_conformance.py +1 -1
  92. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_typing_negative.py +21 -7
  93. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_typing_promises.py +9 -0
  94. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_warn.py +15 -0
  95. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/typing_cases.py +63 -2
  96. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/typing_harness.py +25 -3
  97. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/typing_negative_baseline.py +72 -10
  98. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/typing_promises.py +54 -0
  99. assertpy2-2.26.0/assertpy2/_engine/_poll_typing.py +0 -2724
  100. {assertpy2-2.26.0 → assertpy2-2.27.0}/.gitignore +0 -0
  101. {assertpy2-2.26.0 → assertpy2-2.27.0}/CONTRIBUTING.md +0 -0
  102. {assertpy2-2.26.0 → assertpy2-2.27.0}/LICENSE +0 -0
  103. {assertpy2-2.26.0 → assertpy2-2.27.0}/README.md +0 -0
  104. {assertpy2-2.26.0 → assertpy2-2.27.0}/SECURITY.md +0 -0
  105. {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/__init__.py +0 -0
  106. {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/_dangling.py +0 -0
  107. {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/_engine/__init__.py +0 -0
  108. {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/_engine/_capable_typing.py +0 -0
  109. {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/_engine/_compat.py +0 -0
  110. {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/_engine/_contract.py +0 -0
  111. {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/_engine/_operations.py +0 -0
  112. {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/_engine/_path.py +0 -0
  113. {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/_engine/_size.py +0 -0
  114. {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/_engine/_text.py +0 -0
  115. {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/behave_matchers.py +0 -0
  116. {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/extracting.py +0 -0
  117. {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/outcome.py +0 -0
  118. {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/py.typed +0 -0
  119. {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/warning.py +0 -0
  120. {assertpy2-2.26.0 → assertpy2-2.27.0}/benchmarks/test_perf.py +0 -0
  121. {assertpy2-2.26.0 → assertpy2-2.27.0}/docs/assets/diff-equal.png +0 -0
  122. {assertpy2-2.26.0 → assertpy2-2.27.0}/docs/assets/diff-equal.svg +0 -0
  123. {assertpy2-2.26.0 → assertpy2-2.27.0}/docs/assets/diff-gallery.png +0 -0
  124. {assertpy2-2.26.0 → assertpy2-2.27.0}/docs/assets/diff-match.svg +0 -0
  125. {assertpy2-2.26.0 → assertpy2-2.27.0}/docs/assets/diff-sequence.svg +0 -0
  126. {assertpy2-2.26.0 → assertpy2-2.27.0}/docs/assets/diff-set.svg +0 -0
  127. {assertpy2-2.26.0 → assertpy2-2.27.0}/docs/assets/diff-string.svg +0 -0
  128. {assertpy2-2.26.0 → assertpy2-2.27.0}/docs/extending/custom-assertions.md +0 -0
  129. {assertpy2-2.26.0 → assertpy2-2.27.0}/docs/getting-started/comparison.md +0 -0
  130. {assertpy2-2.26.0 → assertpy2-2.27.0}/docs/getting-started/migration.md +0 -0
  131. {assertpy2-2.26.0 → assertpy2-2.27.0}/docs/getting-started/quickstart.md +0 -0
  132. {assertpy2-2.26.0 → assertpy2-2.27.0}/docs/guides/errors.md +0 -0
  133. {assertpy2-2.26.0 → assertpy2-2.27.0}/docs/guides/fluent.md +0 -0
  134. {assertpy2-2.26.0 → assertpy2-2.27.0}/docs/guides/matchers.md +0 -0
  135. {assertpy2-2.26.0 → assertpy2-2.27.0}/docs/guides/testing.md +0 -0
  136. {assertpy2-2.26.0 → assertpy2-2.27.0}/docs/index.md +0 -0
  137. {assertpy2-2.26.0 → assertpy2-2.27.0}/docs/logo-dark.svg +0 -0
  138. {assertpy2-2.26.0 → assertpy2-2.27.0}/docs/logo.svg +0 -0
  139. {assertpy2-2.26.0 → assertpy2-2.27.0}/docs/recipes.md +0 -0
  140. {assertpy2-2.26.0 → assertpy2-2.27.0}/docs/reference/bytes.md +0 -0
  141. {assertpy2-2.26.0 → assertpy2-2.27.0}/docs/reference/collections.md +0 -0
  142. {assertpy2-2.26.0 → assertpy2-2.27.0}/docs/reference/containment.md +0 -0
  143. {assertpy2-2.26.0 → assertpy2-2.27.0}/docs/reference/core.md +0 -0
  144. {assertpy2-2.26.0 → assertpy2-2.27.0}/docs/reference/dataframes.md +0 -0
  145. {assertpy2-2.26.0 → assertpy2-2.27.0}/docs/reference/dates.md +0 -0
  146. {assertpy2-2.26.0 → assertpy2-2.27.0}/docs/reference/dicts.md +0 -0
  147. {assertpy2-2.26.0 → assertpy2-2.27.0}/docs/reference/dynamic.md +0 -0
  148. {assertpy2-2.26.0 → assertpy2-2.27.0}/docs/reference/exceptions.md +0 -0
  149. {assertpy2-2.26.0 → assertpy2-2.27.0}/docs/reference/extracting.md +0 -0
  150. {assertpy2-2.26.0 → assertpy2-2.27.0}/docs/reference/files.md +0 -0
  151. {assertpy2-2.26.0 → assertpy2-2.27.0}/docs/reference/http.md +0 -0
  152. {assertpy2-2.26.0 → assertpy2-2.27.0}/docs/reference/json.md +0 -0
  153. {assertpy2-2.26.0 → assertpy2-2.27.0}/docs/reference/numbers.md +0 -0
  154. {assertpy2-2.26.0 → assertpy2-2.27.0}/docs/reference/overview.md +0 -0
  155. {assertpy2-2.26.0 → assertpy2-2.27.0}/docs/reference/snapshots.md +0 -0
  156. {assertpy2-2.26.0 → assertpy2-2.27.0}/docs/reference/strings.md +0 -0
  157. {assertpy2-2.26.0 → assertpy2-2.27.0}/docs/reference/warnings.md +0 -0
  158. {assertpy2-2.26.0 → assertpy2-2.27.0}/docs/stylesheets/extra.css +0 -0
  159. {assertpy2-2.26.0 → assertpy2-2.27.0}/mkdocs.yml +0 -0
  160. {assertpy2-2.26.0 → assertpy2-2.27.0}/mkdocs_hooks.py +0 -0
  161. {assertpy2-2.26.0 → assertpy2-2.27.0}/scripts/generate_check_protocols.py +0 -0
  162. {assertpy2-2.26.0 → assertpy2-2.27.0}/scripts/mutation_report.py +0 -0
  163. {assertpy2-2.26.0 → assertpy2-2.27.0}/scripts/regenerate.py +0 -0
  164. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/api_surface.py +0 -0
  165. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/conftest.py +0 -0
  166. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/docs_fixtures.py +0 -0
  167. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/group_compat.py +0 -0
  168. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/poll_parity_cases.py +0 -0
  169. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_api_compatibility.py +0 -0
  170. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_api_vocabulary.py +0 -0
  171. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_architecture_doc.py +0 -0
  172. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_async_predicates.py +0 -0
  173. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_attrs_support.py +0 -0
  174. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_behave_matchers.py +0 -0
  175. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_bool.py +0 -0
  176. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_boundary_cases.py +0 -0
  177. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_callable.py +0 -0
  178. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_chaining.py +0 -0
  179. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_check.py +0 -0
  180. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_check_protocols.py +0 -0
  181. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_class.py +0 -0
  182. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_collection.py +0 -0
  183. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_custom_dict.py +0 -0
  184. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_custom_list.py +0 -0
  185. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_dangling.py +0 -0
  186. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_declared_floors.py +0 -0
  187. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_description.py +0 -0
  188. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_dict.py +0 -0
  189. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_dict_compare.py +0 -0
  190. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_docs_examples.py +0 -0
  191. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_docs_typing.py +0 -0
  192. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_equals.py +0 -0
  193. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_exception_context.py +0 -0
  194. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_expected_contract.py +0 -0
  195. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_expected_exception.py +0 -0
  196. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_expected_warning.py +0 -0
  197. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_extracting.py +0 -0
  198. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_fail.py +0 -0
  199. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_grouped_soft.py +0 -0
  200. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_http.py +0 -0
  201. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_in.py +0 -0
  202. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_inline_record.py +0 -0
  203. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_iterable_cluster.py +0 -0
  204. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_list.py +0 -0
  205. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_match_result.py +0 -0
  206. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_matcher_parity.py +0 -0
  207. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_matchers.py +0 -0
  208. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_message_elision.py +0 -0
  209. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_namedtuple.py +0 -0
  210. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_none.py +0 -0
  211. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_not.py +0 -0
  212. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_operation_contract.py +0 -0
  213. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_optional_integration_contracts.py +0 -0
  214. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_overload_order.py +0 -0
  215. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_overloads.py +0 -0
  216. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_pipeline.py +0 -0
  217. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_poll_parity.py +0 -0
  218. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_property_attrs.py +0 -0
  219. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_recursive_assertion.py +0 -0
  220. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_recursive_compare.py +0 -0
  221. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_recursive_compare_attrs.py +0 -0
  222. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_recursive_compare_pydantic.py +0 -0
  223. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_refusals.py +0 -0
  224. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_regex_groups.py +0 -0
  225. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_requirement.py +0 -0
  226. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_rich_diff.py +0 -0
  227. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_same_as.py +0 -0
  228. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_skip_guard.py +0 -0
  229. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_soft_fail.py +0 -0
  230. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_string.py +0 -0
  231. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_structural.py +0 -0
  232. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_sync_eventually.py +0 -0
  233. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_traceback.py +0 -0
  234. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_type.py +0 -0
  235. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_type_expression_failures.py +0 -0
  236. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_typing_claims.py +0 -0
  237. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_typing_completeness.py +0 -0
  238. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_typing_from_a_wheel.py +0 -0
  239. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_typing_http.py +0 -0
  240. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_typing_integrations.py +0 -0
  241. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_vacuity_contract.py +0 -0
  242. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/typing_http.py +0 -0
  243. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/typing_integrations.py +0 -0
  244. {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/typing_integrations_baseline.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: assertpy2
3
- Version: 2.26.0
3
+ Version: 2.27.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
@@ -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
@@ -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,8 +26,9 @@ 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 is_attrs_instance, is_mapping_like, is_model_dump_object
30
+ from ._ordering import nan_operand
31
+ from ._require import raised_inside, verdict
29
32
 
30
33
  if TYPE_CHECKING:
31
34
  from collections.abc import Callable
@@ -225,16 +228,73 @@ def _is_real_number(value) -> bool:
225
228
  def _within_tolerance(actual, expected, tolerance) -> bool:
226
229
  """Return whether two real numbers are within ``tolerance`` (absolute); ``NaN`` is never within.
227
230
 
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.
231
+ Checked by type rather than through `math.isnan`, which overflows on an arbitrary-precision ``int``
232
+ and signals on a `Decimal` NaN.
233
+
234
+ The subtraction is tried as written, so two floats keep the arithmetic they always had. A `Decimal`
235
+ against a ``float`` refuses to subtract at all and a bignum ``int`` overflows one, and those two pairs
236
+ are measured exactly instead, through `fractions.Fraction`.
230
237
  """
231
- if isinstance(actual, float) and math.isnan(actual):
232
- return False
233
- if isinstance(expected, float) and math.isnan(expected):
238
+ if nan_operand(actual) or nan_operand(expected):
234
239
  return False
235
240
  if actual == expected: # equal values (including inf == inf) are within any tolerance
236
241
  return True
237
- return abs(actual - expected) <= tolerance
242
+ if _is_infinite(actual) or _is_infinite(expected):
243
+ return False # unequal, and no distance from an infinity is within a finite tolerance
244
+ try:
245
+ return abs(actual - expected) <= tolerance
246
+ except (TypeError, OverflowError) as error:
247
+ if raised_inside(error): # their own `__sub__` or `__abs__` raised: a bug in the value, not a refusal
248
+ raise
249
+ return abs(fractions.Fraction(actual) - fractions.Fraction(expected)) <= fractions.Fraction(tolerance)
250
+
251
+
252
+ def _is_infinite(value) -> bool:
253
+ """A `float` or `Decimal` infinity, asked by type so nothing else is converted to a float to answer.
254
+
255
+ Through `Decimal`'s own method rather than the value's: a subclass overriding `is_infinite` would
256
+ otherwise decide this, and run its code before the arithmetic it is being asked about.
257
+ """
258
+ if isinstance(value, float):
259
+ return math.isinf(value)
260
+ return isinstance(value, decimal.Decimal) and decimal.Decimal.is_infinite(value)
261
+
262
+
263
+ class WindowRefusedError(TypeError):
264
+ """The two operands form no window: neither the arithmetic nor an exact conversion takes them.
265
+
266
+ A `TypeError` still, so a caller that let the operand's own refusal out keeps letting this one out,
267
+ and a matcher, which may not raise, can tell it from a `__sub__` of somebody's own that raised.
268
+ """
269
+
270
+
271
+ def tolerance_window(middle: Any, tolerance: Any) -> tuple[Any, Any]:
272
+ """The closed interval ``middle`` plus and minus ``tolerance``.
273
+
274
+ Typed as `Any` because the pairing is a run-time fact: the assertion has refused every combination
275
+ but two before it asks, a number against a number and a datetime against a timedelta, and a checker
276
+ reading the public signature alone is right to refuse the arithmetic. The matcher asks without
277
+ refusing anything first, which is what `WindowRefusedError` is for: it hands back the operands' own
278
+ refusal as something a matcher may answer "no match" to.
279
+ """
280
+ if _is_infinite(middle) and _is_real_number(tolerance):
281
+ # its own window, the way a float infinity already gets one: no distance from it is finite. The
282
+ # tolerance is still read, or an infinity paired with anything at all would answer "close enough"
283
+ return middle, middle
284
+ try:
285
+ return middle - tolerance, middle + tolerance
286
+ except TypeError as refusal:
287
+ if raised_inside(refusal): # their own `__sub__` raised: that is a bug in the value
288
+ raise
289
+ if not all(isinstance(operand, numbers.Number) for operand in (middle, tolerance)):
290
+ raise WindowRefusedError(str(refusal)) from None
291
+ # a `Decimal` refuses arithmetic with a `float` or a `Fraction`, which both convert exactly, while
292
+ # an infinite or NaN one does not convert at all
293
+ try:
294
+ exact, span = fractions.Fraction(middle), fractions.Fraction(tolerance)
295
+ except (TypeError, ValueError, OverflowError) as failed:
296
+ raise WindowRefusedError(str(failed)) from None
297
+ return exact - span, exact + span
238
298
 
239
299
 
240
300
  def _resolve_comparator(actual, config: _CompareConfig, *, field):
@@ -293,6 +353,51 @@ def _keyed_types_differ(actual, expected) -> bool:
293
353
  return False
294
354
 
295
355
 
356
+ def _kinds_never_equal(actual, expected) -> bool:
357
+ """Whether ``==`` rejects the pair by its kind alone, which a walk over the parts cannot see.
358
+
359
+ Under a compare config the walker decides by parts, so ``[1.0]`` against ``(1.0,)``, or two dataclasses
360
+ of different classes holding the same fields, had no differing part and passed, while ``==`` rejects
361
+ both outright. Asked only once ``==`` has already said no, so it never fails a pair ``==`` accepts.
362
+
363
+ The kind alone is not the answer: a list subclass may define ``__eq__`` to accept a tuple. So a pair
364
+ of different kinds counts only when both sides' own ``__eq__`` decline it, which leaves ``==`` at
365
+ identity whatever the parts hold. A pydantic model answers ``False`` rather than declining, so which of
366
+ class or fields decided cannot be told apart, and models are left to the walk.
367
+ """
368
+ if isinstance(actual, (list, tuple)) and isinstance(expected, (list, tuple)):
369
+ kinds_differ = isinstance(actual, list) is not isinstance(expected, list)
370
+ else:
371
+ both_dataclasses = all(
372
+ dataclasses.is_dataclass(side) and not isinstance(side, type) for side in (actual, expected)
373
+ )
374
+ both_attrs = is_attrs_instance(actual) and is_attrs_instance(expected)
375
+ kinds_differ = (both_dataclasses or both_attrs) and type(actual) is not type(expected)
376
+ return kinds_differ and _both_decline(actual, expected)
377
+
378
+
379
+ def _declines(value: Any, other: Any) -> bool:
380
+ """Whether *value*'s own ``__eq__`` answers ``NotImplemented`` for *other*, asked as ``==`` asks it.
381
+
382
+ Read statically off the type, since `type(x).__eq__` is an ordinary read that a metaclass answers,
383
+ and bound through the descriptor protocol, since that is what turns a function, a `staticmethod` or
384
+ anything else on the class into the callable the operator uses.
385
+ """
386
+ held: Any = inspect.getattr_static(type(value), "__eq__", None)
387
+ bind: Any = getattr(type(held), "__get__", None)
388
+ # measured against `==` itself: a callable that binds to nothing is called with the other side alone
389
+ asking = held if bind is None else bind(held, value, type(value))
390
+ return asking(other) is NotImplemented
391
+
392
+
393
+ def _both_decline(actual: Any, expected: Any) -> bool:
394
+ """Whether each side's own ``__eq__`` answers ``NotImplemented`` for the other."""
395
+ try:
396
+ return _declines(actual, expected) and _declines(expected, actual)
397
+ except Exception: # an `__eq__` that raises decides nothing here, and the walk goes on as before
398
+ return False
399
+
400
+
296
401
  def _node_decision(actual, expected, config: _CompareConfig | None, *, field=None, at_root: bool = False) -> str:
297
402
  """Classify a node as ``"equal"``, ``"leaf"``, ``"recurse"`` or ``"strict"``.
298
403
 
@@ -327,7 +432,18 @@ def _node_decision(actual, expected, config: _CompareConfig | None, *, field=Non
327
432
  return "strict"
328
433
  if config.tolerance is not None and _is_real_number(actual) and _is_real_number(expected):
329
434
  return "equal" if _within_tolerance(actual, expected, config.tolerance) else "leaf"
330
- return "recurse" if _guarded_not_equal(actual, expected) else "equal"
435
+ return _plain_decision(actual, expected, config, at_root=at_root)
436
+
437
+
438
+ def _plain_decision(actual, expected, config: _CompareConfig | None, *, at_root: bool = False) -> str:
439
+ """``==``'s answer as a decision, where under a config a pair ruled out by its kind alone is a leaf."""
440
+ if actual is expected and not at_root:
441
+ # the same rule a container's own `==` applies to its members, and the verdict came from that `==`:
442
+ # without it the diff listed a NaN both sides hold as differing, and the hint blamed it
443
+ return "equal"
444
+ if not _guarded_not_equal(actual, expected):
445
+ return "equal"
446
+ return "leaf" if config is not None and _kinds_never_equal(actual, expected) else "recurse"
331
447
 
332
448
 
333
449
  def _spec_matches(key, value, specs) -> bool:
@@ -27,6 +27,7 @@ or answered for the wrong one. Ask the walker and read its answer instead: ``No
27
27
 
28
28
  from __future__ import annotations
29
29
 
30
+ import collections
30
31
  import dataclasses
31
32
  import difflib
32
33
  from typing import TYPE_CHECKING, TypeVar
@@ -48,7 +49,7 @@ def _field_dict(obj, is_model):
48
49
  """Field mapping of a pydantic-style model (``model_dump()``) or an attrs instance (shallow)."""
49
50
  if is_model:
50
51
  return obj.model_dump()
51
- return {field.name: getattr(obj, field.name) for field in obj.__attrs_attrs__}
52
+ return {field.name: getattr(obj, field.name) for field in obj.__attrs_attrs__ if field.eq is not False}
52
53
 
53
54
 
54
55
  def _child_entries(actual, expected, path: _Path, *, descended_for, _seen=None, config=None) -> list[DiffEntry]:
@@ -329,8 +330,9 @@ def _dataclass_diff_entries(actual, expected, prefix: _Path, seen, config=None)
329
330
  so both report dataclass fields identically.
330
331
  """
331
332
  entries: list[DiffEntry] = []
332
- actual_names = [field.name for field in dataclasses.fields(actual)]
333
- expected_names = [field.name for field in dataclasses.fields(expected)]
333
+ # a field declared `compare=False` is outside the dataclass's own `==`, so it stays outside here
334
+ actual_names = [field.name for field in dataclasses.fields(actual) if field.compare]
335
+ expected_names = [field.name for field in dataclasses.fields(expected) if field.compare]
334
336
  in_actual, in_expected = set(actual_names), set(expected_names)
335
337
  for field in _ordered_keys(actual_names, expected_names):
336
338
  if field not in in_expected:
@@ -492,6 +494,7 @@ def _mapping_diff_entries(actual, expected, prefix: _Path, child_seen: set[int],
492
494
  entries: list[DiffEntry] = []
493
495
  actual_keys = set(kept)
494
496
  expected_keys = set(kept_expected)
497
+ entries.extend(_order_entries(actual, expected, kept, kept_expected, prefix))
495
498
  if config is not None and config.strict_types:
496
499
  # `{True} & {1}` hands back whichever side the set drew from, losing the type that differs
497
500
  stored = {key: key for key in kept_expected}
@@ -522,6 +525,15 @@ def _mapping_diff_entries(actual, expected, prefix: _Path, child_seen: set[int],
522
525
  return entries
523
526
 
524
527
 
528
+ def _order_entries(actual, expected, kept, kept_expected, prefix: _Path) -> list[DiffEntry]:
529
+ """The key order of two `OrderedDict` values holding the same keys in different places, which their `==` reads."""
530
+ if not (isinstance(actual, collections.OrderedDict) and isinstance(expected, collections.OrderedDict)):
531
+ return []
532
+ if set(kept) != set(kept_expected) or list(kept) == list(kept_expected):
533
+ return []
534
+ return [prefix.leaf_entry(actual=list(kept), expected=list(kept_expected))]
535
+
536
+
525
537
  def _sub_diff_entries(
526
538
  actual: object, expected: object, prefix: _Path = _ROOT, *, _seen: set[int] | None = None, config=None
527
539
  ) -> list[DiffEntry] | None:
@@ -14,13 +14,27 @@ calling `self._dict_not_equal(...)` still works, and the matcher calls the same
14
14
 
15
15
  from __future__ import annotations
16
16
 
17
+ import collections
17
18
  import collections.abc
19
+ import dataclasses
20
+ import datetime
21
+ import enum
22
+ import numbers
23
+ import pathlib
18
24
  import re
19
25
  import types
20
- from typing import TYPE_CHECKING, cast
21
-
22
- from ._compare import _guarded_not_equal, _keyed_types_differ, _node_decision, _spec_matches
26
+ import uuid
27
+ from typing import TYPE_CHECKING, Any, cast
28
+
29
+ from ._compare import (
30
+ _guarded_not_equal,
31
+ _keyed_types_differ,
32
+ _kinds_never_equal,
33
+ _node_decision,
34
+ _spec_matches,
35
+ )
23
36
  from ._diff import _sub_diff_entries
37
+ from ._introspection import is_attrs_instance, is_model_dump_object, is_namedtuple
24
38
  from ._path import _ROOT
25
39
  from ._require import refuse
26
40
 
@@ -46,6 +60,80 @@ def normalize_key_specs(specs: object, param: str) -> list:
46
60
  refuse(specs, "a key, a nested-path tuple, or a list/set/frozenset of them", subject=param)
47
61
 
48
62
 
63
+ def key_specs_given(specs: object) -> bool:
64
+ """Whether an ``ignore``/``include`` argument asks for anything, a falsy key such as ``0`` or ``""`` included.
65
+
66
+ Truthiness answered this and dropped a single falsy key in silence. An empty collection still asks
67
+ for nothing, as it always did.
68
+ """
69
+ return specs is not None and not (isinstance(specs, (list, set, frozenset)) and not specs)
70
+
71
+
72
+ def comparable_fields(obj: object) -> dict | None:
73
+ """An object with introspectable fields as a dict of them, or ``None`` when it has no fields to compare.
74
+
75
+ Dataclasses are converted by reference rather than through `dataclasses.asdict`, which deep-copies and
76
+ crashes on a field that cannot be copied, and a field declared ``compare=False`` is left out, as the
77
+ dataclass's own ``==`` leaves it out. An attrs field declared ``eq=False`` is left out the same way.
78
+
79
+ A value of a builtin kind is not a bag of fields even when it carries a ``__dict__``: a subclass of
80
+ `Decimal` or `str` has an empty one, and reading it made every two such values compare equal.
81
+ """
82
+ if dataclasses.is_dataclass(obj) and not isinstance(obj, type):
83
+ return cast("dict", _shallow_fields(obj))
84
+ if is_namedtuple(obj):
85
+ return dict(obj._asdict())
86
+ if is_model_dump_object(obj):
87
+ return obj.model_dump()
88
+ if is_attrs_instance(obj):
89
+ # deferred: at module level it cost 8.5 ms and 22 modules of a 39.8 ms import wherever attrs is installed
90
+ import attrs
91
+
92
+ return attrs.asdict(obj, filter=lambda attribute, _value: attribute.eq is not False)
93
+ builtin_kinds = (
94
+ type,
95
+ numbers.Number,
96
+ str,
97
+ bytes,
98
+ bytearray,
99
+ datetime.date,
100
+ datetime.time,
101
+ datetime.timedelta,
102
+ enum.Enum,
103
+ uuid.UUID,
104
+ pathlib.PurePath,
105
+ )
106
+ if hasattr(obj, "__dict__") and not isinstance(obj, builtin_kinds):
107
+ return dict(vars(obj))
108
+ return None
109
+
110
+
111
+ def _shallow_fields(node: Any) -> Any:
112
+ """`dataclasses.asdict` by reference, without the copy, and without the fields ``==`` leaves out."""
113
+ if dataclasses.is_dataclass(node) and not isinstance(node, type):
114
+ return {
115
+ field.name: _shallow_fields(getattr(node, field.name))
116
+ for field in dataclasses.fields(node)
117
+ if field.compare
118
+ }
119
+ if isinstance(node, tuple) and hasattr(node, "_fields"):
120
+ return type(node)(*[_shallow_fields(item) for item in node])
121
+ if isinstance(node, (list, tuple)):
122
+ return type(node)(_shallow_fields(item) for item in node)
123
+ if isinstance(node, dict):
124
+ return {_shallow_fields(key): _shallow_fields(value) for key, value in node.items()}
125
+ return node
126
+
127
+
128
+ def _as_fields(value: object) -> dict | None:
129
+ """A plain dict as itself, anything else through `comparable_fields`."""
130
+ return value if isinstance(value, dict) else comparable_fields(value)
131
+
132
+
133
+ def _plain_sequence(value: object) -> bool:
134
+ return isinstance(value, (list, tuple)) and not is_namedtuple(value)
135
+
136
+
49
137
  def ignore_specs(ignore: object) -> list:
50
138
  """Ignore-specs, keeping nested paths whole: a one-element tuple is just that key."""
51
139
  return [
@@ -193,6 +281,8 @@ def values_differ(value: object, other: object, config: _CompareConfig | None, *
193
281
  return False
194
282
  if config is None:
195
283
  return _guarded_not_equal(value, other)
284
+ if _kinds_never_equal(value, other) and _guarded_not_equal(value, other):
285
+ return True
196
286
  entries = _sub_diff_entries(value, other, _ROOT, config=config)
197
287
  if entries is None:
198
288
  # a leaf the walker does not decompose: `strict_types` asked here called two equal sets unequal
@@ -246,36 +336,32 @@ def mapping_differs(
246
336
  return False
247
337
  seen = seen | {pair}
248
338
 
249
- if not (ignore or include or config is not None):
339
+ ignoring, including = key_specs_given(ignore), key_specs_given(include)
340
+ if not (ignoring or including or config is not None):
250
341
  return _guarded_not_equal(actual, expected)
251
342
 
252
- ignores = ignore_specs(ignore) if ignore else []
253
- if ignore or include:
254
- includes = include_specs(include) if include else []
255
- if include:
256
- missing = missing_include_keys(left, includes)
257
- if missing:
258
- raise IncludeKeysMissingError(left, includes, missing)
259
- keys_in_actual = {
260
- key
261
- for key in left
262
- if (not ignore or not _spec_matches(key, left[key], ignores))
263
- and (not include or _spec_matches(key, left[key], includes))
264
- }
265
- keys_in_expected = {
266
- key
267
- for key in right
268
- if (not ignore or not _spec_matches(key, right[key], ignores))
269
- and (not include or _spec_matches(key, right[key], includes))
270
- }
271
- else:
272
- keys_in_actual = set(left)
273
- keys_in_expected = set(right)
274
-
275
- if keys_in_actual != keys_in_expected:
343
+ ignores = ignore_specs(ignore) if ignoring else []
344
+ includes = include_specs(include) if including else []
345
+ # read once for the whole mapping: asked per key, a five-hundred path include normalised itself five
346
+ # hundred times. After `include_specs`, so a one-shot iterable is still refused under its own name
347
+ nested_paths = ignore_specs(include) if including else []
348
+ if includes:
349
+ missing = missing_include_keys(left, includes)
350
+ if missing:
351
+ raise IncludeKeysMissingError(left, includes, missing)
352
+ keys_in_actual = _kept_keys(left, ignores, includes)
353
+ keys_in_expected = _kept_keys(right, ignores, includes)
354
+ if keys_in_actual != keys_in_expected or _order_differs(actual, expected, keys_in_actual):
276
355
  return True
277
- if config is not None and config.strict_types and _keyed_types_differ(actual, expected):
278
- # `{True: "a"}` and `{1: "a"}` are equal to Python and not under strict types; the walk below sees only values
356
+ if (
357
+ config is not None
358
+ and config.strict_types
359
+ and _keyed_types_differ(
360
+ dict.fromkeys(key for key in left if key in keys_in_actual),
361
+ dict.fromkeys(key for key in right if key in keys_in_expected),
362
+ )
363
+ ):
364
+ # `{True: "a"}` and `{1: "a"}` are equal to Python and not under strict types; only the keys still compared
279
365
  return True
280
366
  for key in keys_in_actual:
281
367
  nested_left, nested_right = left[key], right[key]
@@ -285,18 +371,78 @@ def mapping_differs(
285
371
  continue
286
372
  if decision == "leaf":
287
373
  return True
288
- nested_ignore = [entry[1:] for entry in ignores if type(entry) is tuple and entry[0] == key] if ignore else None
374
+ nested_ignore = (
375
+ [entry[1:] for entry in ignores if type(entry) is tuple and entry[0] == key] if ignoring else None
376
+ )
289
377
  # the nested half of an include keeps whole paths, and the level above already consumed the first segment
290
378
  nested_include = (
291
- [entry[1:] for entry in ignore_specs(include) if type(entry) is tuple and entry[0] == key]
292
- if include
293
- else None
379
+ [entry[1:] for entry in nested_paths if type(entry) is tuple and entry[0] == key] if including else None
294
380
  )
295
- if mapping_shaped(nested_left, check_values=False) and mapping_shaped(nested_right, check_values=False):
296
- if mapping_differs(
297
- nested_left, nested_right, ignore=nested_ignore, include=nested_include, config=config, seen=seen
298
- ):
299
- return True
300
- elif values_differ(nested_left, nested_right, config):
381
+ if _nested_differs(
382
+ nested_left, nested_right, ignore=nested_ignore, include=nested_include, config=config, seen=seen
383
+ ):
301
384
  return True
302
385
  return False
386
+
387
+
388
+ def _kept_keys(mapping: MappingLike, ignores: list, includes: list) -> set:
389
+ """The keys a comparison looks at: those no ignore-spec names, and an include-spec does when there are any."""
390
+ return {
391
+ key
392
+ for key in mapping
393
+ if not (ignores and _spec_matches(key, mapping[key], ignores))
394
+ and (not includes or _spec_matches(key, mapping[key], includes))
395
+ }
396
+
397
+
398
+ def _order_differs(actual: object, expected: object, kept: set) -> bool:
399
+ """Whether two `OrderedDict` values hold the compared keys in different orders, which their `==` reads."""
400
+ if not (isinstance(actual, collections.OrderedDict) and isinstance(expected, collections.OrderedDict)):
401
+ return False
402
+ return [key for key in actual if key in kept] != [key for key in expected if key in kept]
403
+
404
+
405
+ def _nested_differs(
406
+ left: object, right: object, *, ignore: object, include: object, config: _CompareConfig | None, seen: frozenset
407
+ ) -> bool:
408
+ """One value under a key, compared with the rest of the key path that reaches into it."""
409
+ if mapping_shaped(left, check_values=False) and mapping_shaped(right, check_values=False):
410
+ return mapping_differs(left, right, ignore=ignore, include=include, config=config, seen=seen)
411
+ if key_specs_given(ignore) or key_specs_given(include):
412
+ # a path that goes on into a dataclass, a model or an object is followed through its fields
413
+ left_fields, right_fields = comparable_fields(left), comparable_fields(right)
414
+ if left_fields is not None and right_fields is not None:
415
+ return mapping_differs(left_fields, right_fields, ignore=ignore, include=include, config=config, seen=seen)
416
+ return values_differ(left, right, config)
417
+
418
+
419
+ def filtered_differs(
420
+ actual: object, expected: object, *, ignore: object, include: object, config: _CompareConfig | None
421
+ ) -> bool:
422
+ """`is_equal_to`'s verdict under ``ignore``/``include`` for a pair that is not two mappings.
423
+
424
+ The builder takes a sequence pair element by element and anything else through its fields. A matcher
425
+ answering the same question takes the same route, or `match.equal_to(user, ignore="updated_at")`
426
+ compared ``updated_at`` anyway. An element with no fields is compared whole, as the builder compares
427
+ it. At the top a value with no fields is one the builder refuses with a `TypeError`, and a matcher,
428
+ which must not raise, answers that it differs.
429
+ """
430
+ if _plain_sequence(actual) and _plain_sequence(expected):
431
+ sequence_actual = cast("list | tuple", actual)
432
+ sequence_expected = cast("list | tuple", expected)
433
+ if len(sequence_actual) != len(sequence_expected):
434
+ return True
435
+ return any(
436
+ _filtered_pair_differs(item, counterpart, ignore=ignore, include=include, config=config, at_root=False)
437
+ for item, counterpart in zip(sequence_actual, sequence_expected, strict=True)
438
+ )
439
+ return _filtered_pair_differs(actual, expected, ignore=ignore, include=include, config=config, at_root=True)
440
+
441
+
442
+ def _filtered_pair_differs(
443
+ actual: object, expected: object, *, ignore: object, include: object, config: _CompareConfig | None, at_root: bool
444
+ ) -> bool:
445
+ left, right = _as_fields(actual), _as_fields(expected)
446
+ if left is None or right is None:
447
+ return at_root or values_differ(actual, expected, config)
448
+ return mapping_differs(left, right, ignore=ignore, include=include, config=config)