assertpy2 2.30.0__tar.gz → 2.31.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 (300) hide show
  1. {assertpy2-2.30.0 → assertpy2-2.31.0}/PKG-INFO +1 -1
  2. {assertpy2-2.30.0 → assertpy2-2.31.0}/assertpy2/_engine/_contract.py +199 -71
  3. {assertpy2-2.30.0 → assertpy2-2.31.0}/assertpy2/_snapshot_codec.py +3 -2
  4. {assertpy2-2.30.0 → assertpy2-2.31.0}/assertpy2/assertpy.py +232 -40
  5. {assertpy2-2.30.0 → assertpy2-2.31.0}/assertpy2/errors.py +18 -0
  6. {assertpy2-2.30.0 → assertpy2-2.31.0}/assertpy2/json_mixin.py +9 -7
  7. {assertpy2-2.30.0 → assertpy2-2.31.0}/assertpy2/pytest_plugin.py +391 -58
  8. {assertpy2-2.30.0 → assertpy2-2.31.0}/assertpy2/snapshot.py +2 -2
  9. {assertpy2-2.30.0 → assertpy2-2.31.0}/docs/concepts/type-safety.md +10 -2
  10. {assertpy2-2.30.0 → assertpy2-2.31.0}/docs/extending/integrations.md +4 -0
  11. {assertpy2-2.30.0 → assertpy2-2.31.0}/docs/guides/errors.md +63 -4
  12. {assertpy2-2.30.0 → assertpy2-2.31.0}/pyproject.toml +43 -4
  13. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/api_snapshot.json +7 -0
  14. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/conftest.py +4 -0
  15. assertpy2-2.31.0/tests/test_exact_oracle.py +180 -0
  16. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_pytest_plugin.py +782 -8
  17. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_snapshots.py +52 -4
  18. assertpy2-2.31.0/tests/test_soft_summary_blocks.py +356 -0
  19. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_structural.py +445 -27
  20. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_typing.py +3 -2
  21. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_typing_chain_corpus.py +2 -2
  22. assertpy2-2.31.0/tests/test_warn_on_failure.py +158 -0
  23. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/typing_chain_corpus_baseline.json +0 -198
  24. {assertpy2-2.30.0 → assertpy2-2.31.0}/.gitignore +0 -0
  25. {assertpy2-2.30.0 → assertpy2-2.31.0}/CONTRIBUTING.md +0 -0
  26. {assertpy2-2.30.0 → assertpy2-2.31.0}/LICENSE +0 -0
  27. {assertpy2-2.30.0 → assertpy2-2.31.0}/README.md +0 -0
  28. {assertpy2-2.30.0 → assertpy2-2.31.0}/SECURITY.md +0 -0
  29. {assertpy2-2.30.0 → assertpy2-2.31.0}/assertpy2/__init__.py +0 -0
  30. {assertpy2-2.30.0 → assertpy2-2.31.0}/assertpy2/_clustering.py +0 -0
  31. {assertpy2-2.30.0 → assertpy2-2.31.0}/assertpy2/_dangling.py +0 -0
  32. {assertpy2-2.30.0 → assertpy2-2.31.0}/assertpy2/_engine/__init__.py +0 -0
  33. {assertpy2-2.30.0 → assertpy2-2.31.0}/assertpy2/_engine/_builder_check_typing.py +0 -0
  34. {assertpy2-2.30.0 → assertpy2-2.31.0}/assertpy2/_engine/_capable_typing.py +0 -0
  35. {assertpy2-2.30.0 → assertpy2-2.31.0}/assertpy2/_engine/_check_typing.py +0 -0
  36. {assertpy2-2.30.0 → assertpy2-2.31.0}/assertpy2/_engine/_compare.py +0 -0
  37. {assertpy2-2.30.0 → assertpy2-2.31.0}/assertpy2/_engine/_compat.py +0 -0
  38. {assertpy2-2.30.0 → assertpy2-2.31.0}/assertpy2/_engine/_diff.py +0 -0
  39. {assertpy2-2.30.0 → assertpy2-2.31.0}/assertpy2/_engine/_equality.py +0 -0
  40. {assertpy2-2.30.0 → assertpy2-2.31.0}/assertpy2/_engine/_introspection.py +0 -0
  41. {assertpy2-2.30.0 → assertpy2-2.31.0}/assertpy2/_engine/_membership.py +0 -0
  42. {assertpy2-2.30.0 → assertpy2-2.31.0}/assertpy2/_engine/_mixin_base.py +0 -0
  43. {assertpy2-2.30.0 → assertpy2-2.31.0}/assertpy2/_engine/_negated_typing.py +0 -0
  44. {assertpy2-2.30.0 → assertpy2-2.31.0}/assertpy2/_engine/_operations.py +0 -0
  45. {assertpy2-2.30.0 → assertpy2-2.31.0}/assertpy2/_engine/_ordering.py +0 -0
  46. {assertpy2-2.30.0 → assertpy2-2.31.0}/assertpy2/_engine/_pairing.py +0 -0
  47. {assertpy2-2.30.0 → assertpy2-2.31.0}/assertpy2/_engine/_path.py +0 -0
  48. {assertpy2-2.30.0 → assertpy2-2.31.0}/assertpy2/_engine/_poll_typing.py +0 -0
  49. {assertpy2-2.30.0 → assertpy2-2.31.0}/assertpy2/_engine/_require.py +0 -0
  50. {assertpy2-2.30.0 → assertpy2-2.31.0}/assertpy2/_engine/_size.py +0 -0
  51. {assertpy2-2.30.0 → assertpy2-2.31.0}/assertpy2/_engine/_text.py +0 -0
  52. {assertpy2-2.30.0 → assertpy2-2.31.0}/assertpy2/_engine/_typing.py +0 -0
  53. {assertpy2-2.30.0 → assertpy2-2.31.0}/assertpy2/_hints.py +0 -0
  54. {assertpy2-2.30.0 → assertpy2-2.31.0}/assertpy2/_inline.py +0 -0
  55. {assertpy2-2.30.0 → assertpy2-2.31.0}/assertpy2/_matcher_impls.py +0 -0
  56. {assertpy2-2.30.0 → assertpy2-2.31.0}/assertpy2/_satisfies.py +0 -0
  57. {assertpy2-2.30.0 → assertpy2-2.31.0}/assertpy2/async_assertions.py +0 -0
  58. {assertpy2-2.30.0 → assertpy2-2.31.0}/assertpy2/base.py +0 -0
  59. {assertpy2-2.30.0 → assertpy2-2.31.0}/assertpy2/behave_matchers.py +0 -0
  60. {assertpy2-2.30.0 → assertpy2-2.31.0}/assertpy2/bytes_mixin.py +0 -0
  61. {assertpy2-2.30.0 → assertpy2-2.31.0}/assertpy2/collection.py +0 -0
  62. {assertpy2-2.30.0 → assertpy2-2.31.0}/assertpy2/contains.py +0 -0
  63. {assertpy2-2.30.0 → assertpy2-2.31.0}/assertpy2/dataframe.py +0 -0
  64. {assertpy2-2.30.0 → assertpy2-2.31.0}/assertpy2/date.py +0 -0
  65. {assertpy2-2.30.0 → assertpy2-2.31.0}/assertpy2/dict.py +0 -0
  66. {assertpy2-2.30.0 → assertpy2-2.31.0}/assertpy2/dynamic.py +0 -0
  67. {assertpy2-2.30.0 → assertpy2-2.31.0}/assertpy2/exception.py +0 -0
  68. {assertpy2-2.30.0 → assertpy2-2.31.0}/assertpy2/extracting.py +0 -0
  69. {assertpy2-2.30.0 → assertpy2-2.31.0}/assertpy2/file.py +0 -0
  70. {assertpy2-2.30.0 → assertpy2-2.31.0}/assertpy2/helpers.py +0 -0
  71. {assertpy2-2.30.0 → assertpy2-2.31.0}/assertpy2/http_mixin.py +0 -0
  72. {assertpy2-2.30.0 → assertpy2-2.31.0}/assertpy2/matchers.py +0 -0
  73. {assertpy2-2.30.0 → assertpy2-2.31.0}/assertpy2/numeric.py +0 -0
  74. {assertpy2-2.30.0 → assertpy2-2.31.0}/assertpy2/outcome.py +0 -0
  75. {assertpy2-2.30.0 → assertpy2-2.31.0}/assertpy2/py.typed +0 -0
  76. {assertpy2-2.30.0 → assertpy2-2.31.0}/assertpy2/string.py +0 -0
  77. {assertpy2-2.30.0 → assertpy2-2.31.0}/assertpy2/warning.py +0 -0
  78. {assertpy2-2.30.0 → assertpy2-2.31.0}/benchmarks/test_perf.py +0 -0
  79. {assertpy2-2.30.0 → assertpy2-2.31.0}/docs/assets/diff-equal.png +0 -0
  80. {assertpy2-2.30.0 → assertpy2-2.31.0}/docs/assets/diff-equal.svg +0 -0
  81. {assertpy2-2.30.0 → assertpy2-2.31.0}/docs/assets/diff-gallery.png +0 -0
  82. {assertpy2-2.30.0 → assertpy2-2.31.0}/docs/assets/diff-match.svg +0 -0
  83. {assertpy2-2.30.0 → assertpy2-2.31.0}/docs/assets/diff-sequence.svg +0 -0
  84. {assertpy2-2.30.0 → assertpy2-2.31.0}/docs/assets/diff-set.svg +0 -0
  85. {assertpy2-2.30.0 → assertpy2-2.31.0}/docs/assets/diff-string.svg +0 -0
  86. {assertpy2-2.30.0 → assertpy2-2.31.0}/docs/concepts/stability.md +0 -0
  87. {assertpy2-2.30.0 → assertpy2-2.31.0}/docs/extending/custom-assertions.md +0 -0
  88. {assertpy2-2.30.0 → assertpy2-2.31.0}/docs/getting-started/comparison.md +0 -0
  89. {assertpy2-2.30.0 → assertpy2-2.31.0}/docs/getting-started/migration.md +0 -0
  90. {assertpy2-2.30.0 → assertpy2-2.31.0}/docs/getting-started/quickstart.md +0 -0
  91. {assertpy2-2.30.0 → assertpy2-2.31.0}/docs/guides/assertions.md +0 -0
  92. {assertpy2-2.30.0 → assertpy2-2.31.0}/docs/guides/data.md +0 -0
  93. {assertpy2-2.30.0 → assertpy2-2.31.0}/docs/guides/fluent.md +0 -0
  94. {assertpy2-2.30.0 → assertpy2-2.31.0}/docs/guides/matchers.md +0 -0
  95. {assertpy2-2.30.0 → assertpy2-2.31.0}/docs/guides/testing.md +0 -0
  96. {assertpy2-2.30.0 → assertpy2-2.31.0}/docs/index.md +0 -0
  97. {assertpy2-2.30.0 → assertpy2-2.31.0}/docs/logo-dark.svg +0 -0
  98. {assertpy2-2.30.0 → assertpy2-2.31.0}/docs/logo.svg +0 -0
  99. {assertpy2-2.30.0 → assertpy2-2.31.0}/docs/recipes.md +0 -0
  100. {assertpy2-2.30.0 → assertpy2-2.31.0}/docs/reference/async.md +0 -0
  101. {assertpy2-2.30.0 → assertpy2-2.31.0}/docs/reference/bytes.md +0 -0
  102. {assertpy2-2.30.0 → assertpy2-2.31.0}/docs/reference/collections.md +0 -0
  103. {assertpy2-2.30.0 → assertpy2-2.31.0}/docs/reference/containment.md +0 -0
  104. {assertpy2-2.30.0 → assertpy2-2.31.0}/docs/reference/core.md +0 -0
  105. {assertpy2-2.30.0 → assertpy2-2.31.0}/docs/reference/dataframes.md +0 -0
  106. {assertpy2-2.30.0 → assertpy2-2.31.0}/docs/reference/dates.md +0 -0
  107. {assertpy2-2.30.0 → assertpy2-2.31.0}/docs/reference/dicts.md +0 -0
  108. {assertpy2-2.30.0 → assertpy2-2.31.0}/docs/reference/dynamic.md +0 -0
  109. {assertpy2-2.30.0 → assertpy2-2.31.0}/docs/reference/entry-points.md +0 -0
  110. {assertpy2-2.30.0 → assertpy2-2.31.0}/docs/reference/errors.md +0 -0
  111. {assertpy2-2.30.0 → assertpy2-2.31.0}/docs/reference/exceptions.md +0 -0
  112. {assertpy2-2.30.0 → assertpy2-2.31.0}/docs/reference/extracting.md +0 -0
  113. {assertpy2-2.30.0 → assertpy2-2.31.0}/docs/reference/files.md +0 -0
  114. {assertpy2-2.30.0 → assertpy2-2.31.0}/docs/reference/http.md +0 -0
  115. {assertpy2-2.30.0 → assertpy2-2.31.0}/docs/reference/json.md +0 -0
  116. {assertpy2-2.30.0 → assertpy2-2.31.0}/docs/reference/matchers.md +0 -0
  117. {assertpy2-2.30.0 → assertpy2-2.31.0}/docs/reference/numbers.md +0 -0
  118. {assertpy2-2.30.0 → assertpy2-2.31.0}/docs/reference/overview.md +0 -0
  119. {assertpy2-2.30.0 → assertpy2-2.31.0}/docs/reference/snapshots.md +0 -0
  120. {assertpy2-2.30.0 → assertpy2-2.31.0}/docs/reference/strings.md +0 -0
  121. {assertpy2-2.30.0 → assertpy2-2.31.0}/docs/reference/warnings.md +0 -0
  122. {assertpy2-2.30.0 → assertpy2-2.31.0}/docs/stylesheets/extra.css +0 -0
  123. {assertpy2-2.30.0 → assertpy2-2.31.0}/mkdocs.yml +0 -0
  124. {assertpy2-2.30.0 → assertpy2-2.31.0}/mkdocs_hooks.py +0 -0
  125. {assertpy2-2.30.0 → assertpy2-2.31.0}/scripts/generate_check_protocols.py +0 -0
  126. {assertpy2-2.30.0 → assertpy2-2.31.0}/scripts/generate_poll_protocols.py +0 -0
  127. {assertpy2-2.30.0 → assertpy2-2.31.0}/scripts/mutation_report.py +0 -0
  128. {assertpy2-2.30.0 → assertpy2-2.31.0}/scripts/regenerate.py +0 -0
  129. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/api_surface.py +0 -0
  130. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/chain_corpus.py +0 -0
  131. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/chain_model.py +0 -0
  132. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/chain_surfaces.py +0 -0
  133. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/data/json_schema_test_suite/LICENSE +0 -0
  134. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/data/json_schema_test_suite/date-time.json +0 -0
  135. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/data/json_schema_test_suite/date.json +0 -0
  136. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/data/json_schema_test_suite/duration.json +0 -0
  137. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/data/json_schema_test_suite/email.json +0 -0
  138. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/data/json_schema_test_suite/hostname.json +0 -0
  139. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/data/json_schema_test_suite/multipleOf.json +0 -0
  140. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/data/json_schema_test_suite/time.json +0 -0
  141. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/data/json_schema_test_suite/uri.json +0 -0
  142. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/docs_fixtures.py +0 -0
  143. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/format_corpus.py +0 -0
  144. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/group_compat.py +0 -0
  145. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/poll_parity_baseline.py +0 -0
  146. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/poll_parity_cases.py +0 -0
  147. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/pyright_baseline.py +0 -0
  148. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_api_compatibility.py +0 -0
  149. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_api_vocabulary.py +0 -0
  150. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_architecture_doc.py +0 -0
  151. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_async.py +0 -0
  152. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_async_predicates.py +0 -0
  153. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_attrs_support.py +0 -0
  154. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_behave_matchers.py +0 -0
  155. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_bool.py +0 -0
  156. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_boundary_cases.py +0 -0
  157. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_bytes.py +0 -0
  158. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_call_awaitables.py +0 -0
  159. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_callable.py +0 -0
  160. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_capable_protocol.py +0 -0
  161. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_chain_machine.py +0 -0
  162. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_chaining.py +0 -0
  163. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_check.py +0 -0
  164. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_check_protocols.py +0 -0
  165. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_class.py +0 -0
  166. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_class_in_spec.py +0 -0
  167. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_clustering.py +0 -0
  168. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_collection.py +0 -0
  169. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_complexity.py +0 -0
  170. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_core.py +0 -0
  171. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_custom_dict.py +0 -0
  172. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_custom_list.py +0 -0
  173. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_dangling.py +0 -0
  174. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_dataframe.py +0 -0
  175. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_datetime.py +0 -0
  176. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_declared_floors.py +0 -0
  177. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_deep_nesting.py +0 -0
  178. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_description.py +0 -0
  179. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_dict.py +0 -0
  180. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_dict_compare.py +0 -0
  181. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_diff_laws.py +0 -0
  182. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_difference_places.py +0 -0
  183. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_docs_examples.py +0 -0
  184. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_docs_typing.py +0 -0
  185. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_duality.py +0 -0
  186. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_dynamic.py +0 -0
  187. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_equality_signals.py +0 -0
  188. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_equals.py +0 -0
  189. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_errors.py +0 -0
  190. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_evaluation_core.py +0 -0
  191. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_exception_context.py +0 -0
  192. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_exception_fields.py +0 -0
  193. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_expected_contract.py +0 -0
  194. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_expected_exception.py +0 -0
  195. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_expected_warning.py +0 -0
  196. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_extensions.py +0 -0
  197. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_extracting.py +0 -0
  198. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_fail.py +0 -0
  199. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_file.py +0 -0
  200. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_fixed_width_integers.py +0 -0
  201. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_grouped_soft.py +0 -0
  202. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_hints.py +0 -0
  203. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_hostile_values.py +0 -0
  204. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_http.py +0 -0
  205. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_import_cost.py +0 -0
  206. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_in.py +0 -0
  207. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_inline_record.py +0 -0
  208. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_inline_snapshot.py +0 -0
  209. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_integer_arguments.py +0 -0
  210. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_iterable_cluster.py +0 -0
  211. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_json.py +0 -0
  212. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_list.py +0 -0
  213. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_match_result.py +0 -0
  214. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_matcher_algebra.py +0 -0
  215. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_matcher_parity.py +0 -0
  216. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_matcher_registry.py +0 -0
  217. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_matchers.py +0 -0
  218. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_membership_failures.py +0 -0
  219. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_membership_family.py +0 -0
  220. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_message_elision.py +0 -0
  221. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_message_values.py +0 -0
  222. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_missing_names.py +0 -0
  223. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_moment_relations.py +0 -0
  224. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_namedtuple.py +0 -0
  225. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_negated_protocols.py +0 -0
  226. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_none.py +0 -0
  227. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_not.py +0 -0
  228. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_number_zoo.py +0 -0
  229. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_numbers.py +0 -0
  230. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_numeric_questions.py +0 -0
  231. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_one_shot_chains.py +0 -0
  232. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_openapi_30_keywords.py +0 -0
  233. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_openapi_contract.py +0 -0
  234. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_openapi_format_suite.py +0 -0
  235. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_openapi_formats.py +0 -0
  236. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_openapi_multiple_of.py +0 -0
  237. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_openapi_oracle.py +0 -0
  238. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_openapi_part_read.py +0 -0
  239. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_operation_contract.py +0 -0
  240. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_optional_integration_contracts.py +0 -0
  241. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_overload_order.py +0 -0
  242. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_overloads.py +0 -0
  243. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_pin_coverage.py +0 -0
  244. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_pipeline.py +0 -0
  245. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_poll_parity.py +0 -0
  246. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_poll_protocols.py +0 -0
  247. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_prerequisites.py +0 -0
  248. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_property_attrs.py +0 -0
  249. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_property_based.py +0 -0
  250. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_protocol_parity.py +0 -0
  251. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_public_surface.py +0 -0
  252. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_pyright_baseline.py +0 -0
  253. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_recursive_assertion.py +0 -0
  254. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_recursive_compare.py +0 -0
  255. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_recursive_compare_attrs.py +0 -0
  256. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_recursive_compare_config.py +0 -0
  257. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_recursive_compare_pydantic.py +0 -0
  258. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_refusal_frames.py +0 -0
  259. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_refusals.py +0 -0
  260. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_regex_groups.py +0 -0
  261. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_render_laws.py +0 -0
  262. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_requirement.py +0 -0
  263. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_rich_diff.py +0 -0
  264. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_same_as.py +0 -0
  265. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_satisfy.py +0 -0
  266. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_skip_guard.py +0 -0
  267. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_soft.py +0 -0
  268. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_soft_fail.py +0 -0
  269. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_strict_reach.py +0 -0
  270. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_string.py +0 -0
  271. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_structure_sentence.py +0 -0
  272. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_surface_conformance.py +0 -0
  273. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_sync_eventually.py +0 -0
  274. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_text_relations.py +0 -0
  275. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_traceback.py +0 -0
  276. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_type.py +0 -0
  277. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_type_expression_failures.py +0 -0
  278. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_typing_claims.py +0 -0
  279. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_typing_completeness.py +0 -0
  280. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_typing_conformance.py +0 -0
  281. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_typing_from_a_wheel.py +0 -0
  282. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_typing_harness.py +0 -0
  283. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_typing_http.py +0 -0
  284. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_typing_integrations.py +0 -0
  285. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_typing_negative.py +0 -0
  286. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_typing_promises.py +0 -0
  287. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_unrenderable_values.py +0 -0
  288. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_vacuity_contract.py +0 -0
  289. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_verdict_corpus.py +0 -0
  290. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_verdicts_held_to_python.py +0 -0
  291. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/test_warn.py +0 -0
  292. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/typing_cases.py +0 -0
  293. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/typing_harness.py +0 -0
  294. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/typing_http.py +0 -0
  295. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/typing_integrations.py +0 -0
  296. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/typing_integrations_baseline.py +0 -0
  297. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/typing_negative_baseline.py +0 -0
  298. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/typing_promises.py +0 -0
  299. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/verdict_corpus.py +0 -0
  300. {assertpy2-2.30.0 → assertpy2-2.31.0}/tests/verdict_golden.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: assertpy2
