assertpy2 2.3.1__tar.gz → 2.3.3__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 (82) hide show
  1. {assertpy2-2.3.1 → assertpy2-2.3.3}/.github/workflows/ci.yml +3 -0
  2. {assertpy2-2.3.1 → assertpy2-2.3.3}/PKG-INFO +52 -3
  3. {assertpy2-2.3.1 → assertpy2-2.3.3}/README.md +50 -1
  4. {assertpy2-2.3.1 → assertpy2-2.3.3}/assertpy2/__init__.py +3 -1
  5. assertpy2-2.3.3/assertpy2/_mixin_base.py +89 -0
  6. {assertpy2-2.3.1 → assertpy2-2.3.3}/assertpy2/assertpy.py +3 -2
  7. {assertpy2-2.3.1 → assertpy2-2.3.3}/assertpy2/base.py +2 -1
  8. {assertpy2-2.3.1 → assertpy2-2.3.3}/assertpy2/behave_matchers.py +1 -1
  9. {assertpy2-2.3.1 → assertpy2-2.3.3}/assertpy2/collection.py +3 -1
  10. {assertpy2-2.3.1 → assertpy2-2.3.3}/assertpy2/contains.py +2 -1
  11. {assertpy2-2.3.1 → assertpy2-2.3.3}/assertpy2/date.py +3 -1
  12. {assertpy2-2.3.1 → assertpy2-2.3.3}/assertpy2/dict.py +3 -1
  13. {assertpy2-2.3.1 → assertpy2-2.3.3}/assertpy2/dynamic.py +3 -1
  14. {assertpy2-2.3.1 → assertpy2-2.3.3}/assertpy2/exception.py +8 -5
  15. {assertpy2-2.3.1 → assertpy2-2.3.3}/assertpy2/extracting.py +3 -1
  16. {assertpy2-2.3.1 → assertpy2-2.3.3}/assertpy2/file.py +4 -7
  17. {assertpy2-2.3.1 → assertpy2-2.3.3}/assertpy2/helpers.py +3 -1
  18. {assertpy2-2.3.1 → assertpy2-2.3.3}/assertpy2/matchers.py +121 -54
  19. {assertpy2-2.3.1 → assertpy2-2.3.3}/assertpy2/numeric.py +3 -1
  20. {assertpy2-2.3.1 → assertpy2-2.3.3}/assertpy2/pytest_plugin.py +1 -1
  21. {assertpy2-2.3.1 → assertpy2-2.3.3}/assertpy2/snapshot.py +8 -3
  22. {assertpy2-2.3.1 → assertpy2-2.3.3}/assertpy2/string.py +93 -1
  23. {assertpy2-2.3.1 → assertpy2-2.3.3}/docs/api.md +102 -0
  24. {assertpy2-2.3.1 → assertpy2-2.3.3}/pyproject.toml +6 -3
  25. assertpy2-2.3.3/tests/test_matcher_registry.py +155 -0
  26. assertpy2-2.3.3/tests/test_regex_groups.py +118 -0
  27. {assertpy2-2.3.1 → assertpy2-2.3.3}/uv.lock +28 -1
  28. {assertpy2-2.3.1 → assertpy2-2.3.3}/.codecov.yml +0 -0
  29. {assertpy2-2.3.1 → assertpy2-2.3.3}/.github/dependabot.yml +0 -0
  30. {assertpy2-2.3.1 → assertpy2-2.3.3}/.github/workflows/codeql.yml +0 -0
  31. {assertpy2-2.3.1 → assertpy2-2.3.3}/.github/workflows/publish.yml +0 -0
  32. {assertpy2-2.3.1 → assertpy2-2.3.3}/.github/workflows/scorecard.yml +0 -0
  33. {assertpy2-2.3.1 → assertpy2-2.3.3}/.gitignore +0 -0
  34. {assertpy2-2.3.1 → assertpy2-2.3.3}/CONTRIBUTING.md +0 -0
  35. {assertpy2-2.3.1 → assertpy2-2.3.3}/LICENSE +0 -0
  36. {assertpy2-2.3.1 → assertpy2-2.3.3}/SECURITY.md +0 -0
  37. {assertpy2-2.3.1 → assertpy2-2.3.3}/assertpy2/_typing.py +0 -0
  38. {assertpy2-2.3.1 → assertpy2-2.3.3}/assertpy2/async_assertions.py +0 -0
  39. {assertpy2-2.3.1 → assertpy2-2.3.3}/assertpy2/errors.py +0 -0
  40. {assertpy2-2.3.1 → assertpy2-2.3.3}/assertpy2/py.typed +0 -0
  41. {assertpy2-2.3.1 → assertpy2-2.3.3}/docs/logo-dark.svg +0 -0
  42. {assertpy2-2.3.1 → assertpy2-2.3.3}/docs/logo.svg +0 -0
  43. {assertpy2-2.3.1 → assertpy2-2.3.3}/tests/test_async.py +0 -0
  44. {assertpy2-2.3.1 → assertpy2-2.3.3}/tests/test_behave_matchers.py +0 -0
  45. {assertpy2-2.3.1 → assertpy2-2.3.3}/tests/test_bool.py +0 -0
  46. {assertpy2-2.3.1 → assertpy2-2.3.3}/tests/test_class.py +0 -0
  47. {assertpy2-2.3.1 → assertpy2-2.3.3}/tests/test_collection.py +0 -0
  48. {assertpy2-2.3.1 → assertpy2-2.3.3}/tests/test_core.py +0 -0
  49. {assertpy2-2.3.1 → assertpy2-2.3.3}/tests/test_custom_dict.py +0 -0
  50. {assertpy2-2.3.1 → assertpy2-2.3.3}/tests/test_custom_list.py +0 -0
  51. {assertpy2-2.3.1 → assertpy2-2.3.3}/tests/test_datetime.py +0 -0
  52. {assertpy2-2.3.1 → assertpy2-2.3.3}/tests/test_description.py +0 -0
  53. {assertpy2-2.3.1 → assertpy2-2.3.3}/tests/test_dict.py +0 -0
  54. {assertpy2-2.3.1 → assertpy2-2.3.3}/tests/test_dict_compare.py +0 -0
  55. {assertpy2-2.3.1 → assertpy2-2.3.3}/tests/test_dyn.py +0 -0
  56. {assertpy2-2.3.1 → assertpy2-2.3.3}/tests/test_equals.py +0 -0
  57. {assertpy2-2.3.1 → assertpy2-2.3.3}/tests/test_errors.py +0 -0
  58. {assertpy2-2.3.1 → assertpy2-2.3.3}/tests/test_expected_exception.py +0 -0
  59. {assertpy2-2.3.1 → assertpy2-2.3.3}/tests/test_extensions.py +0 -0
  60. {assertpy2-2.3.1 → assertpy2-2.3.3}/tests/test_extracting.py +0 -0
  61. {assertpy2-2.3.1 → assertpy2-2.3.3}/tests/test_fail.py +0 -0
  62. {assertpy2-2.3.1 → assertpy2-2.3.3}/tests/test_file.py +0 -0
  63. {assertpy2-2.3.1 → assertpy2-2.3.3}/tests/test_in.py +0 -0
  64. {assertpy2-2.3.1 → assertpy2-2.3.3}/tests/test_list.py +0 -0
  65. {assertpy2-2.3.1 → assertpy2-2.3.3}/tests/test_matchers.py +0 -0
  66. {assertpy2-2.3.1 → assertpy2-2.3.3}/tests/test_matchers_phase3.py +0 -0
  67. {assertpy2-2.3.1 → assertpy2-2.3.3}/tests/test_namedtuple.py +0 -0
  68. {assertpy2-2.3.1 → assertpy2-2.3.3}/tests/test_none.py +0 -0
  69. {assertpy2-2.3.1 → assertpy2-2.3.3}/tests/test_numbers.py +0 -0
  70. {assertpy2-2.3.1 → assertpy2-2.3.3}/tests/test_overloads.py +0 -0
  71. {assertpy2-2.3.1 → assertpy2-2.3.3}/tests/test_phase2.py +0 -0
  72. {assertpy2-2.3.1 → assertpy2-2.3.3}/tests/test_pytest_plugin.py +0 -0
  73. {assertpy2-2.3.1 → assertpy2-2.3.3}/tests/test_readme.py +0 -0
  74. {assertpy2-2.3.1 → assertpy2-2.3.3}/tests/test_same_as.py +0 -0
  75. {assertpy2-2.3.1 → assertpy2-2.3.3}/tests/test_snapshots.py +0 -0
  76. {assertpy2-2.3.1 → assertpy2-2.3.3}/tests/test_soft.py +0 -0
  77. {assertpy2-2.3.1 → assertpy2-2.3.3}/tests/test_soft_fail.py +0 -0
  78. {assertpy2-2.3.1 → assertpy2-2.3.3}/tests/test_string.py +0 -0
  79. {assertpy2-2.3.1 → assertpy2-2.3.3}/tests/test_structural.py +0 -0
  80. {assertpy2-2.3.1 → assertpy2-2.3.3}/tests/test_traceback.py +0 -0
  81. {assertpy2-2.3.1 → assertpy2-2.3.3}/tests/test_type.py +0 -0
  82. {assertpy2-2.3.1 → assertpy2-2.3.3}/tests/test_warn.py +0 -0
