assertpy2 2.5.1__tar.gz → 2.6.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 (113) hide show
  1. assertpy2-2.6.0/.gitattributes +2 -0
  2. assertpy2-2.6.0/.github/dependabot.yml +20 -0
  3. {assertpy2-2.5.1 → assertpy2-2.6.0}/PKG-INFO +1 -1
  4. assertpy2-2.6.0/SECURITY.md +24 -0
  5. {assertpy2-2.5.1 → assertpy2-2.6.0}/assertpy2/_mixin_base.py +12 -1
  6. {assertpy2-2.5.1 → assertpy2-2.6.0}/assertpy2/_typing.py +3 -1
  7. {assertpy2-2.5.1 → assertpy2-2.6.0}/assertpy2/assertpy.py +5 -1
  8. {assertpy2-2.5.1 → assertpy2-2.6.0}/assertpy2/base.py +96 -86
  9. {assertpy2-2.5.1 → assertpy2-2.6.0}/assertpy2/collection.py +28 -28
  10. {assertpy2-2.5.1 → assertpy2-2.6.0}/assertpy2/contains.py +35 -30
  11. {assertpy2-2.5.1 → assertpy2-2.6.0}/assertpy2/dict.py +22 -22
  12. {assertpy2-2.5.1 → assertpy2-2.6.0}/assertpy2/errors.py +3 -3
  13. {assertpy2-2.5.1 → assertpy2-2.6.0}/assertpy2/exception.py +15 -7
  14. {assertpy2-2.5.1 → assertpy2-2.6.0}/assertpy2/extracting.py +31 -31
  15. {assertpy2-2.5.1 → assertpy2-2.6.0}/assertpy2/helpers.py +89 -69
  16. {assertpy2-2.5.1 → assertpy2-2.6.0}/assertpy2/matchers.py +7 -7
  17. {assertpy2-2.5.1 → assertpy2-2.6.0}/assertpy2/snapshot.py +1 -1
  18. {assertpy2-2.5.1 → assertpy2-2.6.0}/assertpy2/string.py +17 -17
  19. assertpy2-2.6.0/assertpy2/warning.py +123 -0
  20. {assertpy2-2.5.1 → assertpy2-2.6.0}/docs/errors.md +28 -0
  21. {assertpy2-2.5.1 → assertpy2-2.6.0}/pyproject.toml +3 -3
  22. {assertpy2-2.5.1 → assertpy2-2.6.0}/tests/test_async.py +8 -8
  23. {assertpy2-2.5.1 → assertpy2-2.6.0}/tests/test_errors.py +1 -1
  24. {assertpy2-2.5.1 → assertpy2-2.6.0}/tests/test_expected_exception.py +2 -4
  25. assertpy2-2.6.0/tests/test_expected_warning.py +177 -0
  26. {assertpy2-2.5.1 → assertpy2-2.6.0}/tests/test_extracting.py +2 -2
  27. {assertpy2-2.5.1 → assertpy2-2.6.0}/tests/test_file.py +2 -2
  28. {assertpy2-2.5.1 → assertpy2-2.6.0}/tests/test_pytest_plugin.py +2 -2
  29. {assertpy2-2.5.1 → assertpy2-2.6.0}/tests/test_rich_diff.py +68 -64
  30. {assertpy2-2.5.1 → assertpy2-2.6.0}/tests/test_soft.py +2 -2
  31. {assertpy2-2.5.1 → assertpy2-2.6.0}/tests/test_traceback.py +3 -1
  32. {assertpy2-2.5.1 → assertpy2-2.6.0}/uv.lock +1 -1
  33. assertpy2-2.5.1/.github/dependabot.yml +0 -20
  34. assertpy2-2.5.1/SECURITY.md +0 -16
  35. {assertpy2-2.5.1 → assertpy2-2.6.0}/.codecov.yml +0 -0
  36. {assertpy2-2.5.1 → assertpy2-2.6.0}/.github/workflows/ci.yml +0 -0
  37. {assertpy2-2.5.1 → assertpy2-2.6.0}/.github/workflows/codeql.yml +0 -0
  38. {assertpy2-2.5.1 → assertpy2-2.6.0}/.github/workflows/docs.yml +0 -0
  39. {assertpy2-2.5.1 → assertpy2-2.6.0}/.github/workflows/publish.yml +0 -0
  40. {assertpy2-2.5.1 → assertpy2-2.6.0}/.github/workflows/scorecard.yml +0 -0
  41. {assertpy2-2.5.1 → assertpy2-2.6.0}/.github/workflows/zizmor.yml +0 -0
  42. {assertpy2-2.5.1 → assertpy2-2.6.0}/.gitignore +0 -0
  43. {assertpy2-2.5.1 → assertpy2-2.6.0}/CONTRIBUTING.md +0 -0
  44. {assertpy2-2.5.1 → assertpy2-2.6.0}/LICENSE +0 -0
  45. {assertpy2-2.5.1 → assertpy2-2.6.0}/README.md +0 -0
  46. {assertpy2-2.5.1 → assertpy2-2.6.0}/assertpy2/__init__.py +0 -0
  47. {assertpy2-2.5.1 → assertpy2-2.6.0}/assertpy2/_compat.py +0 -0
  48. {assertpy2-2.5.1 → assertpy2-2.6.0}/assertpy2/async_assertions.py +0 -0
  49. {assertpy2-2.5.1 → assertpy2-2.6.0}/assertpy2/behave_matchers.py +0 -0
  50. {assertpy2-2.5.1 → assertpy2-2.6.0}/assertpy2/bytes_mixin.py +0 -0
  51. {assertpy2-2.5.1 → assertpy2-2.6.0}/assertpy2/date.py +0 -0
  52. {assertpy2-2.5.1 → assertpy2-2.6.0}/assertpy2/dynamic.py +0 -0
  53. {assertpy2-2.5.1 → assertpy2-2.6.0}/assertpy2/file.py +0 -0
  54. {assertpy2-2.5.1 → assertpy2-2.6.0}/assertpy2/json_mixin.py +0 -0
  55. {assertpy2-2.5.1 → assertpy2-2.6.0}/assertpy2/numeric.py +0 -0
  56. {assertpy2-2.5.1 → assertpy2-2.6.0}/assertpy2/py.typed +0 -0
  57. {assertpy2-2.5.1 → assertpy2-2.6.0}/assertpy2/pytest_plugin.py +0 -0
  58. {assertpy2-2.5.1 → assertpy2-2.6.0}/docs/assertions.md +0 -0
  59. {assertpy2-2.5.1 → assertpy2-2.6.0}/docs/comparison.md +0 -0
  60. {assertpy2-2.5.1 → assertpy2-2.6.0}/docs/data.md +0 -0
  61. {assertpy2-2.5.1 → assertpy2-2.6.0}/docs/extending.md +0 -0
  62. {assertpy2-2.5.1 → assertpy2-2.6.0}/docs/fluent.md +0 -0
  63. {assertpy2-2.5.1 → assertpy2-2.6.0}/docs/getting-started.md +0 -0
  64. {assertpy2-2.5.1 → assertpy2-2.6.0}/docs/index.md +0 -0
  65. {assertpy2-2.5.1 → assertpy2-2.6.0}/docs/integrations.md +0 -0
  66. {assertpy2-2.5.1 → assertpy2-2.6.0}/docs/logo-dark.svg +0 -0
  67. {assertpy2-2.5.1 → assertpy2-2.6.0}/docs/logo.svg +0 -0
  68. {assertpy2-2.5.1 → assertpy2-2.6.0}/docs/matchers.md +0 -0
  69. {assertpy2-2.5.1 → assertpy2-2.6.0}/docs/migration.md +0 -0
  70. {assertpy2-2.5.1 → assertpy2-2.6.0}/docs/testing.md +0 -0
  71. {assertpy2-2.5.1 → assertpy2-2.6.0}/mkdocs.yml +0 -0
  72. {assertpy2-2.5.1 → assertpy2-2.6.0}/tests/test_behave_matchers.py +0 -0
  73. {assertpy2-2.5.1 → assertpy2-2.6.0}/tests/test_bool.py +0 -0
  74. {assertpy2-2.5.1 → assertpy2-2.6.0}/tests/test_bytes.py +0 -0
  75. {assertpy2-2.5.1 → assertpy2-2.6.0}/tests/test_class.py +0 -0
  76. {assertpy2-2.5.1 → assertpy2-2.6.0}/tests/test_collection.py +0 -0
  77. {assertpy2-2.5.1 → assertpy2-2.6.0}/tests/test_core.py +0 -0
  78. {assertpy2-2.5.1 → assertpy2-2.6.0}/tests/test_custom_dict.py +0 -0
  79. {assertpy2-2.5.1 → assertpy2-2.6.0}/tests/test_custom_list.py +0 -0
  80. {assertpy2-2.5.1 → assertpy2-2.6.0}/tests/test_datetime.py +0 -0
  81. {assertpy2-2.5.1 → assertpy2-2.6.0}/tests/test_description.py +0 -0
  82. {assertpy2-2.5.1 → assertpy2-2.6.0}/tests/test_dict.py +0 -0
  83. {assertpy2-2.5.1 → assertpy2-2.6.0}/tests/test_dict_compare.py +0 -0
  84. {assertpy2-2.5.1 → assertpy2-2.6.0}/tests/test_dyn.py +0 -0
  85. {assertpy2-2.5.1 → assertpy2-2.6.0}/tests/test_equals.py +0 -0
  86. {assertpy2-2.5.1 → assertpy2-2.6.0}/tests/test_extensions.py +0 -0
  87. {assertpy2-2.5.1 → assertpy2-2.6.0}/tests/test_fail.py +0 -0
  88. {assertpy2-2.5.1 → assertpy2-2.6.0}/tests/test_grouped_soft.py +0 -0
  89. {assertpy2-2.5.1 → assertpy2-2.6.0}/tests/test_in.py +0 -0
  90. {assertpy2-2.5.1 → assertpy2-2.6.0}/tests/test_json.py +0 -0
  91. {assertpy2-2.5.1 → assertpy2-2.6.0}/tests/test_list.py +0 -0
  92. {assertpy2-2.5.1 → assertpy2-2.6.0}/tests/test_matcher_registry.py +0 -0
  93. {assertpy2-2.5.1 → assertpy2-2.6.0}/tests/test_matchers.py +0 -0
  94. {assertpy2-2.5.1 → assertpy2-2.6.0}/tests/test_matchers_phase3.py +0 -0
  95. {assertpy2-2.5.1 → assertpy2-2.6.0}/tests/test_namedtuple.py +0 -0
  96. {assertpy2-2.5.1 → assertpy2-2.6.0}/tests/test_none.py +0 -0
  97. {assertpy2-2.5.1 → assertpy2-2.6.0}/tests/test_not.py +0 -0
  98. {assertpy2-2.5.1 → assertpy2-2.6.0}/tests/test_numbers.py +0 -0
  99. {assertpy2-2.5.1 → assertpy2-2.6.0}/tests/test_overloads.py +0 -0
  100. {assertpy2-2.5.1 → assertpy2-2.6.0}/tests/test_phase2.py +0 -0
  101. {assertpy2-2.5.1 → assertpy2-2.6.0}/tests/test_pipeline.py +0 -0
  102. {assertpy2-2.5.1 → assertpy2-2.6.0}/tests/test_readme.py +0 -0
  103. {assertpy2-2.5.1 → assertpy2-2.6.0}/tests/test_recursive_compare.py +0 -0
  104. {assertpy2-2.5.1 → assertpy2-2.6.0}/tests/test_recursive_compare_attrs.py +0 -0
  105. {assertpy2-2.5.1 → assertpy2-2.6.0}/tests/test_recursive_compare_pydantic.py +0 -0
  106. {assertpy2-2.5.1 → assertpy2-2.6.0}/tests/test_regex_groups.py +0 -0
  107. {assertpy2-2.5.1 → assertpy2-2.6.0}/tests/test_same_as.py +0 -0
  108. {assertpy2-2.5.1 → assertpy2-2.6.0}/tests/test_snapshots.py +0 -0
  109. {assertpy2-2.5.1 → assertpy2-2.6.0}/tests/test_soft_fail.py +0 -0
  110. {assertpy2-2.5.1 → assertpy2-2.6.0}/tests/test_string.py +0 -0
  111. {assertpy2-2.5.1 → assertpy2-2.6.0}/tests/test_structural.py +0 -0
  112. {assertpy2-2.5.1 → assertpy2-2.6.0}/tests/test_type.py +0 -0
  113. {assertpy2-2.5.1 → assertpy2-2.6.0}/tests/test_warn.py +0 -0