3
- Version: 2.30.0
3
+ Version: 2.31.0
4
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
@@ -36,6 +36,7 @@ from typing import (
36
36
  )
37
37
 
38
38
  from ..errors import DiffEntry, Step
39
+ from ._introspection import definition_of
39
40
 
40
41
  if TYPE_CHECKING:
41
42
  from collections.abc import Callable
@@ -52,15 +53,15 @@ def _alias_sources(alias: object) -> list[tuple[object, ...]]:
52
53
  return [tuple(path)] if path else []
53
54
 
54
55
 
55
- def _declared_keys(model: Any) -> set[str]:
56
- """Field names plus the top-level key of every alias, so an aliased payload key is not mistaken for drift."""
57
- keys: set[str] = set()
58
- for name, info in model.model_fields.items():
59
- keys.add(name)
60
- for alias in (getattr(info, "alias", None), getattr(info, "validation_alias", None)):
61
- if alias is not None:
62
- keys.update(source[0] for source in _alias_sources(alias) if isinstance(source[0], str))
63
- return keys
56
+ def _spellings(name: str, info: Any) -> list[object]:
57
+ """Every key that spells a field: its name, the first key of each alias validation may read it by, and the alias
58
+ it is dumped under. Which of them validation reads is the config's to say (`_field_sources`)."""
59
+ keys: list[object] = [name]
60
+ for alias in (getattr(info, "alias", None), getattr(info, "validation_alias", None)):
61
+ if alias is not None:
62
+ keys += [source[0] for source in _alias_sources(alias)]
63
+ shown = getattr(info, "serialization_alias", None)
64
+ return [*keys, shown] if isinstance(shown, str) else keys
64
65
 