@@ -64,3 +64,6 @@ jobs:
64
64
 
65
65
  - name: Ruff format check
66
66
  run: uv run ruff format --check .
67
+
68
+ - name: Type check
69
+ run: uv run ty check assertpy2/
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: assertpy2
3
- Version: 2.3.1
4
- Summary: Fluent assertion library for Python with full type safety and soft assertions
3
+ Version: 2.3.3
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
7
7
  Project-URL: Issues, https://github.com/Solganis/assertpy2/issues
@@ -52,9 +52,11 @@ Description-Content-Type: text/markdown
52
52
  <a href="https://pepy.tech/projects/assertpy2"><img src="https://static.pepy.tech/badge/assertpy2/month" alt="Downloads"></a>
53
53
  <a href="https://pypi.org/project/assertpy2/"><img src="https://img.shields.io/pypi/pyversions/assertpy2" alt="Python"></a>
54
54
  <a href="https://codecov.io/gh/Solganis/assertpy2"><img src="https://codecov.io/gh/Solganis/assertpy2/graph/badge.svg" alt="Coverage"></a>
55
- <a href="https://github.com/Solganis/assertpy2/blob/main/LICENSE"><img src="https://img.shields.io/github/license/Solganis/assertpy2" alt="License"></a>
55
+ <br>
56
+ <a href="https://github.com/Solganis/assertpy2/blob/main/docs/api.md"><img src="https://img.shields.io/badge/Docs-Read%20The%20Docs-black" alt="Documentation"></a>
56
57
  <a href="https://docs.astral.sh/ruff/"><img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json" alt="Ruff"></a>
