assertpy2 2.3.1__tar.gz → 2.3.2__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 (80) hide show
  1. {assertpy2-2.3.1 → assertpy2-2.3.2}/.github/workflows/ci.yml +3 -0
  2. {assertpy2-2.3.1 → assertpy2-2.3.2}/PKG-INFO +3 -3
  3. {assertpy2-2.3.1 → assertpy2-2.3.2}/README.md +1 -1
  4. assertpy2-2.3.2/assertpy2/_mixin_base.py +89 -0
  5. {assertpy2-2.3.1 → assertpy2-2.3.2}/assertpy2/assertpy.py +3 -2
  6. {assertpy2-2.3.1 → assertpy2-2.3.2}/assertpy2/base.py +2 -1
  7. {assertpy2-2.3.1 → assertpy2-2.3.2}/assertpy2/behave_matchers.py +1 -1
  8. {assertpy2-2.3.1 → assertpy2-2.3.2}/assertpy2/collection.py +3 -1
  9. {assertpy2-2.3.1 → assertpy2-2.3.2}/assertpy2/contains.py +2 -1
  10. {assertpy2-2.3.1 → assertpy2-2.3.2}/assertpy2/date.py +3 -1
  11. {assertpy2-2.3.1 → assertpy2-2.3.2}/assertpy2/dict.py +3 -1
  12. {assertpy2-2.3.1 → assertpy2-2.3.2}/assertpy2/dynamic.py +3 -1
  13. {assertpy2-2.3.1 → assertpy2-2.3.2}/assertpy2/exception.py +8 -5
  14. {assertpy2-2.3.1 → assertpy2-2.3.2}/assertpy2/extracting.py +3 -1
  15. {assertpy2-2.3.1 → assertpy2-2.3.2}/assertpy2/file.py +4 -7
  16. {assertpy2-2.3.1 → assertpy2-2.3.2}/assertpy2/helpers.py +3 -1
  17. {assertpy2-2.3.1 → assertpy2-2.3.2}/assertpy2/matchers.py +54 -54
  18. {assertpy2-2.3.1 → assertpy2-2.3.2}/assertpy2/numeric.py +3 -1
  19. {assertpy2-2.3.1 → assertpy2-2.3.2}/assertpy2/pytest_plugin.py +1 -1
  20. {assertpy2-2.3.1 → assertpy2-2.3.2}/assertpy2/snapshot.py +8 -3
  21. {assertpy2-2.3.1 → assertpy2-2.3.2}/assertpy2/string.py +3 -1
  22. {assertpy2-2.3.1 → assertpy2-2.3.2}/pyproject.toml +6 -3
  23. {assertpy2-2.3.1 → assertpy2-2.3.2}/uv.lock +28 -1
  24. {assertpy2-2.3.1 → assertpy2-2.3.2}/.codecov.yml +0 -0
  25. {assertpy2-2.3.1 → assertpy2-2.3.2}/.github/dependabot.yml +0 -0
  26. {assertpy2-2.3.1 → assertpy2-2.3.2}/.github/workflows/codeql.yml +0 -0
  27. {assertpy2-2.3.1 → assertpy2-2.3.2}/.github/workflows/publish.yml +0 -0
  28. {assertpy2-2.3.1 → assertpy2-2.3.2}/.github/workflows/scorecard.yml +0 -0
  29. {assertpy2-2.3.1 → assertpy2-2.3.2}/.gitignore +0 -0
  30. {assertpy2-2.3.1 → assertpy2-2.3.2}/CONTRIBUTING.md +0 -0
  31. {assertpy2-2.3.1 → assertpy2-2.3.2}/LICENSE +0 -0
  32. {assertpy2-2.3.1 → assertpy2-2.3.2}/SECURITY.md +0 -0
  33. {assertpy2-2.3.1 → assertpy2-2.3.2}/assertpy2/__init__.py +0 -0
  34. {assertpy2-2.3.1 → assertpy2-2.3.2}/assertpy2/_typing.py +0 -0
  35. {assertpy2-2.3.1 → assertpy2-2.3.2}/assertpy2/async_assertions.py +0 -0
  36. {assertpy2-2.3.1 → assertpy2-2.3.2}/assertpy2/errors.py +0 -0
  37. {assertpy2-2.3.1 → assertpy2-2.3.2}/assertpy2/py.typed +0 -0
  38. {assertpy2-2.3.1 → assertpy2-2.3.2}/docs/api.md +0 -0
  39. {assertpy2-2.3.1 → assertpy2-2.3.2}/docs/logo-dark.svg +0 -0
  40. {assertpy2-2.3.1 → assertpy2-2.3.2}/docs/logo.svg +0 -0
  41. {assertpy2-2.3.1 → assertpy2-2.3.2}/tests/test_async.py +0 -0
  42. {assertpy2-2.3.1 → assertpy2-2.3.2}/tests/test_behave_matchers.py +0 -0
  43. {assertpy2-2.3.1 → assertpy2-2.3.2}/tests/test_bool.py +0 -0
  44. {assertpy2-2.3.1 → assertpy2-2.3.2}/tests/test_class.py +0 -0
  45. {assertpy2-2.3.1 → assertpy2-2.3.2}/tests/test_collection.py +0 -0
  46. {assertpy2-2.3.1 → assertpy2-2.3.2}/tests/test_core.py +0 -0
  47. {assertpy2-2.3.1 → assertpy2-2.3.2}/tests/test_custom_dict.py +0 -0
  48. {assertpy2-2.3.1 → assertpy2-2.3.2}/tests/test_custom_list.py +0 -0
  49. {assertpy2-2.3.1 → assertpy2-2.3.2}/tests/test_datetime.py +0 -0
  50. {assertpy2-2.3.1 → assertpy2-2.3.2}/tests/test_description.py +0 -0
  51. {assertpy2-2.3.1 → assertpy2-2.3.2}/tests/test_dict.py +0 -0
  52. {assertpy2-2.3.1 → assertpy2-2.3.2}/tests/test_dict_compare.py +0 -0
  53. {assertpy2-2.3.1 → assertpy2-2.3.2}/tests/test_dyn.py +0 -0
  54. {assertpy2-2.3.1 → assertpy2-2.3.2}/tests/test_equals.py +0 -0
  55. {assertpy2-2.3.1 → assertpy2-2.3.2}/tests/test_errors.py +0 -0
  56. {assertpy2-2.3.1 → assertpy2-2.3.2}/tests/test_expected_exception.py +0 -0
  57. {assertpy2-2.3.1 → assertpy2-2.3.2}/tests/test_extensions.py +0 -0
  58. {assertpy2-2.3.1 → assertpy2-2.3.2}/tests/test_extracting.py +0 -0
  59. {assertpy2-2.3.1 → assertpy2-2.3.2}/tests/test_fail.py +0 -0
  60. {assertpy2-2.3.1 → assertpy2-2.3.2}/tests/test_file.py +0 -0
  61. {assertpy2-2.3.1 → assertpy2-2.3.2}/tests/test_in.py +0 -0
  62. {assertpy2-2.3.1 → assertpy2-2.3.2}/tests/test_list.py +0 -0
  63. {assertpy2-2.3.1 → assertpy2-2.3.2}/tests/test_matchers.py +0 -0
  64. {assertpy2-2.3.1 → assertpy2-2.3.2}/tests/test_matchers_phase3.py +0 -0
  65. {assertpy2-2.3.1 → assertpy2-2.3.2}/tests/test_namedtuple.py +0 -0
  66. {assertpy2-2.3.1 → assertpy2-2.3.2}/tests/test_none.py +0 -0
  67. {assertpy2-2.3.1 → assertpy2-2.3.2}/tests/test_numbers.py +0 -0
  68. {assertpy2-2.3.1 → assertpy2-2.3.2}/tests/test_overloads.py +0 -0
  69. {assertpy2-2.3.1 → assertpy2-2.3.2}/tests/test_phase2.py +0 -0
  70. {assertpy2-2.3.1 → assertpy2-2.3.2}/tests/test_pytest_plugin.py +0 -0
  71. {assertpy2-2.3.1 → assertpy2-2.3.2}/tests/test_readme.py +0 -0
  72. {assertpy2-2.3.1 → assertpy2-2.3.2}/tests/test_same_as.py +0 -0
  73. {assertpy2-2.3.1 → assertpy2-2.3.2}/tests/test_snapshots.py +0 -0
  74. {assertpy2-2.3.1 → assertpy2-2.3.2}/tests/test_soft.py +0 -0
  75. {assertpy2-2.3.1 → assertpy2-2.3.2}/tests/test_soft_fail.py +0 -0
  76. {assertpy2-2.3.1 → assertpy2-2.3.2}/tests/test_string.py +0 -0
  77. {assertpy2-2.3.1 → assertpy2-2.3.2}/tests/test_structural.py +0 -0
  78. {assertpy2-2.3.1 → assertpy2-2.3.2}/tests/test_traceback.py +0 -0
  79. {assertpy2-2.3.1 → assertpy2-2.3.2}/tests/test_type.py +0 -0
  80. {assertpy2-2.3.1 → assertpy2-2.3.2}/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.2
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,9 @@ 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>
56
55
  <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