@@ -0,0 +1,2 @@
1
+ * text=auto eol=lf
2
+ *.{png,ico,jpg,gif} binary
@@ -0,0 +1,20 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: pip
4
+ directory: /
5
+ schedule:
6
+ interval: weekly
7
+ cooldown:
8
+ default-days: 7
9
+ semver-major-days: 7
10
+ commit-message:
11
+ prefix: "chore(deps)"
12
+
13
+ - package-ecosystem: github-actions
14
+ directory: /
15
+ schedule:
16
+ interval: weekly
17
+ cooldown:
18
+ default-days: 7
19
+ commit-message:
20
+ prefix: "ci(deps)"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: assertpy2
3
- Version: 2.5.1
3
+ Version: 2.6.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
@@ -0,0 +1,24 @@
1
+ # Security Policy
2
+
3
+ ## Supported Versions
4
+
5
+ | Version | Supported |
6
+ |---------|-----------|
7
+ | 2.x | Yes |
8
+ | < 2.0 | No |
9
+
10
+ ## Reporting a Vulnerability
11
+
12
+ If you discover a security vulnerability, please report it responsibly:
13
+
14
+ 1. **Email**: [solganis.dev@gmail.com](mailto:solganis.dev@gmail.com)
15
+ 2. **Subject**: `[assertpy2] Security: <brief description>`
16
+ 3. Include steps to reproduce, affected versions, and potential impact
17
+
18
+ Please do **not** open a public GitHub issue for security vulnerabilities.
19
+
20
+ ## Response Timeline
21
+
22
+ - Acknowledgement: within 48 hours
23
+ - Initial assessment: within 7 days
24
+ - Fix or mitigation: depends on severity, typically within 30 days
@@ -16,6 +16,7 @@ class _MixinBase:
16
16
  expected: type[BaseException] | None