57
58
  <a href="https://github.com/astral-sh/uv"><img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json" alt="uv"></a>
59
+ <a href="https://github.com/astral-sh/ty"><img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ty/main/assets/badge/v0.json" alt="ty"></a>
58
60
  <a href="https://scorecard.dev/viewer/?uri=github.com/Solganis/assertpy2"><img src="https://api.scorecard.dev/projects/github.com/Solganis/assertpy2/badge" alt="OpenSSF Scorecard"></a>
59
61
  <a href="https://www.bestpractices.dev/projects/12990"><img src="https://www.bestpractices.dev/projects/12990/badge" alt="OpenSSF Best Practices"></a>
60
62
  </p>
@@ -173,6 +175,8 @@ assert_that(items).is_type_of(list).is_length(3).contains("admin")
173
175
  - **Snapshot testing**: store and compare data structures in JSON format, inspired by Jest.
174
176
  - **Allure integration**: auto-attach structured diff and actual/expected data to Allure reports.
175
177
  - **Behave step matchers**: ready-made parameter types (`PositiveInt`, `BoolLike`, etc.) for Behave step definitions.
178
+ - **Custom matchers**: register domain-specific matchers via `register_matcher()`, composable with `&`, `|`, `~`.
179
+ - **Regex group extraction**: `extracting_group()` and `matches_with_groups()` to assert on regex captures fluently.
176
180
  - **Extensions**: add custom assertions via `add_extension()`.