56
  <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>
57
+ <a href="https://github.com/astral-sh/ty"><img src="https://img.shields.io/badge/type--checker-ty-blue" alt="ty"></a>
58
58
  <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
59
  <a href="https://www.bestpractices.dev/projects/12990"><img src="https://www.bestpractices.dev/projects/12990/badge" alt="OpenSSF Best Practices"></a>
60
60
  </p>
@@ -17,9 +17,9 @@
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>
21
20
  <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
21
  <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>
22
+ <a href="https://github.com/astral-sh/ty"><img src="https://img.shields.io/badge/type--checker-ty-blue" alt="ty"></a>
23
23
  <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
24
  <a href="https://www.bestpractices.dev/projects/12990"><img src="https://www.bestpractices.dev/projects/12990/badge" alt="OpenSSF Best Practices"></a>
25
25
  </p>
@@ -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.2"
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):
@@ -2,30 +2,30 @@ from __future__ import annotations
2
2
 
3
3
  import re
4
4
  import uuid as _uuid_mod
5
- from typing import Protocol, runtime_checkable
5
+ from typing import Any, Protocol, runtime_checkable
6
6
 
7
7
 
8
8
  @runtime_checkable
9
9
  class Matcher(Protocol):
10
10
  """Protocol for composable matcher objects."""
11
11
 
12
- def matches(self, value: object) -> bool: ...
12
+ def matches(self, value: Any) -> bool: ...
13
13
 
14
14
  def describe(self) -> str: ...
15
15
 