65
66
 
66
67
  def _field_sources(name: str, info: Any, config: Any) -> tuple[tuple[object, ...], ...]:
@@ -215,8 +216,9 @@ and how it was taken (the keys followed for a field or a key, ``None`` for a pos
215
216
  collection that keeps no positions). A hop into a mapping's key, and one into what JSON text decodes to, is named
216
217
  by nothing. Read out only where something is found (`_placed`)."""
217
218
 
218
- _Found = tuple[_Hop, object]
219
- """An undeclared field: where it stands and the value sent there."""
219
+ _Found = tuple[_Hop, object] | tuple[_Hop, object, str]
220
+ """An undeclared field: where it stands and the value sent there, and, where the key spells a field of the model, why
221
+ validation did not read it."""
220
222
 
221
223
  _DECLARED: dict[tuple[object, object], tuple[Any, Any]] = {}
222
224
  """What a declaration says a container built from it holds, per declaration and kind of container: worked out once,
@@ -348,6 +350,20 @@ def _item_nodes(node: object, owner: object) -> tuple[tuple[Any, ...] | None, An
348
350
  return None, _either([*every, *unknown])
349
351
 
350
352
 
353
+ def _missing_is_its_own(cls: Any) -> bool:
354
+ """Whether the enum *cls* answers a value it does not name by a `_missing_` other than `Enum`'s.
355
+
356
+ Its constructor calls that method, and a schema names it only where pydantic calls it in the constructor's
357
+ place. A class that cannot be read says yes.
358
+ """
359
+ try:
360
+ found = definition_of(cls, "_missing_")
361
+ # no class: its tree is read off the slots of `type`, so nothing the class or its metaclass says is asked
362
+ except TypeError:
363
+ return True
364
+ return found is None or found[0] is not enum.Enum
365
+
366
+
351
367
  _OWN_CODE: dict[type, tuple[object, bool]] = {}
352
368
  """Per model class, beside the validator it was read with: whether validating by it runs code that is not pydantic's.
353
369
  At most 256 classes (`_remember`)."""
@@ -355,10 +371,12 @@ At most 256 classes (`_remember`)."""
355
371
 
356
372
  def runs_own_code(model: Any) -> bool:
357
373
  """Whether validation by *model* runs code of its own anywhere in its schema: a validator, a `model_post_init`
358
- (private attributes bring one), a custom `__init__`, a dataclass `__post_init__`, a called class.
374
+ (private attributes bring one), a custom `__init__`, a dataclass `__post_init__`, a called class, a named tuple,
375
+ whose class is called as well, an enum's own `_missing_`.
359
376
 
360
377
  Pydantic's own validation never changes the payload it is given, so only such a model can leave it other than
361
- it was sent. A schema that cannot be read says yes.
378
+ it was sent. A default factory that takes no data is not counted: validation hands it nothing of the payload.
379
+ A schema that cannot be read says yes.
362
380
  """
363
381
  validator = getattr(model, "__pydantic_validator__", None)
364
382
  known = _OWN_CODE.get(model)
@@ -374,11 +392,12 @@ def runs_own_code(model: Any) -> bool:
374
392
  kind = node.get("type")
375
393
  answer = isinstance(kind, str) and bool(
376
394
  kind.startswith("function-")
377
- or kind == "call"
395
+ or kind in ("call", "named-tuple")
378
396
  or node.get("post_init")
379
397
  or node.get("custom_init")
380
398
  or node.get("default_factory_takes_data")
381
399
  or node.get("missing")
400
+ or (kind == "enum" and _missing_is_its_own(node.get("cls")))
382
401
  or callable(node.get("discriminator"))
383
402
  )
384
403
  # a serialiser runs at dump time, metadata is never validated, a default may hold itself
@@ -719,8 +738,37 @@ def _adapter(declared: _Declared) -> Any:
719
738
 
720
739
  _FieldReads = tuple[tuple[str, tuple[tuple[object, ...], ...], _Declared], ...]
721
740
 
722
- _READS: dict[type, tuple[object, frozenset[str], _FieldReads, _Declared | None]] = {}
723
- """Per model class, with the validator they were read beside: the keys it declares, where each field is read, and
741
+ _Sources = tuple[tuple[object, ...], ...]
742
+ _KeyReads = tuple[frozenset[object], tuple[_Sources, ...], dict[object, _Sources]]
743
+ """Which keys of a payload a model or a record reads: the keys it reads wherever they are sent, the sources of each
744
+ field that may be read from another place, and every key that spells a field beside where validation reads that
745
+ field."""
746
+
747
+
748
+ def _key_reads(fields: collections.abc.Iterable[tuple[_Sources, collections.abc.Iterable[object]]]) -> _KeyReads:
749
+ """The keys the *fields* are read from, each given as its sources in the order validation tries them and the
750
+ keys that spell it.
751
+
752
+ A field is read from the first of its sources a payload holds whole. The first of them, where it is a key and
753
+ no path, is read wherever it is sent. A later one is read only where those before it are absent, and a path only
754
+ where it leads somewhere, so those are told per payload (`_unread`): of two spellings sent validation drops the
755
+ later, and the key a path starts at is dropped where the path leads nowhere.
756
+ """
757
+ sure: set[object] = set()
758
+ chosen: list[_Sources] = []
759
+ spelled: dict[object, _Sources] = {}
760
+ for sources, spellings in fields:
761
+ if sources and len(sources[0]) == 1:
762
+ sure.add(sources[0][0])
763
+ if len(sources) != 1 or len(sources[0]) != 1:
764
+ chosen.append(sources)
765
+ for key in (*(source[0] for source in sources), *spellings):
766
+ spelled.setdefault(key, sources)
767
+ return frozenset(sure), tuple(chosen), spelled
768
+
769
+
770
+ _READS: dict[type, tuple[object, _KeyReads, _FieldReads, _Declared | None]] = {}
771
+ """Per model class, with the validator they were read beside: the keys it reads, where each field is read, and
724
772
  what a typed `__pydantic_extra__` declares its extra values as.
725
773
 
726
774
  Worked out per level of every payload, it was most of the walk: a list of 1000 nested models asked for it 1000 times.
@@ -729,7 +777,7 @@ A rebuild replaces the validator, and with it what the class reads, so an entry
729
777
  was made with. At most 256 classes are kept, so models made on the fly cannot grow it without bound."""
730
778
 
731
779
 
732
- def _reads_of(model: Any) -> tuple[frozenset[str], _FieldReads, _Declared | None]:
780
+ def _reads_of(model: Any) -> tuple[_KeyReads, _FieldReads, _Declared | None]:
733
781
  validator = getattr(model, "__pydantic_validator__", None)
734
782
  known = _READS.get(model)
735
783
  if known is None or known[0] is not validator:
@@ -742,7 +790,10 @@ def _reads_of(model: Any) -> tuple[frozenset[str], _FieldReads, _Declared | None
742
790
  if _may_hold_model(getattr(info, "annotation", None)) or _keyed_under(nodes.get(name), model)
743
791
  )
744
792
  extras = _extra_values(model, nodes.get("__pydantic_extra__")) if config.get("extra") == "allow" else None
745
- known = (validator, frozenset(_declared_keys(model)), fields, extras)
793
+ keyed = _key_reads(
794
+ (_field_sources(name, info, config), _spellings(name, info)) for name, info in model.model_fields.items()
795
+ )
796
+ known = (validator, keyed, fields, extras)
746
797
  if model in _READS or len(_READS) < 256:
747
798
  _READS[model] = known
748
799
  return known[1], known[2], known[3]
@@ -789,10 +840,10 @@ def _extra_values(model: Any, node: object) -> _Declared | None:
789
840
 
790
841
 
791
842
  _RecordReads = tuple[
792
- frozenset[str], tuple[tuple[str, tuple[tuple[object, ...], ...], _Declared | None], ...], _Declared
843
+ _KeyReads, frozenset[str], bool, tuple[tuple[str, tuple[tuple[object, ...], ...], _Declared | None], ...], _Declared
793
844
  ]
794
- """For a dataclass or a `TypedDict`: the payload keys it takes, the fields worth walking with where each is read and
795
- what it declares, and the class as a declaration of its own."""
845
+ """For a dataclass or a `TypedDict`: the payload keys it reads, the names of its fields, whether it keeps extras, the
846
+ fields worth walking with where each is read and what it declares, and the class as a declaration of its own."""
796
847
 
797
848
  _RECORDS: dict[tuple[object, object], tuple[object, _RecordReads]] = {}
798
849
  """Per dataclass or `TypedDict`, by its schema or its class, and the class whose schema holds it, beside the schema or
@@ -804,9 +855,9 @@ def _record_reads(record: _Record, owner: object = None) -> _RecordReads:
804
855
 
805
856
  The keys it takes and where each field is read come from the schema pydantic built, the one the walk came to it
806
857
  by, the record's own, or the one of the class that declared it (*owner*). It holds what no annotation tells: an
807
- alias written as text, the one of several `Field`s that wins, a field ``init=False`` leaves out. The name is read
808
- after its aliases, since a config that allows it reaches a `TypedDict` from the model above. Where no schema
809
- holds the record, the annotations are read as written (`_written_fields`).
858
+ alias written as text, the one of several `Field`s that wins, a field ``init=False`` leaves out, and the config
859
+ that says whether a name is read, which reaches a `TypedDict` from the model above. Where no schema holds the
860
+ record, the annotations are read as written (`_written_fields`), and a name counts as read after its aliases.
810
861
 
811
862
  What each field declares below it is the schema of that field, beside its annotation as pydantic resolved it or
812
863
  as written where that can be read.
@@ -824,29 +875,30 @@ def _record_reads(record: _Record, owner: object = None) -> _RecordReads:
824
875
  # only later pydantic names the class in the schema of a `TypedDict`
825
876
  told = _written_fields(kind) if isinstance(kind, type) else ()
826
877
  written = {name: (sources, annotation, held) for name, sources, annotation, held in told}
827
- reads = _schema_fields(_Record(node, kind), holder)
828
- reads = reads or [(name, sources, held, None) for name, (sources, _, held) in written.items()]
829
- accepted, fields = set(), []
830
- for name, sources, held, schema in reads:
831
- accepted.add(name)
832
- accepted.update(source[0] for source in sources if isinstance(source[0], str))
878
+ found = _record_node(_Record(node, kind), holder)
879
+ said = found if isinstance(found, dict) else {}
880
+ # an older pydantic says it in the config alone, and a `TypedDict` that declares its extras in the schema alone
881
+ keeps = said.get("extra_behavior", (said.get("config") or {}).get("extra_fields_behavior")) == "allow"
882
+ reads = _schema_fields(_Record(found, kind), holder)
883
+ reads = reads or [(name, sources, held, None, ()) for name, (sources, _, held) in written.items()]
884
+ keyed, fields = [], []
885
+ for name, sources, held, schema, spellings in reads:
886
+ keyed.append((sources, (name, *spellings)))
833
887
  annotation = written[name][1] if name in written else None
834
888
  if held and (annotation is None or _may_hold_model(annotation) or _keyed_under(schema, holder)):
835
889
  fields.append(
836
890
  (name, sources, None if annotation is None and schema is None else (annotation, holder, schema))
837
891
  )
838
- answer = (frozenset(accepted), tuple(fields), (kind, None, None))
892
+ names = frozenset(name for name, *_ in reads)
893
+ answer = (_key_reads(keyed), names, keeps, tuple(fields), (kind, None, None))
839
894
  _remember(_RECORDS, key, read_with, answer, 256)
840
895
  return answer
841
896
 
842
897
 
843
- def _schema_fields(record: _Record, holder: object) -> list[tuple[str, tuple[tuple[object, ...], ...], bool, Any]]:
844
- """Each key the schema says a record takes: its name, where it is read (its alias paths, then its name), whether
845
- the record holds it, which an `InitVar` is not, and the schema of its value. The schema is the one the walk came
846
- to the record by, else the one the schema of *holder* has for its class; empty where it has none.
847
-
848
- A `TypedDict` is told by the id its ``ref`` ends in, since only later pydantic names its class there.
849
- """
898
+ def _record_node(record: _Record, holder: object) -> Any:
899
+ """The schema of a record: the one the walk came to it by, else the one the schema of *holder* has for its class,
900
+ or ``None``. A `TypedDict` is told by the id its ``ref`` ends in, since only later pydantic names its class
901
+ there."""
850
902
  found, kind = record
851
903
  pending: list[Any] = [] if found is not None else [getattr(holder, "__pydantic_core_schema__", None)]
852
904
  while pending and found is None:
@@ -858,7 +910,20 @@ def _schema_fields(record: _Record, holder: object) -> list[tuple[str, tuple[tup
858
910
  pending.extend(value for key, value in node.items() if key not in ("serialization", "metadata", "default"))
859
911
  elif isinstance(node, (list, tuple)):
860
912
  pending.extend(node)
861
- by_alias = isinstance(found, dict) and (found.get("config") or {}).get("validate_by_alias") is not False
913
+ return found
914
+
915
+
916
+ def _schema_fields(
917
+ record: _Record, holder: object
918
+ ) -> list[tuple[str, tuple[tuple[object, ...], ...], bool, Any, tuple[object, ...]]]:
919
+ """Each key the schema says a record takes: its name, where it is read (its alias paths, then its name, each
920
+ where the record's config reads by it), whether the record holds it, which an `InitVar` is not, the schema of its
921
+ value, and the other keys that spell it. Empty where no schema holds the record (`_record_node`).
922
+ """
923
+ found = _record_node(record, holder)
924
+ config = (found.get("config") or {}) if isinstance(found, dict) else {}
925
+ by_alias, by_name = config.get("validate_by_alias") is not False, config.get("validate_by_name")
926
+ by_name = by_name if by_name is not None else config.get("populate_by_name")
862
927
  while isinstance(found, dict) and found.get("type") not in ("typed-dict", "dataclass-args"):
863
928
  found = found.get("schema")
864
929
  if not isinstance(found, dict):
@@ -870,11 +935,11 @@ def _schema_fields(record: _Record, holder: object) -> list[tuple[str, tuple[tup
870
935
  if field.get("init") is not False:
871
936
  alias = field.get("validation_alias")
872
937
  paths = [alias] if isinstance(alias, str) or not alias or not isinstance(alias[0], list) else alias
873
- sources = [(path,) if isinstance(path, str) else tuple(path) for path in paths if path and by_alias]
874
- # counted as declared beside the alias validation reads, as a model's is
938
+ aliases = [(path,) if isinstance(path, str) else tuple(path) for path in paths if path]
939
+ read_at = (*(aliases if by_alias else ()), *([(name,)] if by_name or not aliases else ()))
875
940
  shown = field.get("serialization_alias")
876
- read_at = (*sources, (name,), *([(shown,)] if isinstance(shown, str) else []))
877
- reads.append((name, read_at, not field.get("init_only"), field.get("schema")))
941
+ spellings = (*(path[0] for path in aliases), *([shown] if isinstance(shown, str) else ()))
942
+ reads.append((name, read_at, not field.get("init_only"), field.get("schema"), spellings))
878
943
  return reads
879
944
 
880
945
 
@@ -952,6 +1017,33 @@ def _raw_field(
952
1017
  return None, None
953
1018
 
954
1019
 
1020
+ def _unread(left: list[object], payload: collections.abc.Mapping, reads: _KeyReads) -> list[object]:
1021
+ """Those of the keys *left*, which no field is sure to read, that no field was read from in *payload*: the ones
1022
+ validation dropped, or kept as extras."""
1023
+ read = {source[0] for sources in reads[1] for source in [_raw_field(payload, sources)[0]] if source}
1024
+ return [key for key in left if key not in read]
1025
+
1026
+
1027
+ def _undeclared(payload: collections.abc.Mapping, key: object, path: _Hop, reads: _KeyReads) -> _Found:
1028
+ """The find for a *key* of *payload* validation dropped, with the reason where the key spells a field: the
1029
+ source its field was read from in its place, or the ones validation reads it from where the payload sent none."""
1030
+ sources = reads[2].get(key)
1031
+ if not sources:
1032
+ return (path, key, (key,)), payload[key]
1033
+ read, _ = _raw_field(payload, sources)
1034
+ reason = (
1035
+ f"its field was read from <{_spelt(read)}>"
1036
+ if read is not None
1037
+ else f"validation reads its field from {' or '.join(f'<{_spelt(source)}>' for source in sources)}"
1038
+ )
1039
+ return (path, key, (key,)), payload[key], reason
1040
+
1041
+
1042
+ def _spelt(source: tuple[object, ...]) -> str:
1043
+ """A source as a failure names it: a key, and the keys and positions a path goes on by."""
1044
+ return "".join(f"[{step}]" if isinstance(step, int) else f".{step}" for step in source).removeprefix(".")
1045
+
1046
+
955
1047
  def _placed(hop: _Hop) -> tuple[str, tuple[Step, ...]]:
956
1048
  """Where *hop* stands, as the text a failure names it by and as the steps that reach it in the payload."""
957
1049
  hops = []
@@ -1023,7 +1115,7 @@ def contract_drift(
1023
1115
  reads those items by position.
1024
1116
  """
1025
1117
  model = type(instance)
1026
- declared, fields, extras = _reads_of(model)
1118
+ reads, fields, extras = _reads_of(model)
1027
1119
  replayed = _REPLAYED in _seen
1028
1120
  if getattr(model, "__pydantic_root_model__", False):
1029
1121
  return _declared_drift(payload, instance.root, path, _seen, fields[0][2], replayed) if fields else []
@@ -1034,10 +1126,14 @@ def contract_drift(
1034
1126
  return []
1035
1127
  seen = _seen | {(id(payload), id(instance))}
1036
1128
  drift: list[_Found] = []
1037
- if getattr(model, "model_config", {}).get("extra") != "allow":
1038
- drift += [((path, key, (key,)), payload[key]) for key in payload if key not in declared]
1039
- else:
1040
- drift += _extras_drift(payload, instance, path, seen, extras, replayed, declared)
1129
+ sure = reads[0]
1130
+ left = [key for key in payload if key not in sure]
1131
+ if left:
1132
+ left = _unread(left, payload, reads)
1133
+ if getattr(model, "model_config", {}).get("extra") == "allow":
1134
+ drift += _extras_drift(payload, instance, path, seen, extras, replayed, left)
1135
+ elif left:
1136
+ drift += [_undeclared(payload, key, path, reads) for key in left]
1041
1137
  for name, sources, annotated in fields:
1042
1138
  source, raw = _raw_field(payload, sources)
1043
1139
  if source is None:
@@ -1060,15 +1156,15 @@ def _extras_drift(
1060
1156
  seen: frozenset[tuple[int, int]],
1061
1157
  declared: _Declared | None,
1062
1158
  replayed: bool,
1063
- own: frozenset[str],
1159
+ left: list[object],
1064
1160
  ) -> list[_Found]:
1065
1161
  """Drift under the extras of a model that allows them: built through a typed `__pydantic_extra__` and walked as it
1066
1162
  *declared* them; untyped ones are the payload's own objects.
1067
1163
 
1068
1164
  An extra the payload sent that the model no longer holds was built as that type and removed afterwards, by a
1069
1165
  validator. What it was built into is gone, so it is validated again as the declared type and walked beside what
1070
- that builds, or refused where that cannot tell (`_replayed`). *own* are the keys the model declares, which are
1071
- no extras.
1166
+ that builds, or refused where that cannot tell (`_replayed`). *left* are the keys no field was read from, which
1167
+ are the extras the model was sent.
1072
1168
  """
1073
1169
  held = getattr(instance, "__pydantic_extra__", None)
1074
1170
  # a validator can leave anything there: what is no dict holds no extra
@@ -1080,9 +1176,7 @@ def _extras_drift(
1080
1176
  for entry in _declared_drift(payload[key], value, (path, key, (key,)), seen, declared, replayed)
1081
1177
  ]
1082
1178
  if declared is not None:
1083
- gone: list[tuple[_Hop, object]] = [
1084
- ((path, key, (key,)), payload[key]) for key in payload if key not in own and key not in built
1085
- ]
1179
+ gone: list[tuple[_Hop, object]] = [((path, key, (key,)), payload[key]) for key in left if key not in built]
1086
1180
  if any(_holds(part, _is_mapping, read_text=True) for _, part in gone):
1087
1181
  reason = f"the model no longer holds {len(gone)} of the extras it was sent"
1088
1182
  drift += _replayed(payload, gone, path, seen, reason, _listed(declared))
@@ -1154,10 +1248,11 @@ def exactness_failure(pairs: Any, *, carrier: str) -> tuple[str, list[DiffEntry]
1154
1248
  return f"<the payload> {reason}", [DiffEntry(path=where or ".", steps=steps, actual=payload, expected=reason)]
1155
1249
  if not drift:
1156
1250
  return None
1157
- placed = sorted(((*_placed(hop), sent) for hop, sent in drift), key=operator.itemgetter(0))
1158
- entries = [DiffEntry(path=where, steps=steps, actual=sent, absent="expected") for where, steps, sent in placed]
1251
+ placed = sorted(((*_placed(hop), sent, why) for hop, sent, *why in drift), key=operator.itemgetter(0))
1252
+ entries = [DiffEntry(path=where, steps=steps, actual=sent, absent="expected") for where, steps, sent, _ in placed]
1159
1253
  paths = [entry.path for entry in entries]
1160
- return f"{carrier}{len(entries)} undeclared field(s) the model does not declare: {paths}", entries
1254
+ told = "".join(f"\n<{where}> is not read: {why[0]}" for where, _, _, why in placed if why)
1255
+ return f"{carrier}{len(entries)} undeclared field(s) the model does not declare: {paths}{told}", entries
1161
1256
 
1162
1257
 
1163
1258
  def _value_drift(
@@ -1227,11 +1322,23 @@ def _record_drift(
1227
1322
  owner: object,
1228
1323
  ) -> list[_Found]:
1229
1324
  """Drift under a dataclass or a `TypedDict`, which drop an undeclared key as a model does: the keys *raw* holds
1230
- that the record neither takes nor keeps among its *extras*, where it allows them, and what lies under the
1231
- fields it *built*."""
1232
- accepted, fields, _ = _record_reads(record, owner)
1325
+ that the record neither read a field from nor keeps among its *extras*, where it allows them, and what lies under
1326
+ the fields it *built*.
1327
+
1328
+ A key that is the name of a field is no extra by being in what was built, where every field is: it is kept only
1329
+ by a record that keeps extras, which puts what it was sent there over the field.
1330
+ """
1331
+ reads, names, keeps, fields, _ = _record_reads(record, owner)
1233
1332
  replayed = _REPLAYED in seen
1234
- drift: list[_Found] = [((path, key, (key,)), raw[key]) for key in raw if key not in accepted and key not in extras]
1333
+ sure = reads[0]
1334
+ left = [key for key in raw if key not in sure]
1335
+ drift: list[_Found] = []
1336
+ if left:
1337
+ drift = [
1338
+ _undeclared(raw, key, path, reads)
1339
+ for key in _unread(left, raw, reads)
1340
+ if (not keeps if key in names else key not in extras)
1341
+ ]
1235
1342
  for name, sources, declared in fields:
1236
1343
  source, part = _raw_field(raw, sources)
1237
1344
  if source is not None and name in built:
@@ -1250,7 +1357,8 @@ def _unsure_drift(
1250
1357
  """Drift under a dict one of several declared `TypedDict`s may have built.
1251
1358
 
1252
1359
  One whose `Literal` fields do not hold what the dict holds did not build it, where no code of the owner's could
1253
- have rewritten them since. Where that leaves one, and nothing declared beside them may build a dict, the dict is
1360
+ have rewritten them since and no extra was put over them (`_put_over`). Where that leaves one, and nothing
1361
+ declared beside them may build a dict, the dict is
1254
1362
  read as that record. Else nothing tells the rest apart, and the walk does not choose as validation did. A key the
1255
1363
  payload sent that the dict holds neither under its name nor as the field one of them reads from it could have been
1256
1364
  dropped by any, so it refuses; so does a field two of them read from different parts of the payload. What the
@@ -1258,12 +1366,7 @@ def _unsure_drift(
1258
1366
  """
1259
1367
  records = unsure.records
1260
1368
  if not runs_own_code(owner):
1261
- fitting = [
1262
- record
1263
- for record in records
1264
- if all(name not in built or built[name] in allowed for name, allowed in _literals(record, owner))
1265
- ]
1266
- records = tuple(fitting)
1369
+ records = tuple(record for record in records if _may_have_built(raw, built, record, owner))
1267
1370
  if len(records) == 1 and not unsure.beside:
1268
1371
  return _record_drift(raw, built, built, path, seen, _Record(records[0], None), owner)
1269
1372
  read_as, fields, other = _shared_reads(records, unsure.beside, owner)
@@ -1288,6 +1391,31 @@ def _unsure_drift(
1288
1391
  return drift
1289
1392
 
1290
1393
 
1394
+ def _may_have_built(raw: collections.abc.Mapping, built: dict, record: Any, owner: object) -> bool:
1395
+ """Whether the `TypedDict` schema *record* may have built the dict *built* from *raw*: each of its `Literal` fields
1396
+ holds a value it takes, or one a key of the payload was put over."""
1397
+ over = _put_over(raw, record, owner)
1398
+ return all(
1399
+ name not in built or name in over or built[name] in allowed for name, allowed in _literals(record, owner)
1400
+ )
1401
+
1402
+
1403
+ def _put_over(raw: collections.abc.Mapping, record: Any, owner: object) -> collections.abc.Collection[object]:
1404
+ """The fields of the `TypedDict` schema *record* that a key of *raw* was put over, if it built the dict.
1405
+
1406
+ One that keeps extras keeps a key it read no field from, and where that key is the name of a field, as beside the
1407
+ alias the field was read from, it lands on what validation built there. What the dict holds under that name is
1408
+ then what was sent, and does not say that this one did not build it. One that drops extras had nothing put over
1409
+ its fields, so what another would have kept is nothing to it.
1410
+ """
1411
+ reads, names, keeps, _, _ = _record_reads(_Record(record, None), owner)
1412
+ if not keeps:
1413
+ return ()
1414
+ sure = reads[0]
1415
+ left = [key for key in raw if key in names and key not in sure]
1416
+ return _unread(left, raw, reads) if left else left
1417
+
1418
+
1291
1419
  def _literals(record: Any, owner: object) -> list[tuple[str, list[object]]]:
1292
1420
  """The fields of the `TypedDict` schema *record* that take nothing but the values a `Literal` lists, with those
1293
1421
  values, ``None`` among them where the field takes it too."""
@@ -1296,7 +1424,7 @@ def _literals(record: Any, owner: object) -> list[tuple[str, list[object]]]:
1296
1424
  if known is not None and known[0] is record:
1297
1425
  return known[1]
1298
1426
  found = []
1299
- for name, _, _, schema in _schema_fields(_Record(record, None), owner):
1427
+ for name, _, _, schema, _ in _schema_fields(_Record(record, None), owner):
1300
1428
  allowed: list[object] = []
1301
1429
  while _schema_kind(schema) == "nullable":
1302
1430
  allowed.append(None)
@@ -1326,7 +1454,7 @@ def _shared_reads(records: tuple[Any, ...], beside: tuple[Any, ...], owner: obje
1326
1454
  read_at: dict[str, list[tuple[tuple[object, ...], ...]]] = {}
1327
1455
  schemas: dict[str, list[Any]] = {}
1328
1456
  for record in records:
1329
- for name, sources, _, schema in _schema_fields(_Record(record, None), owner):
1457
+ for name, sources, _, schema, _ in _schema_fields(_Record(record, None), owner):
1330
1458
  for source in sources:
1331
1459
  read_as.setdefault(source[0], []).append(name)
1332
1460
  read_at.setdefault(name, []).append(sources)
@@ -1579,7 +1707,7 @@ def _revalidated(
1579
1707
  validate = getattr(model, "model_validate", None)
1580
1708
  validate_json = getattr(model, "model_validate_json", None)
1581
1709
  if validate is None and hasattr(model, "__dataclass_fields__"):
1582
- adapter = _adapter(_record_reads(_Record(None, model))[2])
1710
+ adapter = _adapter(_record_reads(_Record(None, model))[4])
1583
1711
  validate, validate_json = getattr(adapter, "validate_python", None), getattr(adapter, "validate_json", None)
1584
1712
  if validate is None or validate_json is None:
1585
1713
  raise UncheckableDriftError(path, reason, raw)
@@ -279,11 +279,12 @@ def _save(name, val):
279
279
  # end. A value the encoder refuses now leaves no file behind either
280
280
  text = json.dumps(_prepare(val), indent=2, separators=(",", ": "), sort_keys=True, cls=_Encoder)
281
281
  tmp = f"{name}.{os.getpid()}.tmp"
282
- with open(tmp, "w") as file_handle:
282
+ with open(tmp, "w", encoding="utf-8") as file_handle:
283
283
  file_handle.write(text)
284
284
  os.replace(tmp, name)
285
285
 
286
286
 
287
287
  def _load(name):
288
- with open(name) as file_handle:
288
+ # as UTF-8 and not as the platform's default: a file a formatter left unescaped read as mojibake under cp1252
289
+ with open(name, encoding="utf-8") as file_handle:
289
290
  return json.load(file_handle, cls=_Decoder)