177
181
  - Strings, numbers, lists, tuples, sets, dicts, dates, booleans, objects, exceptions.
178
182
 
@@ -348,6 +352,51 @@ assert_that(fred).has_shoe_size(12)
348
352
  assert_that({"a": 1, "b": 2, "c": 3}).snapshot()
349
353
  ```
350
354
 
355
+ ### Custom matchers
356
+
357
+ Register domain-specific matchers on the `match` namespace with `register_matcher()`:
358
+
359
+ ```py
360
+ from assertpy2 import assert_that, match, register_matcher
361
+
362
+ @register_matcher("is_valid_email")
363
+ def is_valid_email():
364
+ return match.matches_regex(r"^[\w.-]+@[\w.-]+\.\w+$")
365
+
366
+ # parametrised matchers
367
+ @register_matcher("has_status")
368
+ def has_status(expected: str):
369
+ return match.has_property("status", match.equal_to(expected))
370
+
371
+ # use everywhere matchers are accepted
372
+ assert_that("alice@example.com").satisfies(match.is_valid_email())
373
+ assert_that(users).extracting("email").each(match.is_valid_email())
374
+ assert_that(data).matches_structure({"email": match.is_valid_email()})
375
+
376
+ # composition works automatically
377
+ assert_that(email).satisfies(match.is_valid_email() & match.contains_string("@company.com"))
378
+ ```
379
+
380
+ Remove with `unregister_matcher("is_valid_email")`.
381
+
382
+ ### Regex group extraction
383
+
384
+ Extract regex groups and continue the fluent chain:
385
+
386
+ ```py
387
+ log = "2024-01-15 ERROR status=500 path=/api/users"
388
+
389
+ # extract a positional group
390
+ assert_that(log).extracting_group(r"status=(\d+)", 1).is_equal_to("500")
391
+
392
+ # extract a named group
393
+ assert_that(log).extracting_group(r"(?P<level>\w+) status", "level").is_equal_to("ERROR")
394
+
395
+ # get all groups as a tuple or dict (named groups)
396
+ assert_that("key=value").matches_with_groups(r"(?P<k>\w+)=(?P<v>\w+)") \
397
+ .contains_entry({"k": "key"}).contains_entry({"v": "value"})
398
+ ```
399
+
351
400
  ### Extensions
352
401
 
353
402
  ```py
@@ -17,9 +17,11 @@
17
17
  <a href="https://pepy.tech/projects/assertpy2"><img src="https://static.pepy.tech/badge/assertpy2/month" alt="Downloads"></a>
18
18
  <a href="https://pypi.org/project/assertpy2/"><img src="https://img.shields.io/pypi/pyversions/assertpy2" alt="Python"></a>
19
19
  <a href="https://codecov.io/gh/Solganis/assertpy2"><img src="https://codecov.io/gh/Solganis/assertpy2/graph/badge.svg" alt="Coverage"></a>
20
- <a href="https://github.com/Solganis/assertpy2/blob/main/LICENSE"><img src="https://img.shields.io/github/license/Solganis/assertpy2" alt="License"></a>
20
+ <br>
21
+ <a href="https://github.com/Solganis/assertpy2/blob/main/docs/api.md"><img src="https://img.shields.io/badge/Docs-Read%20The%20Docs-black" alt="Documentation"></a>
21
22
  <a href="https://docs.astral.sh/ruff/"><img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json" alt="Ruff"></a>
22
23
  <a href="https://github.com/astral-sh/uv"><img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json" alt="uv"></a>
24
+ <a href="https://github.com/astral-sh/ty"><img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ty/main/assets/badge/v0.json" alt="ty"></a>
23
25
  <a href="https://scorecard.dev/viewer/?uri=github.com/Solganis/assertpy2"><img src="https://api.scorecard.dev/projects/github.com/Solganis/assertpy2/badge" alt="OpenSSF Scorecard"></a>