16
- def describe_mismatch(self, value: object) -> str: ...
16
+ def describe_mismatch(self, value: Any) -> str: ...
17
17
 
18
18
 
19
19
  class BaseMatcher:
20
20
  """Abstract base for all matchers with operator support."""
21
21
 
22
- def matches(self, value: object) -> bool:
22
+ def matches(self, value: Any) -> bool:
23
23
  raise NotImplementedError
24
24
 
25
25
  def describe(self) -> str:
26
26
  raise NotImplementedError
27
27
 
28
- def describe_mismatch(self, value: object) -> str:
28
+ def describe_mismatch(self, value: Any) -> str:
29
29
  return f"was <{value}>"
30
30
 
31
31
  def __and__(self, other: Matcher) -> AllOfMatcher:
@@ -54,13 +54,13 @@ class AllOfMatcher(BaseMatcher):
54
54
  def __init__(self, *matchers: Matcher):
55
55
  self.matchers = matchers
56
56
 
57
- def matches(self, value: object) -> bool:
57
+ def matches(self, value: Any) -> bool:
58
58
  return all(m.matches(value) for m in self.matchers)
59
59
 
60
60
  def describe(self) -> str:
61
61
  return f"({' and '.join(m.describe() for m in self.matchers)})"
62
62
 
63
- def describe_mismatch(self, value: object) -> str:
63
+ def describe_mismatch(self, value: Any) -> str:
64
64
  failed = [m for m in self.matchers if not m.matches(value)]
65
65
  return f"<{value}> did not satisfy: {', '.join(m.describe() for m in failed)}"
66
66
 
@@ -71,13 +71,13 @@ class AnyOfMatcher(BaseMatcher):
71
71
  def __init__(self, *matchers: Matcher):
72
72
  self.matchers = matchers
73
73
 
74
- def matches(self, value: object) -> bool:
74
+ def matches(self, value: Any) -> bool:
75
75
  return any(m.matches(value) for m in self.matchers)
76
76
 
77
77
  def describe(self) -> str:
78
78
  return f"({' or '.join(m.describe() for m in self.matchers)})"
79
79
 
80
- def describe_mismatch(self, value: object) -> str:
80
+ def describe_mismatch(self, value: Any) -> str:
81
81
  return f"<{value}> satisfied none of: {', '.join(m.describe() for m in self.matchers)}"
82
82
 
83
83
 
@@ -87,13 +87,13 @@ class NotMatcher(BaseMatcher):
87
87
  def __init__(self, matcher: Matcher):
88
88
  self.matcher = matcher
89
89
 
90
- def matches(self, value: object) -> bool:
90
+ def matches(self, value: Any) -> bool:
91
91
  return not self.matcher.matches(value)
92
92
 
93
93
  def describe(self) -> str:
94
94
  return f"not {self.matcher.describe()}"
95
95
 
96
- def describe_mismatch(self, value: object) -> str:
96
+ def describe_mismatch(self, value: Any) -> str:
97
97
  return f"<{value}> unexpectedly matched {self.matcher.describe()}"
98
98
 
99
99
 
@@ -104,7 +104,7 @@ class EqualToMatcher(BaseMatcher):
104
104
  def __init__(self, expected: object):
105
105
  self.expected = expected
106
106
 
107
- def matches(self, value: object) -> bool:
107
+ def matches(self, value: Any) -> bool:
108
108
  return value == self.expected
109
109
 
110
110
  def describe(self) -> str:
@@ -115,7 +115,7 @@ class GreaterThanMatcher(BaseMatcher):
115
115
  def __init__(self, boundary: object):
116
116
  self.boundary = boundary
117
117
 
118
- def matches(self, value: object) -> bool:
118
+ def matches(self, value: Any) -> bool:
119
119
  return value > self.boundary
120
120
 
121
121
  def describe(self) -> str:
@@ -126,7 +126,7 @@ class GreaterThanOrEqualToMatcher(BaseMatcher):
126
126
  def __init__(self, boundary: object):
127
127
  self.boundary = boundary
128
128
 
129
- def matches(self, value: object) -> bool:
129
+ def matches(self, value: Any) -> bool:
130
130
  return value >= self.boundary
131
131
 
132
132
  def describe(self) -> str:
@@ -137,7 +137,7 @@ class LessThanMatcher(BaseMatcher):
137
137
  def __init__(self, boundary: object):
138
138
  self.boundary = boundary
139
139
 
140
- def matches(self, value: object) -> bool:
140
+ def matches(self, value: Any) -> bool:
141
141
  return value < self.boundary
142
142
 
143
143
  def describe(self) -> str:
@@ -148,7 +148,7 @@ class LessThanOrEqualToMatcher(BaseMatcher):
148
148
  def __init__(self, boundary: object):
149
149
  self.boundary = boundary
150
150
 
151
- def matches(self, value: object) -> bool:
151
+ def matches(self, value: Any) -> bool:
152
152
  return value <= self.boundary
153
153
 
154
154
  def describe(self) -> str:
@@ -160,7 +160,7 @@ class BetweenMatcher(BaseMatcher):
160
160
  self.low = low
161
161
  self.high = high
162
162
 
163
- def matches(self, value: object) -> bool:
163
+ def matches(self, value: Any) -> bool:
164
164
  return self.low <= value <= self.high
165
165
 
166
166
  def describe(self) -> str:
@@ -172,7 +172,7 @@ class CloseToMatcher(BaseMatcher):
172
172
  self.expected = expected
173
173
  self.tolerance = tolerance
174
174
 
175
- def matches(self, value: object) -> bool:
175
+ def matches(self, value: Any) -> bool:
176
176
  return abs(value - self.expected) <= self.tolerance
177
177
 
178
178
  def describe(self) -> str:
@@ -183,7 +183,7 @@ class CloseToMatcher(BaseMatcher):
183
183
 
184
184
 
185
185
  class IsNoneMatcher(BaseMatcher):
186
- def matches(self, value: object) -> bool:
186
+ def matches(self, value: Any) -> bool:
187
187
  return value is None
188
188
 
189
189
  def describe(self) -> str:
@@ -191,7 +191,7 @@ class IsNoneMatcher(BaseMatcher):
191
191
 
192
192
 
193
193
  class IsNotNoneMatcher(BaseMatcher):
194
- def matches(self, value: object) -> bool:
194
+ def matches(self, value: Any) -> bool:
195
195
  return value is not None
196
196
 
197
197
  def describe(self) -> str:
@@ -202,18 +202,18 @@ class IsInstanceOfMatcher(BaseMatcher):
202
202
  def __init__(self, expected_type: type):
203
203
  self.expected_type = expected_type
204
204
 
205
- def matches(self, value: object) -> bool:
205
+ def matches(self, value: Any) -> bool:
206
206
  return isinstance(value, self.expected_type)
207
207
 
208
208
  def describe(self) -> str:
209
209
  return f"an instance of <{self.expected_type.__name__}>"
210
210
 
211
- def describe_mismatch(self, value: object) -> str:
211
+ def describe_mismatch(self, value: Any) -> str:
212
212
  return f"was <{value}> of type <{type(value).__name__}>"
213
213
 
214
214
 
215
215
  class IsTruthyMatcher(BaseMatcher):
216
- def matches(self, value: object) -> bool:
216
+ def matches(self, value: Any) -> bool:
217
217
  return bool(value)
218
218
 
219
219
  def describe(self) -> str:
@@ -221,7 +221,7 @@ class IsTruthyMatcher(BaseMatcher):
221
221
 
222
222
 
223
223
  class IsFalsyMatcher(BaseMatcher):
224
- def matches(self, value: object) -> bool:
224
+ def matches(self, value: Any) -> bool:
225
225
  return not bool(value)
226
226
 
227
227
  def describe(self) -> str:
@@ -235,18 +235,18 @@ class HasLengthMatcher(BaseMatcher):
235
235
  def __init__(self, expected_length: int):
236
236
  self.expected_length = expected_length
237
237
 
238
- def matches(self, value: object) -> bool:
238
+ def matches(self, value: Any) -> bool:
239
239
  return len(value) == self.expected_length
240
240
 
241
241
  def describe(self) -> str:
242
242
  return f"a value of length <{self.expected_length}>"
243
243
 
244
- def describe_mismatch(self, value: object) -> str:
244
+ def describe_mismatch(self, value: Any) -> str:
245
245
  return f"was <{value}> with length <{len(value)}>"
246
246
 
247
247
 
248
248
  class IsEmptyMatcher(BaseMatcher):
249
- def matches(self, value: object) -> bool:
249
+ def matches(self, value: Any) -> bool:
250
250
  return len(value) == 0
251
251
 
252
252
  def describe(self) -> str:
@@ -254,7 +254,7 @@ class IsEmptyMatcher(BaseMatcher):
254
254
 
255
255
 
256
256
  class IsNotEmptyMatcher(BaseMatcher):
257
- def matches(self, value: object) -> bool:
257
+ def matches(self, value: Any) -> bool:
258
258
  return len(value) > 0
259
259
 
260
260
  def describe(self) -> str:
@@ -265,7 +265,7 @@ class IsNotEmptyMatcher(BaseMatcher):
265
265
 
266
266
 
267
267
  class IsPositiveMatcher(BaseMatcher):
268
- def matches(self, value: object) -> bool:
268
+ def matches(self, value: Any) -> bool:
269
269
  return value > 0
270
270
 
271
271
  def describe(self) -> str:
@@ -273,7 +273,7 @@ class IsPositiveMatcher(BaseMatcher):
273
273
 
274
274
 
275
275
  class IsNegativeMatcher(BaseMatcher):
276
- def matches(self, value: object) -> bool:
276
+ def matches(self, value: Any) -> bool:
277
277
  return value < 0
278
278
 
279
279
  def describe(self) -> str:
@@ -281,7 +281,7 @@ class IsNegativeMatcher(BaseMatcher):
281
281
 
282
282
 
283
283
  class IsZeroMatcher(BaseMatcher):
284
- def matches(self, value: object) -> bool:
284
+ def matches(self, value: Any) -> bool:
285
285
  return value == 0
286
286
 
287
287
  def describe(self) -> str:
@@ -289,26 +289,26 @@ class IsZeroMatcher(BaseMatcher):
289
289
 
290
290
 
291
291
  class IsEvenMatcher(BaseMatcher):
292
- def matches(self, value: object) -> bool:
292
+ def matches(self, value: Any) -> bool:
293
293
  return isinstance(value, int) and not isinstance(value, bool) and value % 2 == 0
294
294
 
295
295
  def describe(self) -> str:
296
296
  return "an even integer"
297
297
 