17
17
  logger: logging.LoggerAdapter
18
18
  _not_expected: bool
19
+ _expected_warning: type[Warning] | None
19
20
 
20
21
  def error(
21
22
  self,
@@ -35,9 +36,19 @@ class _MixinBase:
35
36
  logger: logging.LoggerAdapter | None = ...,
36
37
  ) -> Self: ...
37
38
 
39
+ # WarningMixin (terminals dispatched from ExceptionMixin.when_called_with)
40
+
41
+ def _when_called_with_warning(
42
+ self, expected: type[Warning], *some_args: object, **some_kwargs: object
43
+ ) -> Self: ...
44
+
45
+ def _when_called_with_not_warning(
46
+ self, expected: type[Warning], *some_args: object, **some_kwargs: object
47
+ ) -> Self: ...
48
+
38
49
  # HelpersMixin
39
50
 
40
- def _fmt_items(self, i: object) -> str: ...
51
+ def _fmt_items(self, items: object) -> str: ...
41
52
 
42
53
  def _fmt_args_kwargs(self, *some_args: object, **some_kwargs: object) -> str: ...
43
54
 
@@ -35,7 +35,7 @@ if TYPE_CHECKING:
35
35
  def is_in(self, *items: object) -> Self: ...
36
36
  def is_not_in(self, *items: object) -> Self: ...