24
26
  <a href="https://www.bestpractices.dev/projects/12990"><img src="https://www.bestpractices.dev/projects/12990/badge" alt="OpenSSF Best Practices"></a>
25
27
  </p>
@@ -138,6 +140,8 @@ assert_that(items).is_type_of(list).is_length(3).contains("admin")
138
140
  - **Snapshot testing**: store and compare data structures in JSON format, inspired by Jest.
139
141
  - **Allure integration**: auto-attach structured diff and actual/expected data to Allure reports.
140
142
  - **Behave step matchers**: ready-made parameter types (`PositiveInt`, `BoolLike`, etc.) for Behave step definitions.
143
+ - **Custom matchers**: register domain-specific matchers via `register_matcher()`, composable with `&`, `|`, `~`.
144
+ - **Regex group extraction**: `extracting_group()` and `matches_with_groups()` to assert on regex captures fluently.
141
145
  - **Extensions**: add custom assertions via `add_extension()`.
142
146
  - Strings, numbers, lists, tuples, sets, dicts, dates, booleans, objects, exceptions.
143
147
 
@@ -313,6 +317,51 @@ assert_that(fred).has_shoe_size(12)
313
317
  assert_that({"a": 1, "b": 2, "c": 3}).snapshot()
314
318
  ```
315
319
 
320
+ ### Custom matchers
321
+
322
+ Register domain-specific matchers on the `match` namespace with `register_matcher()`:
323
+
324
+ ```py
325
+ from assertpy2 import assert_that, match, register_matcher
326
+
327
+ @register_matcher("is_valid_email")
328
+ def is_valid_email():
329
+ return match.matches_regex(r"^[\w.-]+@[\w.-]+\.\w+$")
330
+
331
+ # parametrised matchers
332
+ @register_matcher("has_status")
333
+ def has_status(expected: str):
334
+ return match.has_property("status", match.equal_to(expected))
335
+
336
+ # use everywhere matchers are accepted
337
+ assert_that("alice@example.com").satisfies(match.is_valid_email())
338
+ assert_that(users).extracting("email").each(match.is_valid_email())
339
+ assert_that(data).matches_structure({"email": match.is_valid_email()})
340
+
341
+ # composition works automatically
342
+ assert_that(email).satisfies(match.is_valid_email() & match.contains_string("@company.com"))
343
+ ```
344
+
345
+ Remove with `unregister_matcher("is_valid_email")`.
346
+
347
+ ### Regex group extraction
348
+
349
+ Extract regex groups and continue the fluent chain:
350
+
351
+ ```py
352
+ log = "2024-01-15 ERROR status=500 path=/api/users"
353
+
354
+ # extract a positional group
355
+ assert_that(log).extracting_group(r"status=(\d+)", 1).is_equal_to("500")
356
+
357
+ # extract a named group
358
+ assert_that(log).extracting_group(r"(?P<level>\w+) status", "level").is_equal_to("ERROR")
359
+
360
+ # get all groups as a tuple or dict (named groups)
361
+ assert_that("key=value").matches_with_groups(r"(?P<k>\w+)=(?P<v>\w+)") \
362
+ .contains_entry({"k": "key"}).contains_entry({"v": "value"})
363
+ ```
364
+
316
365
  ### Extensions
317
366
 
318
367
  ```py
@@ -12,7 +12,7 @@ from .assertpy import (
12
12
  from .async_assertions import AsyncAssertionBuilder
13
13
  from .errors import AssertionFailure, DiffEntry, DiffResult
14
14
  from .file import contents_of
15
- from .matchers import Matcher, match
15
+ from .matchers import Matcher, match, register_matcher, unregister_matcher
16
16
 
17
17
  __all__ = [
18
18
  "AssertionFailure",
@@ -28,7 +28,9 @@ __all__ = [
28
28
  "contents_of",
29
29
  "fail",
30
30
  "match",
31
+ "register_matcher",
31
32
  "remove_extension",
32
33
  "soft_assertions",
33
34
  "soft_fail",
35
+ "unregister_matcher",
34
36
  ]
@@ -0,0 +1,89 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from typing import TYPE_CHECKING, Any
5
+
6
+ if TYPE_CHECKING:
7
+ from typing_extensions import Self
8
+
9
+ from .errors import DiffResult
10
+
11
+
12
+ class _MixinBase:
13
+ if TYPE_CHECKING:
14
+ val: Any
15
+ description: str
16
+ kind: str | None
17
+ expected: type[BaseException] | None
18
+ logger: logging.LoggerAdapter
19
+ _not_expected: bool
20
+
21
+ def error(
22
+ self,
23
+ msg: str,
24
+ *,
25
+ actual: object = ...,
26
+ expected: object = ...,
27
+ diff: DiffResult | None = ...,
28
+ ) -> Self: ...
29
+
30
+ def builder(
31
+ self,
32
+ val: object,
33
+ description: str = ...,
34
+ kind: str | None = ...,
35
+ expected: BaseException | None = ...,
36
+ logger: logging.LoggerAdapter | None = ...,
37
+ ) -> Self: ...
38
+
39
+ # HelpersMixin
40
+
41
+ def _fmt_items(self, i: object) -> str: ...
42
+
43
+ def _fmt_args_kwargs(self, *some_args: object, **some_kwargs: object) -> str: ...
44
+
45
+ def _validate_between_args(self, val_type: type, low: object, high: object) -> None: ...
46
+
47
+ def _validate_close_to_args(self, val: object, other: object, tolerance: object) -> None: ...
48
+
49
+ def _check_dict_like(
50
+ self,
51
+ d: object,
52
+ check_keys: bool = ...,
53
+ check_values: bool = ...,
54
+ check_getitem: bool = ...,
55
+ name: str = ...,
56
+ return_as_bool: bool = ...,
57
+ ) -> bool | None: ...
58
+
59
+ def _check_iterable(self, val: object, check_getitem: bool = ..., name: str = ...) -> None: ...
60
+
61
+ def _dict_not_equal(
62
+ self,
63
+ val: object,
64
+ other: object,
65
+ ignore: object = ...,
66
+ include: object = ...,
67
+ ) -> bool: ...
68
+
69
+ def _dict_err(
70
+ self,
71
+ val: object,
72
+ other: object,
73
+ ignore: object = ...,
74
+ include: object = ...,
75
+ ) -> None: ...
76
+
77
+ # NumericMixin class attrs used by HelpersMixin._validate_between_args
78
+ _NUMERIC_COMPAREABLE: frozenset[type]
79
+ _NUMERIC_NON_COMPAREABLE: frozenset[type]
80
+
81
+ # Cross-mixin methods
82
+
83
+ def contains(self, *items: object) -> Self: ...
84
+
85
+ def does_not_contain(self, *items: object) -> Self: ...
86
+
87
+ def is_equal_to(self, other: object, **kwargs: object) -> Self: ...
88
+
89
+ def is_not_equal_to(self, other: object) -> Self: ...
@@ -73,10 +73,10 @@ from .numeric import NumericMixin
73
73
  from .snapshot import SnapshotMixin
74
74
  from .string import StringMixin
75
75
 
76
- __version__ = "2.3.1"
76
+ __version__ = "2.3.3"
77
77
 
78
78
  __tracebackhide__ = True # clean tracebacks via py.test integration
79
- contextlib.__tracebackhide__ = True # monkey patch contextlib with clean py.test tracebacks
79
+ contextlib.__tracebackhide__ = True # ty: ignore[unresolved-attribute] # pytest monkey-patch
80
80
 
81
81
  # assertpy files
82
82
  ASSERTPY_FILES = [
@@ -475,6 +475,7 @@ class AssertionBuilder(
475
475
  self.kind = kind
476
476
  self.expected = expected
477
477
  self.logger = logger if logger else _default_logger
478
+ self._not_expected = False
478
479
 
479
480
  def builder(self, val, description="", kind=None, expected=None, logger=None):
480
481
  """Helper to build a new :class:`AssertionBuilder` instance. Use this only if not chaining to ``self``.
@@ -31,6 +31,7 @@ from __future__ import annotations
31
31
  import collections.abc
32
32
  from typing import TYPE_CHECKING
33
33
 
34
+ from ._mixin_base import _MixinBase
34
35
  from .errors import DiffEntry, DiffResult
35
36
  from .matchers import Matcher, StructureMatcher
36
37
 
@@ -40,7 +41,7 @@ if TYPE_CHECKING:
40
41
  __tracebackhide__ = True
41
42
 
42
43
 
43
- class BaseMixin:
44
+ class BaseMixin(_MixinBase):
44
45
  """Base mixin."""
45
46
 
46
47
  def described_as(self, description) -> Self:
@@ -77,7 +77,7 @@ def register_assertpy_types() -> None:
77
77
  ImportError: if behave is not installed
78
78
  """
79
79
  try:
80
- from behave import register_type
80
+ from behave import register_type # ty: ignore[unresolved-import] # optional dependency
81
81
  except ImportError:
82
82
  raise ImportError(
83
83
  "behave is required for register_assertpy_types(). Install it with: pip install assertpy2[behave]"
@@ -31,13 +31,15 @@ from __future__ import annotations
31
31
  import collections
32
32
  from typing import TYPE_CHECKING
33
33
 
34
+ from ._mixin_base import _MixinBase
35
+
34
36
  if TYPE_CHECKING:
35
37
  from typing_extensions import Self
36
38
 
37
39
  __tracebackhide__ = True
38
40
 
39
41
 
40
- class CollectionMixin:
42
+ class CollectionMixin(_MixinBase):
41
43
  """Collection assertions mixin."""
42
44
 
43
45
  def is_iterable(self) -> Self:
@@ -31,6 +31,7 @@ from __future__ import annotations
31
31
 
32
32
  from typing import TYPE_CHECKING
33
33
 
34
+ from ._mixin_base import _MixinBase
34
35
  from .matchers import Matcher
35
36
 
36
37
  if TYPE_CHECKING:
@@ -39,7 +40,7 @@ if TYPE_CHECKING:
39
40
  __tracebackhide__ = True
40
41
 
41
42
 
42
- class ContainsMixin:
43
+ class ContainsMixin(_MixinBase):
43
44
  """Containment assertions mixin."""
44
45
 
45
46
  def contains(self, *items) -> Self:
@@ -31,13 +31,15 @@ from __future__ import annotations
31
31
  import datetime
32
32
  from typing import TYPE_CHECKING
33
33
 
34
+ from ._mixin_base import _MixinBase
35
+
34
36
  if TYPE_CHECKING:
35
37
  from typing_extensions import Self
36
38
 
37
39
  __tracebackhide__ = True
38
40
 
39
41
 
40
- class DateMixin:
42
+ class DateMixin(_MixinBase):
41
43
  """Date and time assertions mixin."""
42
44
 
43
45
  def is_before(self, other) -> Self:
@@ -30,13 +30,15 @@ from __future__ import annotations
30
30
 
31
31
  from typing import TYPE_CHECKING
32
32
 
33
+ from ._mixin_base import _MixinBase
34
+
33
35
  if TYPE_CHECKING:
34
36
  from typing_extensions import Self
35
37
 
36
38
  __tracebackhide__ = True
37
39
 
38
40
 
39
- class DictMixin:
41
+ class DictMixin(_MixinBase):
40
42
  """Dict assertions mixin."""
41
43
 
42
44
  def contains_key(self, *keys) -> Self:
@@ -30,10 +30,12 @@ from __future__ import annotations
30
30
 
31
31
  import collections
32
32
 
33
+ from ._mixin_base import _MixinBase
34
+
33
35
  __tracebackhide__ = True
34
36
 
35
37
 
36
- class DynamicMixin:
38
+ class DynamicMixin(_MixinBase):
37
39
  """Dynamic assertions mixin.
38
40
 
39
41
  When testing attributes of an object (or the contents of a dict), the
@@ -28,7 +28,9 @@
28
28
 
29
29
  from __future__ import annotations
30
30
 
31
- from typing import TYPE_CHECKING
31
+ from typing import TYPE_CHECKING, cast
32
+
33
+ from ._mixin_base import _MixinBase
32
34
 
33
35
  if TYPE_CHECKING:
34
36
  from typing_extensions import Self
@@ -46,7 +48,7 @@ class _InertBuilder:
46
48
  return lambda *args, **kwargs: self
47
49
 
48
50
 
49
- class ExceptionMixin:
51
+ class ExceptionMixin(_MixinBase):
50
52
  """Expected exception mixin."""
51
53
 
52
54
  def raises(self, ex) -> Self:
@@ -116,15 +118,16 @@ class ExceptionMixin:
116
118
  f" when called with ({self._fmt_args_kwargs(*some_args, **some_kwargs)}),"
117
119
  f" but raised <{type(e).__name__}>."
118
120
  )
119
- return _InertBuilder()
121
+ return cast("Self", _InertBuilder())
120
122
 
121
123
  self.error(
122
124
  f"Expected <{self.val.__name__}> to raise <{self.expected.__name__}>"
123
125
  f" when called with ({self._fmt_args_kwargs(*some_args, **some_kwargs)})."
124
126
  )
125
- return _InertBuilder()
127
+ return cast("Self", _InertBuilder())
126
128
 
127
129
  def _when_called_with_not_expected(self, *some_args, **some_kwargs) -> Self:
130
+ assert self.expected is not None
128
131
  try:
129
132
  self.val(*some_args, **some_kwargs)
130
133
  except BaseException as e:
@@ -134,7 +137,7 @@ class ExceptionMixin:
134
137
  f" when called with ({self._fmt_args_kwargs(*some_args, **some_kwargs)}),"
135
138
  f" but did raise <{type(e).__name__}>."
136
139
  )
137
- return _InertBuilder()
140
+ return cast("Self", _InertBuilder())
138
141
  return self
139
142
 
140
143
  def does_not_raise(self, ex) -> Self:
@@ -31,13 +31,15 @@ from __future__ import annotations
31
31
  import collections.abc
32
32
  from typing import TYPE_CHECKING
33
33
 
34
+ from ._mixin_base import _MixinBase
35
+
34
36
  if TYPE_CHECKING:
35
37
  from typing_extensions import Self
36
38
 
37
39
  __tracebackhide__ = True
38
40
 
39
41
 
40
- class ExtractingMixin:
42
+ class ExtractingMixin(_MixinBase):
41
43
  """Collection flattening mixin.
42
44
 
43
45
  It is often necessary to test collections of objects. Use the ``extracting()`` helper to
@@ -31,6 +31,8 @@ from __future__ import annotations
31
31
  import os
32
32
  from typing import TYPE_CHECKING
33
33
 
34
+ from ._mixin_base import _MixinBase
35
+
34
36
  if TYPE_CHECKING:
35
37
  from typing_extensions import Self
36
38
 
@@ -72,17 +74,12 @@ def contents_of(file, encoding="utf-8"):
72
74
  raise ValueError(f"val must be file or path, but was type <{type(file).__name__}>") from None
73
75
  raise
74
76
 
75
- if type(contents) is bytes:
76
- return contents.decode(encoding, "replace")
77
- try:
77
+ if isinstance(contents, (bytes, bytearray)):
78
78
  return contents.decode(encoding, "replace")
79
- except AttributeError:
80
- pass # contents is already a str, no decode needed
81
- # if all else fails, just return the contents "as is"
82
79
  return contents
83
80
 
84
81
 
85
- class FileMixin:
82
+ class FileMixin(_MixinBase):
86
83
  """File assertions mixin."""
87
84
 
88
85
  def exists(self) -> Self:
@@ -33,10 +33,12 @@ import numbers
33
33
 
34
34
  from assertpy2.errors import DiffEntry, DiffResult
35
35
 
36
+ from ._mixin_base import _MixinBase
37
+
36
38
  __tracebackhide__ = True
37
39
 
38
40
 
39
- class HelpersMixin:
41
+ class HelpersMixin(_MixinBase):
40
42
  """Helpers mixin. For internal use only."""
41
43
 
42
44
  def _fmt_items(self, i):