298
- def describe_mismatch(self, value: object) -> str:
298
+ def describe_mismatch(self, value: Any) -> str:
299
299
  if isinstance(value, bool) or not isinstance(value, int):
300
300
  return f"was <{value!r}> of type <{type(value).__name__}>, not an integer"
301
301
  return f"was <{value}>, which is odd"
302
302
 
303
303
 
304
304
  class IsOddMatcher(BaseMatcher):
305
- def matches(self, value: object) -> bool:
305
+ def matches(self, value: Any) -> bool:
306
306
  return isinstance(value, int) and not isinstance(value, bool) and value % 2 != 0
307
307
 
308
308
  def describe(self) -> str:
309
309
  return "an odd integer"
310
310
 
311
- def describe_mismatch(self, value: object) -> str:
311
+ def describe_mismatch(self, value: Any) -> str:
312
312
  if isinstance(value, bool) or not isinstance(value, int):
313
313
  return f"was <{value!r}> of type <{type(value).__name__}>, not an integer"
314
314
  return f"was <{value}>, which is even"
@@ -318,26 +318,26 @@ class IsDivisibleByMatcher(BaseMatcher):
318
318
  def __init__(self, divisor: int):
319
319
  self.divisor = divisor
320
320
 
321
- def matches(self, value: object) -> bool:
321
+ def matches(self, value: Any) -> bool:
322
322
  return isinstance(value, int) and not isinstance(value, bool) and value % self.divisor == 0
323
323
 
324
324
  def describe(self) -> str:
325
325
  return f"an integer divisible by <{self.divisor}>"
326
326
 
327
- def describe_mismatch(self, value: object) -> str:
327
+ def describe_mismatch(self, value: Any) -> str:
328
328
  if isinstance(value, bool) or not isinstance(value, int):
329
329
  return f"was <{value!r}> of type <{type(value).__name__}>, not an integer"
330
330
  return f"was <{value}>, which has remainder <{value % self.divisor}>"
331
331
 
332
332
 
333
333
  class IsCallableMatcher(BaseMatcher):
334
- def matches(self, value: object) -> bool:
334
+ def matches(self, value: Any) -> bool:
335
335
  return callable(value)
336
336
 
337
337
  def describe(self) -> str:
338
338
  return "a callable"
339
339
 
340
- def describe_mismatch(self, value: object) -> str:
340
+ def describe_mismatch(self, value: Any) -> str:
341
341
  return f"was <{value!r}> of type <{type(value).__name__}>, which is not callable"
342
342
 
343
343
 
@@ -345,13 +345,13 @@ class IsInMatcher(BaseMatcher):
345
345
  def __init__(self, *values: object):
346
346
  self.values = values
347
347
 
348
- def matches(self, value: object) -> bool:
348
+ def matches(self, value: Any) -> bool:
349
349
  return value in self.values
350
350
 
351
351
  def describe(self) -> str:
352
352
  return f"a value in <{self.values}>"
353
353
 
354
- def describe_mismatch(self, value: object) -> str:
354
+ def describe_mismatch(self, value: Any) -> str:
355
355
  return f"was <{value!r}>, which is not in <{self.values}>"
356
356
 
357
357
 
@@ -360,7 +360,7 @@ class HasPropertyMatcher(BaseMatcher):
360
360
  self.name = name
361
361
  self.matcher = matcher
362
362
 
363
- def matches(self, value: object) -> bool:
363
+ def matches(self, value: Any) -> bool:
364
364
  if not hasattr(value, self.name):
365
365
  return False
366
366
  if self.matcher is not None:
@@ -372,7 +372,7 @@ class HasPropertyMatcher(BaseMatcher):
372
372
  return f"an object with property <{self.name}> matching {self.matcher.describe()}"
373
373
  return f"an object with property <{self.name}>"
374
374
 
375
- def describe_mismatch(self, value: object) -> str:
375
+ def describe_mismatch(self, value: Any) -> str:
376
376
  if not hasattr(value, self.name):
377
377
  return f"<{value!r}> has no property <{self.name}>"
378
378
  if self.matcher is not None:
@@ -388,7 +388,7 @@ class ContainsStringMatcher(BaseMatcher):
388
388
  def __init__(self, substring: str):
389
389
  self.substring = substring
390
390
 
391
- def matches(self, value: object) -> bool:
391
+ def matches(self, value: Any) -> bool:
392
392
  return isinstance(value, str) and self.substring in value
393
393
 
394
394
  def describe(self) -> str:
@@ -399,7 +399,7 @@ class MatchesRegexMatcher(BaseMatcher):
399
399
  def __init__(self, pattern: str):
400
400
  self.pattern = pattern
401
401
 
402
- def matches(self, value: object) -> bool:
402
+ def matches(self, value: Any) -> bool:
403
403
  return isinstance(value, str) and re.search(self.pattern, value) is not None
404
404
 
405
405
  def describe(self) -> str:
@@ -410,7 +410,7 @@ class StartsWithMatcher(BaseMatcher):
410
410
  def __init__(self, prefix: str):
411
411
  self.prefix = prefix
412
412
 
413
- def matches(self, value: object) -> bool:
413
+ def matches(self, value: Any) -> bool:
414
414
  return isinstance(value, str) and value.startswith(self.prefix)
415
415
 
416
416
  def describe(self) -> str:
@@ -421,7 +421,7 @@ class EndsWithMatcher(BaseMatcher):
421
421
  def __init__(self, suffix: str):