37
37
  # SnapshotMixin
38
- def snapshot(self, id: str | None = ..., path: str = ...) -> Self: ...
38
+ def snapshot(self, id: str | None = ..., path: str = ...) -> Self: ... # noqa: A002 # mirrors public snapshot() parameter
39
39
  # NegatedBuilder
40
40
  @property
41
41
  def not_(self) -> NegatedBuilder: ...
@@ -220,5 +220,7 @@ if TYPE_CHECKING:
220
220
 
221
221
  def raises(self, ex: type) -> Self: ...
222
222
  def does_not_raise(self, ex: type) -> Self: ...
223
+ def warns(self, warning: type[Warning] = ...) -> Self: ...
224
+ def does_not_warn(self, warning: type[Warning] = ...) -> Self: ...
223
225
  def when_called_with(self, *some_args: object, **some_kwargs: object) -> Self: ...
224
226
  def eventually(self, *, timeout: float = ..., interval: float = ...) -> AsyncAssertionBuilder: ...
@@ -45,8 +45,9 @@ from .json_mixin import JsonMixin
45
45
  from .numeric import NumericMixin
46
46
  from .snapshot import SnapshotMixin
47
47
  from .string import StringMixin
48
+ from .warning import WarningMixin
48
49
 
49
- __version__ = "2.5.1"
50
+ __version__ = "2.6.0"
50
51
 
51
52
  __tracebackhide__ = True # clean tracebacks via py.test integration
52
53
  contextlib.__tracebackhide__ = True # ty: ignore[unresolved-attribute] # pytest monkey-patch
@@ -75,6 +76,7 @@ ASSERTPY_FILES = [
75
76
  "pytest_plugin.py",
76
77
  "snapshot.py",
77
78
  "string.py",
79
+ "warning.py",
78
80
  ]
79
81
  ]
80
82
 
@@ -555,6 +557,7 @@ class AssertionBuilder(
555
557
  FileMixin,
556
558
  ExtractingMixin,
557
559
  ExceptionMixin,
560
+ WarningMixin,
558
561
  DynamicMixin,
559
562
  DictMixin,
560
563
  DateMixin,
@@ -585,6 +588,7 @@ class AssertionBuilder(
585
588
  self.expected = expected
586
589
  self.logger = logger if logger else _default_logger
587
590
  self._not_expected = False
591
+ self._expected_warning = None
588
592
 
589
593
  @property
590
594
  def not_(self) -> NegatedBuilder:
@@ -175,17 +175,17 @@ class BaseMixin(_MixinBase):
175
175
  actual=actual,
176
176
  expected=expected,
177
177
  )
178
- for i, (a, b) in enumerate(zip(actual, expected, strict=True)):
179
- a_dict = self._to_comparable_dict(a)
180
- b_dict = self._to_comparable_dict(b)
181
- if a_dict is not None and b_dict is not None:
182
- if self._dict_not_equal(a_dict, b_dict, ignore=ignore, include=include):
183
- self._dict_err(a_dict, b_dict, ignore=ignore, include=include)
184
- elif a != b:
178
+ for index, (actual_item, expected_item) in enumerate(zip(actual, expected, strict=True)):
179
+ actual_dict = self._to_comparable_dict(actual_item)
180
+ expected_dict = self._to_comparable_dict(expected_item)
181
+ if actual_dict is not None and expected_dict is not None:
182
+ if self._dict_not_equal(actual_dict, expected_dict, ignore=ignore, include=include):
183
+ self._dict_err(actual_dict, expected_dict, ignore=ignore, include=include)
184
+ elif actual_item != expected_item:
185
185
  return self.error(
186
- f"Expected item at index <{i}> to be equal to <{b}>, but was <{a}>.",
187
- actual=a,
188
- expected=b,
186
+ f"Expected item at index <{index}> to be equal to <{expected_item}>, but was <{actual_item}>.",
187
+ actual=actual_item,
188
+ expected=expected_item,
189
189
  )
190
190
 
191
191
  @staticmethod
@@ -202,11 +202,11 @@ class BaseMixin(_MixinBase):
202
202
  )
203
203
  _seen = _seen | {pair_key[0], pair_key[1]}
204
204
 
205
- def _field_entries(a_val: object, e_val: object, path: str) -> list[DiffEntry]:
206
- sub = BaseMixin._sub_diff_entries(a_val, e_val, path, _seen=_seen)
207
- if sub is not None:
208
- return sub
209
- return [DiffEntry(path=path, actual=a_val, expected=e_val)]
205
+ def _field_entries(actual_value: object, expected_value: object, path: str) -> list[DiffEntry]:
206
+ sub_entries = BaseMixin._sub_diff_entries(actual_value, expected_value, path, _seen=_seen)
207
+ if sub_entries is not None:
208
+ return sub_entries
209
+ return [DiffEntry(path=path, actual=actual_value, expected=expected_value)]
210
210
 
211
211
  if (
212
212
  hasattr(actual, "_fields")
@@ -216,13 +216,13 @@ class BaseMixin(_MixinBase):
216
216
  ):
217
217
  entries: list[DiffEntry] = []
218
218
  for field in actual._fields: # ty: ignore[not-iterable] # guarded by hasattr check above
219
- a_val = getattr(actual, field)
220
- e_val = getattr(expected, field, _SENTINEL)
219
+ actual_value = getattr(actual, field)
220
+ expected_value = getattr(expected, field, _SENTINEL)
221
221
  path = f"{_prefix}.{field}"
222
- if e_val is _SENTINEL:
223
- entries.append(DiffEntry(path=path, actual=a_val, expected=None))
224
- elif a_val != e_val:
225
- entries.extend(_field_entries(a_val, e_val, path))
222
+ if expected_value is _SENTINEL:
223
+ entries.append(DiffEntry(path=path, actual=actual_value, expected=None))
224
+ elif actual_value != expected_value:
225
+ entries.extend(_field_entries(actual_value, expected_value, path))
226
226
  for field in expected._fields: # ty: ignore[not-iterable] # guarded by hasattr check above
227
227
  if not hasattr(actual, field):
228
228
  entries.append(DiffEntry(path=f"{_prefix}.{field}", actual=None, expected=getattr(expected, field)))
@@ -234,8 +234,8 @@ class BaseMixin(_MixinBase):
234
234
  and not isinstance(expected, type)
235
235
  ):
236
236
  entries = []
237
- actual_names = {f.name for f in dataclasses.fields(actual)}
238
- expected_names = {f.name for f in dataclasses.fields(expected)}
237
+ actual_names = {field.name for field in dataclasses.fields(actual)}
238
+ expected_names = {field.name for field in dataclasses.fields(expected)}
239
239
  for field in sorted(actual_names | expected_names):
240
240
  path = f"{_prefix}.{field}"
241
241
  if field not in expected_names:
@@ -243,27 +243,27 @@ class BaseMixin(_MixinBase):
243
243
  elif field not in actual_names:
244
244
  entries.append(DiffEntry(path=path, actual=None, expected=getattr(expected, field)))
245
245
  else:
246
- a_val = getattr(actual, field)
247
- e_val = getattr(expected, field)
248
- if a_val != e_val:
249
- entries.extend(_field_entries(a_val, e_val, path))
246
+ actual_value = getattr(actual, field)
247
+ expected_value = getattr(expected, field)
248
+ if actual_value != expected_value:
249
+ entries.extend(_field_entries(actual_value, expected_value, path))
250
250
  return DiffResult(kind="dataclass", entries=entries)
251
251
  if _is_model_dump_object(actual) and _is_model_dump_object(expected):
252
252
  actual_dict = actual.model_dump() # ty: ignore[unresolved-attribute] # guarded by _is_model_dump_object check above
253
253
  expected_dict = expected.model_dump() # ty: ignore[unresolved-attribute] # guarded by _is_model_dump_object check above
254
254
  entries = []
255
- for k in sorted(set(actual_dict) | set(expected_dict)):
256
- path = f"{_prefix}.{k}" if _prefix else f".{k}"
257
- if k not in expected_dict:
258
- entries.append(DiffEntry(path=path, actual=actual_dict[k], expected=None))
259
- elif k not in actual_dict:
260
- entries.append(DiffEntry(path=path, actual=None, expected=expected_dict[k]))
261
- elif actual_dict[k] != expected_dict[k]:
262
- sub = BaseMixin._sub_diff_entries(actual_dict[k], expected_dict[k], path, _seen=_seen)
263
- if sub is not None:
264
- entries.extend(sub)
255
+ for key in sorted(set(actual_dict) | set(expected_dict)):
256
+ path = f"{_prefix}.{key}" if _prefix else f".{key}"
257
+ if key not in expected_dict:
258
+ entries.append(DiffEntry(path=path, actual=actual_dict[key], expected=None))
259
+ elif key not in actual_dict:
260
+ entries.append(DiffEntry(path=path, actual=None, expected=expected_dict[key]))
261
+ elif actual_dict[key] != expected_dict[key]:
262
+ sub_entries = BaseMixin._sub_diff_entries(actual_dict[key], expected_dict[key], path, _seen=_seen)
263
+ if sub_entries is not None:
264
+ entries.extend(sub_entries)
265
265
  else:
266
- entries.append(DiffEntry(path=path, actual=actual_dict[k], expected=expected_dict[k]))
266
+ entries.append(DiffEntry(path=path, actual=actual_dict[key], expected=expected_dict[key]))
267
267
  return DiffResult(kind="model", entries=entries)
268
268
  if isinstance(actual, (list, tuple)) and isinstance(expected, (list, tuple)):
269
269
  entries = []
@@ -275,9 +275,9 @@ class BaseMixin(_MixinBase):
275
275
  elif i >= len(expected):
276
276
  entries.append(DiffEntry(path=path, actual=actual[i], expected=None))
277
277
  elif actual[i] != expected[i]:
278
- sub = BaseMixin._sub_diff_entries(actual[i], expected[i], path, _seen=_seen)
279
- if sub:
280
- entries.extend(sub)
278
+ sub_entries = BaseMixin._sub_diff_entries(actual[i], expected[i], path, _seen=_seen)
279
+ if sub_entries:
280
+ entries.extend(sub_entries)
281
281
  else:
282
282
  entries.append(DiffEntry(path=path, actual=actual[i], expected=expected[i]))
283
283
  return DiffResult(kind="sequence", entries=entries)
@@ -317,18 +317,18 @@ class BaseMixin(_MixinBase):
317
317
  if isinstance(actual, dict) and isinstance(expected, dict):
318
318
  child_seen = _seen | {id(actual), id(expected)}
319
319
  entries: list[DiffEntry] = []
320
- for k in sorted(set(actual) | set(expected), key=repr):
321
- path = f"{prefix}.{k}"
322
- if k not in expected:
323
- entries.append(DiffEntry(path=path, actual=actual[k], expected=None)) # ty: ignore[invalid-argument-type]
324
- elif k not in actual:
325
- entries.append(DiffEntry(path=path, actual=None, expected=expected[k])) # ty: ignore[invalid-argument-type]
326
- elif actual[k] != expected[k]: # ty: ignore[invalid-argument-type]
327
- sub = BaseMixin._sub_diff_entries(actual[k], expected[k], path, _seen=child_seen) # ty: ignore[invalid-argument-type]
328
- if sub is not None:
329
- entries.extend(sub)
320
+ for key in sorted(set(actual) | set(expected), key=repr):
321
+ path = f"{prefix}.{key}"
322
+ if key not in expected:
323
+ entries.append(DiffEntry(path=path, actual=actual[key], expected=None)) # ty: ignore[invalid-argument-type]
324
+ elif key not in actual:
325
+ entries.append(DiffEntry(path=path, actual=None, expected=expected[key])) # ty: ignore[invalid-argument-type]
326
+ elif actual[key] != expected[key]: # ty: ignore[invalid-argument-type]
327
+ sub_entries = BaseMixin._sub_diff_entries(actual[key], expected[key], path, _seen=child_seen) # ty: ignore[invalid-argument-type]
328
+ if sub_entries is not None:
329
+ entries.extend(sub_entries)
330
330
  else:
331
- entries.append(DiffEntry(path=path, actual=actual[k], expected=expected[k])) # ty: ignore[invalid-argument-type]
331
+ entries.append(DiffEntry(path=path, actual=actual[key], expected=expected[key])) # ty: ignore[invalid-argument-type]
332
332
  return entries or None
333
333
  if (
334
334
  dataclasses.is_dataclass(actual)
@@ -338,17 +338,21 @@ class BaseMixin(_MixinBase):
338
338
  ):
339
339
  child_seen = _seen | {id(actual), id(expected)}
340
340
  entries = []
341
- for f in dataclasses.fields(actual):
342
- a_val = getattr(actual, f.name)
343
- e_val = getattr(expected, f.name, _SENTINEL)
344
- if e_val is _SENTINEL:
345
- entries.append(DiffEntry(path=f"{prefix}.{f.name}", actual=a_val, expected=None))
346
- elif a_val != e_val:
347
- sub = BaseMixin._sub_diff_entries(a_val, e_val, f"{prefix}.{f.name}", _seen=child_seen)
348
- if sub is not None:
349
- entries.extend(sub)
341
+ for field in dataclasses.fields(actual):
342
+ actual_value = getattr(actual, field.name)
343
+ expected_value = getattr(expected, field.name, _SENTINEL)
344
+ if expected_value is _SENTINEL:
345
+ entries.append(DiffEntry(path=f"{prefix}.{field.name}", actual=actual_value, expected=None))
346
+ elif actual_value != expected_value:
347
+ sub_entries = BaseMixin._sub_diff_entries(
348
+ actual_value, expected_value, f"{prefix}.{field.name}", _seen=child_seen
349
+ )
350
+ if sub_entries is not None:
351
+ entries.extend(sub_entries)
350
352
  else:
351
- entries.append(DiffEntry(path=f"{prefix}.{f.name}", actual=a_val, expected=e_val))
353
+ entries.append(
354
+ DiffEntry(path=f"{prefix}.{field.name}", actual=actual_value, expected=expected_value)
355
+ )
352
356
  return entries or None
353
357
  if (
354
358
  hasattr(actual, "_fields")
@@ -359,16 +363,20 @@ class BaseMixin(_MixinBase):
359
363
  child_seen = _seen | {id(actual), id(expected)}
360
364
  entries = []
361
365
  for field in actual._fields: # ty: ignore[not-iterable] # guarded by hasattr check above
362
- a_val = getattr(actual, field)
363
- e_val = getattr(expected, field, _SENTINEL)
364
- if e_val is _SENTINEL:
365
- entries.append(DiffEntry(path=f"{prefix}.{field}", actual=a_val, expected=None))
366
- elif a_val != e_val:
367
- sub = BaseMixin._sub_diff_entries(a_val, e_val, f"{prefix}.{field}", _seen=child_seen)
368
- if sub is not None:
369
- entries.extend(sub)
366
+ actual_value = getattr(actual, field)
367
+ expected_value = getattr(expected, field, _SENTINEL)
368
+ if expected_value is _SENTINEL:
369
+ entries.append(DiffEntry(path=f"{prefix}.{field}", actual=actual_value, expected=None))
370
+ elif actual_value != expected_value:
371
+ sub_entries = BaseMixin._sub_diff_entries(
372
+ actual_value, expected_value, f"{prefix}.{field}", _seen=child_seen
373
+ )
374
+ if sub_entries is not None:
375
+ entries.extend(sub_entries)
370
376
  else:
371
- entries.append(DiffEntry(path=f"{prefix}.{field}", actual=a_val, expected=e_val))
377
+ entries.append(
378
+ DiffEntry(path=f"{prefix}.{field}", actual=actual_value, expected=expected_value)
379
+ )
372
380
  for field in expected._fields: # ty: ignore[not-iterable] # guarded by hasattr check above
373
381
  if not hasattr(actual, field):
374
382
  entries.append(DiffEntry(path=f"{prefix}.{field}", actual=None, expected=getattr(expected, field)))
@@ -378,18 +386,20 @@ class BaseMixin(_MixinBase):
378
386
  actual_dict = actual.model_dump() # ty: ignore[unresolved-attribute] # guarded by _is_model_dump_object check above
379
387
  expected_dict = expected.model_dump() # ty: ignore[unresolved-attribute] # guarded by _is_model_dump_object check above
380
388
  entries = []
381
- for k in sorted(set(actual_dict) | set(expected_dict)):
382
- path = f"{prefix}.{k}"
383
- if k not in expected_dict:
384
- entries.append(DiffEntry(path=path, actual=actual_dict[k], expected=None))
385
- elif k not in actual_dict:
386
- entries.append(DiffEntry(path=path, actual=None, expected=expected_dict[k]))
387
- elif actual_dict[k] != expected_dict[k]:
388
- sub = BaseMixin._sub_diff_entries(actual_dict[k], expected_dict[k], path, _seen=child_seen)
389
- if sub is not None:
390
- entries.extend(sub)
389
+ for key in sorted(set(actual_dict) | set(expected_dict)):
390
+ path = f"{prefix}.{key}"
391
+ if key not in expected_dict:
392
+ entries.append(DiffEntry(path=path, actual=actual_dict[key], expected=None))
393
+ elif key not in actual_dict:
394
+ entries.append(DiffEntry(path=path, actual=None, expected=expected_dict[key]))
395
+ elif actual_dict[key] != expected_dict[key]:
396
+ sub_entries = BaseMixin._sub_diff_entries(
397
+ actual_dict[key], expected_dict[key], path, _seen=child_seen
398
+ )
399
+ if sub_entries is not None:
400
+ entries.extend(sub_entries)
391
401
  else:
392
- entries.append(DiffEntry(path=path, actual=actual_dict[k], expected=expected_dict[k]))
402
+ entries.append(DiffEntry(path=path, actual=actual_dict[key], expected=expected_dict[key]))
393
403
  return entries or None
394
404
  return None
395
405
 
@@ -875,8 +885,8 @@ class BaseMixin(_MixinBase):
875
885
  if type(some_type) is not type and not issubclass(type(some_type), type):
876
886
  raise TypeError("given arg must be a type")
877
887
  if type(self.val) is not some_type:
878
- t = self._type(self.val)
879
- return self.error(f"Expected <{self.val}:{t}> to be of type <{some_type.__name__}>, but was not.")
888
+ type_name = self._type(self.val)
889
+ return self.error(f"Expected <{self.val}:{type_name}> to be of type <{some_type.__name__}>, but was not.")
880
890
  return self
881
891
 
882
892
  def is_instance_of(self, some_class) -> Self:
@@ -912,9 +922,9 @@ class BaseMixin(_MixinBase):
912
922
  """
913
923
  try:
914
924
  if not isinstance(self.val, some_class):
915
- t = self._type(self.val)
925
+ type_name = self._type(self.val)
916
926
  return self.error(
917
- f"Expected <{self.val}:{t}> to be instance of class <{some_class.__name__}>, but was not."
927
+ f"Expected <{self.val}:{type_name}> to be instance of class <{some_class.__name__}>, but was not."
918
928
  )
919
929
  except TypeError:
920
930
  raise TypeError("given arg must be a class") from None
@@ -93,36 +93,36 @@ class CollectionMixin(_MixinBase):
93
93
  missing = []
94
94
  if hasattr(self.val, "keys") and callable(self.val.keys) and hasattr(self.val, "__getitem__"):
95
95
  superdict = {}
96
- for idx, j in enumerate(supersets):
97
- self._check_dict_like(j, check_values=False, name=f"arg #{idx + 1}")
98
- for k in j:
99
- superdict.update({k: j[k]})
100
-
101
- for i in self.val:
102
- if i not in superdict:
103
- missing.append({i: self.val[i]}) # bad key
104
- elif self.val[i] != superdict[i]:
105
- missing.append({i: self.val[i]}) # bad val
96
+ for superset_index, superset in enumerate(supersets):
97
+ self._check_dict_like(superset, check_values=False, name=f"arg #{superset_index + 1}")
98
+ for key in superset:
99
+ superdict.update({key: superset[key]})
100
+
101
+ for key in self.val:
102
+ if key not in superdict:
103
+ missing.append({key: self.val[key]}) # bad key
104
+ elif self.val[key] != superdict[key]:
105
+ missing.append({key: self.val[key]}) # bad val
106
106
  if missing:
107
107
  return self.error(
108
108
  f"Expected <{self.val}> to be subset of {self._fmt_items(superdict)}, "
109
109
  f"but {self._fmt_items(missing)} {'was' if len(missing) == 1 else 'were'} missing."
110
110
  )
111
111
  else:
112
- superset = set()
113
- for j in supersets:
112
+ superset_values = set()
113
+ for superset in supersets:
114
114
  try:
115
- for k in j:
116
- superset.add(k)
115
+ for key in superset:
116
+ superset_values.add(key)
117
117
  except TypeError:
118
- superset.add(j)
118
+ superset_values.add(superset)
119
119
 
120
- for i in self.val:
121
- if i not in superset:
122
- missing.append(i)
120
+ for item in self.val:
121
+ if item not in superset_values:
122
+ missing.append(item)
123
123
  if missing:
124
124
  return self.error(
125
- f"Expected <{self.val}> to be subset of {self._fmt_items(superset)}, "
125
+ f"Expected <{self.val}> to be subset of {self._fmt_items(superset_values)}, "
126
126
  f"but {self._fmt_items(missing)} {'was' if len(missing) == 1 else 'were'} missing."
127
127
  )
128
128
 
@@ -161,22 +161,22 @@ class CollectionMixin(_MixinBase):
161
161
  if not isinstance(self.val, collections.abc.Iterable):
162
162
  raise TypeError("val is not iterable")
163
163
 
164
- prev = None
165
- for i, x in enumerate(self.val):
166
- if i > 0:
164
+ previous = None
165
+ for index, current in enumerate(self.val):
166
+ if index > 0:
167
167
  if reverse:
168
- if key(x) > key(prev):
168
+ if key(current) > key(previous):
169
169
  return self.error(
170
170
  f"Expected <{self.val}> to be sorted reverse, "
171
- f"but subset {self._fmt_items([prev, x])} at index {i - 1} is not."
171
+ f"but subset {self._fmt_items([previous, current])} at index {index - 1} is not."
172
172
  )
173
173
  else:
174
- if key(x) < key(prev):
174
+ if key(current) < key(previous):
175
175
  return self.error(
176
176
  f"Expected <{self.val}> to be sorted, "
177
- f"but subset {self._fmt_items([prev, x])} at index {i - 1} is not."
177
+ f"but subset {self._fmt_items([previous, current])} at index {index - 1} is not."
178
178
  )
179
- prev = x
179
+ previous = current
180
180
 
181
181
  return self
182
182
 
@@ -237,7 +237,7 @@ class CollectionMixin(_MixinBase):
237
237
  """
238
238
  if not isinstance(self.val, collections.abc.Iterable):
239
239
  raise TypeError("val is not iterable")
240
- return self.builder([x for item in self.val for x in func(item)], self.description, self.kind)
240
+ return self.builder([inner for item in self.val for inner in func(item)], self.description, self.kind)
241
241
 
242
242
  def first(self) -> Self:
243
243
  """Returns a new builder with the first element of val.