422
422
  self.suffix = suffix
423
423
 
424
- def matches(self, value: object) -> bool:
424
+ def matches(self, value: Any) -> bool:
425
425
  return isinstance(value, str) and value.endswith(self.suffix)
426
426
 
427
427
  def describe(self) -> str:
@@ -434,7 +434,7 @@ class EndsWithMatcher(BaseMatcher):
434
434
  class IgnoreMatcher(BaseMatcher):
435
435
  """Always matches. Used in structure specs to accept any value for a field."""
436
436
 
437
- def matches(self, value: object) -> bool:
437
+ def matches(self, value: Any) -> bool:
438
438
  return True
439
439
 
440
440
  def describe(self) -> str:
@@ -444,7 +444,7 @@ class IgnoreMatcher(BaseMatcher):
444
444
  class IsUuidMatcher(BaseMatcher):
445
445
  """Matches valid UUID strings."""
446
446
 
447
- def matches(self, value: object) -> bool:
447
+ def matches(self, value: Any) -> bool:
448
448
  if not isinstance(value, str):
449
449
  return False
450
450
  try:
@@ -460,7 +460,7 @@ class IsUuidMatcher(BaseMatcher):
460
460
  class IsNonEmptyStringMatcher(BaseMatcher):
461
461
  """Matches non-empty strings."""
462
462
 
463
- def matches(self, value: object) -> bool:
463
+ def matches(self, value: Any) -> bool:
464
464
  return isinstance(value, str) and len(value) > 0
465
465
 
466
466
  def describe(self) -> str:
@@ -473,7 +473,7 @@ class EachMatcher(BaseMatcher):
473
473
  def __init__(self, matcher: Matcher):
474
474
  self.matcher = matcher
475
475
 
476
- def matches(self, value: object) -> bool:
476
+ def matches(self, value: Any) -> bool:
477
477
  try:
478
478
  return all(self.matcher.matches(item) for item in value)
479
479
  except TypeError:
@@ -482,7 +482,7 @@ class EachMatcher(BaseMatcher):
482
482
  def describe(self) -> str:
483
483
  return f"each item matching {self.matcher.describe()}"
484
484
 
485
- def describe_mismatch(self, value: object) -> str:
485
+ def describe_mismatch(self, value: Any) -> str:
486
486
  try:
487
487
  for i, item in enumerate(value):
488
488
  if not self.matcher.matches(item):
@@ -498,7 +498,7 @@ class StructureMatcher(BaseMatcher):
498
498
  def __init__(self, spec: dict):
499
499
  self._spec = spec
500
500
 
501
- def matches(self, value: object) -> bool:
501
+ def matches(self, value: Any) -> bool:
502
502
  if not isinstance(value, dict):
503
503
  return False
504
504
  return self._match_recursive(value, self._spec, "") is None
@@ -506,7 +506,7 @@ class StructureMatcher(BaseMatcher):
506
506
  def describe(self) -> str:
507
507
  return f"a dict matching structure {self._describe_spec(self._spec)}"
508
508
 
509
- def describe_mismatch(self, value: object) -> str:
509
+ def describe_mismatch(self, value: Any) -> str:
510
510
  if not isinstance(value, dict):
511
511
  return f"was not a dict: <{value}>"
512
512
  error = self._match_recursive(value, self._spec, "")
@@ -33,13 +33,15 @@ import math
33
33
  import numbers
34
34
  from typing import TYPE_CHECKING
35
35
 
36
+ from ._mixin_base import _MixinBase
37
+
36
38
  if TYPE_CHECKING:
37
39
  from typing_extensions import Self
38
40
 
39
41
  __tracebackhide__ = True
40
42
 
41
43
 
42
- class NumericMixin:
44
+ class NumericMixin(_MixinBase):
43
45
  """Numeric assertions mixin."""
44
46
 
45
47
  _NUMERIC_COMPAREABLE = frozenset({datetime.datetime, datetime.timedelta, datetime.date, datetime.time})
@@ -7,7 +7,7 @@ import warnings
7
7
  import pytest
8
8
 
9
9
  try:
10
- import allure
10
+ import allure # ty: ignore[unresolved-import] # optional dependency
11
11
 
12
12
  _HAS_ALLURE = True # pragma: no cover - only when allure-pytest is installed
13
13
  except ImportError:
@@ -35,13 +35,15 @@ import os
35
35
  import sys
36
36
  from typing import TYPE_CHECKING
37
37
 
38
+ from ._mixin_base import _MixinBase
39
+
38
40
  if TYPE_CHECKING:
39
41
  from typing_extensions import Self
40
42
 
41
43
  __tracebackhide__ = True
42
44
 
43
45
 
44
- class SnapshotMixin:
46
+ class SnapshotMixin(_MixinBase):
45
47
  """Snapshot mixin.
46
48
 
47
49
  Take a snapshot of a python data structure, store it on disk in JSON format, and automatically
@@ -187,9 +189,12 @@ class SnapshotMixin:
187
189
  else:
188
190
  # make id from filename and line number
189
191
  f = inspect.currentframe()
190
- fpath = os.path.basename(f.f_back.f_code.co_filename)
192
+ caller = f.f_back if f is not None else None
193
+ if caller is None: # pragma: no cover - frame introspection always available in CPython
194
+ raise RuntimeError("cannot determine caller frame")
195
+ fpath = os.path.basename(caller.f_code.co_filename)
191
196
  fname = os.path.splitext(fpath)[0]
192
- lineno = str(f.f_back.f_lineno)
197
+ lineno = str(caller.f_lineno)
193
198
  snapname = _name(path, fname)
194
199
 
195
200
  if not os.path.exists(path):
@@ -32,13 +32,15 @@ import collections.abc
32
32
  import re
33
33
  from typing import TYPE_CHECKING
34
34
 
35
+ from ._mixin_base import _MixinBase
36
+
35
37
  if TYPE_CHECKING:
36
38
  from typing_extensions import Self
37
39
 
38
40
  __tracebackhide__ = True
39
41
 
40
42
 
41
- class StringMixin:
43
+ class StringMixin(_MixinBase):
42
44
  """String assertions mixin."""
43
45
 
44
46
  def is_equal_to_ignoring_case(self, other) -> Self:
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
  name = "assertpy2"
3
- version = "2.3.1"
4
- description = "Fluent assertion library for Python with full type safety and soft assertions"
3
+ version = "2.3.2"
4
+ description = "Fluent assertion library for Python with composable matchers, structural matching, and full type safety"
5
5
  readme = "README.md"
6
6
  license = "BSD-3-Clause"
7
7
  requires-python = ">=3.10"
@@ -55,6 +55,7 @@ dev = [
55
55
  "pytest>=9.0.3",
56
56
  "pytest-cov>=6.1",
57
57
  "ruff>=0.15.17",
58
+ "ty>=0.0.49",
58
59
  ]
59
60
 
60
61
  [tool.pytest.ini_options]
@@ -73,7 +74,9 @@ line-length = 120
73
74
  target-version = "py310"
74
75
 
75
76
  [tool.ruff.lint]
76
- select = ["E", "F", "I", "UP", "B", "SIM", "RUF"]
77
+ preview = true
78
+ explicit-preview-rules = true
79
+ select = ["E", "E301", "F", "I", "UP", "B", "SIM", "RUF"]
77
80
  ignore = ["E501"]
78
81
 
79
82
  [tool.ruff.lint.per-file-ignores]
@@ -30,7 +30,7 @@ wheels = [
30
30
 
31
31
  [[package]]
32
32
  name = "assertpy2"
33
- version = "2.2.0"
33
+ version = "2.3.1"
34
34
  source = { editable = "." }
35
35
  dependencies = [
36
36
  { name = "typing-extensions" },
@@ -49,6 +49,7 @@ dev = [
49
49
  { name = "pytest" },
50
50
  { name = "pytest-cov" },
51
51
  { name = "ruff" },
52
+ { name = "ty" },
52
53
  ]
53
54
 
54
55
  [package.metadata]
@@ -64,6 +65,7 @@ dev = [
64
65
  { name = "pytest", specifier = ">=9.0.3" },
65
66
  { name = "pytest-cov", specifier = ">=6.1" },
66
67
  { name = "ruff", specifier = ">=0.15.17" },
68
+ { name = "ty", specifier = ">=0.0.49" },
67
69
  ]
68
70
 
69
71
  [[package]]
@@ -428,6 +430,31 @@ wheels = [
428
430
  { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" },
429
431
  ]
430
432
 
433
+ [[package]]
434
+ name = "ty"
435
+ version = "0.0.49"
436
+ source = { registry = "https://pypi.org/simple" }
437
+ sdist = { url = "https://files.pythonhosted.org/packages/1d/8d/37cb91808069509d43a2a11743e12f1e854fd808dbef2203309d256718cd/ty-0.0.49.tar.gz", hash = "sha256:0a027bd0c9c75d035641a365d087ad883446057f9be0b9826251c2aecafbf145", size = 5884753, upload-time = "2026-06-12T03:08:20.221Z" }
438
+ wheels = [
439
+ { url = "https://files.pythonhosted.org/packages/ca/de/9237c6a96356612dd0393db1e94cf21f903616adf3a3701bf3da6e4adc92/ty-0.0.49-py3-none-linux_armv6l.whl", hash = "sha256:12c0c4310b936d762a8586c210b53d4fa4bb361a04429afa89bf84b922e5e065", size = 11834671, upload-time = "2026-06-12T03:07:53.062Z" },
440
+ { url = "https://files.pythonhosted.org/packages/8f/15/daf5a14a5e07012277d450c75325c94614e2acfec4c620c881486118c410/ty-0.0.49-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:737bfdc2caf9712a8580944dcdc80a450a37a4f2bc83c8fa9b7433b374f9e471", size = 11589570, upload-time = "2026-06-12T03:08:25.779Z" },
441
+ { url = "https://files.pythonhosted.org/packages/7d/58/30bdf98436488aca25f0763bf7f92a061528d42461b686453029e845e4c5/ty-0.0.49-py3-none-macosx_11_0_arm64.whl", hash = "sha256:ab90c1baf3b1701d282fce4b02fa552a962d109f8972c46ef6b22429503bfea4", size = 10985236, upload-time = "2026-06-12T03:08:36.664Z" },
442
+ { url = "https://files.pythonhosted.org/packages/22/45/ece503e4a1396e13a1a9a0cde51afe476a6506a1d557eeadf8ad45c83bc0/ty-0.0.49-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4ce8ecf6ba6fc79bd137cc0557a754f7e5f2dfe9436412551d480d680e248ad", size = 11504302, upload-time = "2026-06-12T03:08:01.664Z" },
443
+ { url = "https://files.pythonhosted.org/packages/17/dc/5d09333d289dfbca1804eaade125c9e8a1a992a2a592a8b80c5e9b589ca9/ty-0.0.49-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:10d85c6865c984e78661e0bd20b180514b4a289739224e84816e342bdf381e04", size = 11626629, upload-time = "2026-06-12T03:08:06.844Z" },
444
+ { url = "https://files.pythonhosted.org/packages/f2/36/155f41c9dd7237c4b609211f29f77755a139ee6218605dadc7fe21d5e3c8/ty-0.0.49-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d96a67a206619e01fa92f35a22267ec634bba62be24b1d0e947020cc179995b", size = 12074481, upload-time = "2026-06-12T03:08:09.643Z" },
445
+ { url = "https://files.pythonhosted.org/packages/96/4c/998ee13cd5045f1f8b36982de7343163832ac53f27debe01b0de0e8bd968/ty-0.0.49-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3de9f648564e0a66344ef397770387cb0d093735f8679d2c5a08a4741e79814d", size = 12678042, upload-time = "2026-06-12T03:08:39.319Z" },
446
+ { url = "https://files.pythonhosted.org/packages/85/c9/9a505aba85c41ce54cbcaa14f8d79aa084b86151d2d70df11c4655b92898/ty-0.0.49-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5779179ab397d15f8c9dbb8f506ec1b1745f54eac639982f76ef3ce538943b50", size = 12316194, upload-time = "2026-06-12T03:08:18.023Z" },
447
+ { url = "https://files.pythonhosted.org/packages/c9/b8/ded37fb93503294abbc83c36470bb1413bea05048b745881d4470b518a06/ty-0.0.49-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:792d4974e93cc09bd32f934586080bbbe21b8e777099cb521cb2de18b68a49f0", size = 12145507, upload-time = "2026-06-12T03:07:56.505Z" },
448
+ { url = "https://files.pythonhosted.org/packages/2f/07/392e80d78f02445f695b815bb9eb0fffacda68b03faee38c900f7b990815/ty-0.0.49-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:727bda86deb136073e525c2e78d60e38aedcce5d80579170844a52bbf7c1440d", size = 12365967, upload-time = "2026-06-12T03:08:12.553Z" },
449
+ { url = "https://files.pythonhosted.org/packages/50/d3/31b0c2a7fbedd3373e389cb1d81b8d2128f6f868fafb46557736a6f9aca8/ty-0.0.49-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4f2fc2bc4a8d2ff1cca59fd94772cabdfec4062d47a0b3a0784be46d94d0540b", size = 11475283, upload-time = "2026-06-12T03:08:28.334Z" },
450
+ { url = "https://files.pythonhosted.org/packages/5a/5b/329e101638920b468a3bb63059c9f66ef99b44aac501222c44832a507321/ty-0.0.49-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:3724bd9badef333321578b6a941fbc571ebf49141ec2356a8590fbe4c9aa588d", size = 11645343, upload-time = "2026-06-12T03:08:15.246Z" },
451
+ { url = "https://files.pythonhosted.org/packages/a9/76/c897e615e32f80ca81c8c1bc49b9a1f72ff9e3cfea0f8345ba505fe28472/ty-0.0.49-py3-none-musllinux_1_2_i686.whl", hash = "sha256:166c6eb52ee4af3c5a9bb267d165d93000daa55c6758cd8ff3199741fb75917d", size = 11725585, upload-time = "2026-06-12T03:08:33.915Z" },
452
+ { url = "https://files.pythonhosted.org/packages/59/e1/fdb42ee239f618800842681af5bb8598117e74512c10974a8b7b9086a898/ty-0.0.49-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:91e81d832c287b05782ee32eb1b801f62c1fa08df37d589d2b88c3f1d51c9731", size = 12237261, upload-time = "2026-06-12T03:08:31.105Z" },
453
+ { url = "https://files.pythonhosted.org/packages/98/0f/a2d6a5fc9d0786cbeb3c200786da4e18c203589be3984bb5def83ca92320/ty-0.0.49-py3-none-win32.whl", hash = "sha256:7186af5ca9829d1f5d8916bcf767b8e819bfbf61b1b8ec843bb3fc699cb502e1", size = 11100789, upload-time = "2026-06-12T03:07:59.092Z" },
454
+ { url = "https://files.pythonhosted.org/packages/d0/9d/473ac8bc57b5a2d121da893bf9dd74a118efb19a01d711df1a6e397f05cc/ty-0.0.49-py3-none-win_amd64.whl", hash = "sha256:ae2142fc126a01effcca0c222908b0e6654b5ba1266d4e4d406e4866aef8e1d1", size = 12204644, upload-time = "2026-06-12T03:08:04.327Z" },
455
+ { url = "https://files.pythonhosted.org/packages/ef/a2/8959249da951ba3977fee20e688d28678b8a1d30a9ed4464228a85d45853/ty-0.0.49-py3-none-win_arm64.whl", hash = "sha256:75d5e2e7649765f31f4bed6c8adb149a75b18edd3fa6336dac4d0efc1a66466f", size = 11558965, upload-time = "2026-06-12T03:08:23.012Z" },
456
+ ]
457
+
431
458
  [[package]]
432
459
  name = "typing-extensions"
433
460
  version = "4.15.0"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes