assertpy2 2.9.0__tar.gz → 2.9.1__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 (125) hide show
  1. {assertpy2-2.9.0 → assertpy2-2.9.1}/PKG-INFO +3 -1
  2. {assertpy2-2.9.0 → assertpy2-2.9.1}/README.md +2 -0
  3. {assertpy2-2.9.0 → assertpy2-2.9.1}/assertpy2/assertpy.py +1 -1
  4. {assertpy2-2.9.0 → assertpy2-2.9.1}/assertpy2/matchers.py +40 -50
  5. {assertpy2-2.9.0 → assertpy2-2.9.1}/docs/comparison.md +1 -1
  6. {assertpy2-2.9.0 → assertpy2-2.9.1}/docs/type-safety.md +8 -3
  7. {assertpy2-2.9.0 → assertpy2-2.9.1}/pyproject.toml +1 -1
  8. assertpy2-2.9.1/tests/test_callable.py +48 -0
  9. assertpy2-2.9.1/tests/test_chaining.py +22 -0
  10. {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_datetime.py +71 -0
  11. {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_expected_exception.py +50 -1
  12. {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_list.py +70 -0
  13. {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_matchers.py +268 -0
  14. assertpy2-2.9.1/tests/test_satisfy.py +93 -0
  15. {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_structural.py +10 -0
  16. {assertpy2-2.9.0 → assertpy2-2.9.1}/uv.lock +1 -1
  17. assertpy2-2.9.0/tests/test_matchers_phase3.py +0 -269
  18. assertpy2-2.9.0/tests/test_phase2.py +0 -349
  19. {assertpy2-2.9.0 → assertpy2-2.9.1}/.codecov.yml +0 -0
  20. {assertpy2-2.9.0 → assertpy2-2.9.1}/.gitattributes +0 -0
  21. {assertpy2-2.9.0 → assertpy2-2.9.1}/.github/dependabot.yml +0 -0
  22. {assertpy2-2.9.0 → assertpy2-2.9.1}/.github/workflows/ci.yml +0 -0
  23. {assertpy2-2.9.0 → assertpy2-2.9.1}/.github/workflows/codeql.yml +0 -0
  24. {assertpy2-2.9.0 → assertpy2-2.9.1}/.github/workflows/docs.yml +0 -0
  25. {assertpy2-2.9.0 → assertpy2-2.9.1}/.github/workflows/mutation.yml +0 -0
  26. {assertpy2-2.9.0 → assertpy2-2.9.1}/.github/workflows/publish.yml +0 -0
  27. {assertpy2-2.9.0 → assertpy2-2.9.1}/.github/workflows/scorecard.yml +0 -0
  28. {assertpy2-2.9.0 → assertpy2-2.9.1}/.github/workflows/zizmor.yml +0 -0
  29. {assertpy2-2.9.0 → assertpy2-2.9.1}/.gitignore +0 -0
  30. {assertpy2-2.9.0 → assertpy2-2.9.1}/CONTRIBUTING.md +0 -0
  31. {assertpy2-2.9.0 → assertpy2-2.9.1}/LICENSE +0 -0
  32. {assertpy2-2.9.0 → assertpy2-2.9.1}/SECURITY.md +0 -0
  33. {assertpy2-2.9.0 → assertpy2-2.9.1}/assertpy2/__init__.py +0 -0
  34. {assertpy2-2.9.0 → assertpy2-2.9.1}/assertpy2/_compat.py +0 -0
  35. {assertpy2-2.9.0 → assertpy2-2.9.1}/assertpy2/_introspection.py +0 -0
  36. {assertpy2-2.9.0 → assertpy2-2.9.1}/assertpy2/_mixin_base.py +0 -0
  37. {assertpy2-2.9.0 → assertpy2-2.9.1}/assertpy2/_typing.py +0 -0
  38. {assertpy2-2.9.0 → assertpy2-2.9.1}/assertpy2/async_assertions.py +0 -0
  39. {assertpy2-2.9.0 → assertpy2-2.9.1}/assertpy2/base.py +0 -0
  40. {assertpy2-2.9.0 → assertpy2-2.9.1}/assertpy2/behave_matchers.py +0 -0
  41. {assertpy2-2.9.0 → assertpy2-2.9.1}/assertpy2/bytes_mixin.py +0 -0
  42. {assertpy2-2.9.0 → assertpy2-2.9.1}/assertpy2/collection.py +0 -0
  43. {assertpy2-2.9.0 → assertpy2-2.9.1}/assertpy2/contains.py +0 -0
  44. {assertpy2-2.9.0 → assertpy2-2.9.1}/assertpy2/date.py +0 -0
  45. {assertpy2-2.9.0 → assertpy2-2.9.1}/assertpy2/dict.py +0 -0
  46. {assertpy2-2.9.0 → assertpy2-2.9.1}/assertpy2/dynamic.py +0 -0
  47. {assertpy2-2.9.0 → assertpy2-2.9.1}/assertpy2/errors.py +0 -0
  48. {assertpy2-2.9.0 → assertpy2-2.9.1}/assertpy2/exception.py +0 -0
  49. {assertpy2-2.9.0 → assertpy2-2.9.1}/assertpy2/extracting.py +0 -0
  50. {assertpy2-2.9.0 → assertpy2-2.9.1}/assertpy2/file.py +0 -0
  51. {assertpy2-2.9.0 → assertpy2-2.9.1}/assertpy2/helpers.py +0 -0
  52. {assertpy2-2.9.0 → assertpy2-2.9.1}/assertpy2/json_mixin.py +0 -0
  53. {assertpy2-2.9.0 → assertpy2-2.9.1}/assertpy2/numeric.py +0 -0
  54. {assertpy2-2.9.0 → assertpy2-2.9.1}/assertpy2/py.typed +0 -0
  55. {assertpy2-2.9.0 → assertpy2-2.9.1}/assertpy2/pytest_plugin.py +0 -0
  56. {assertpy2-2.9.0 → assertpy2-2.9.1}/assertpy2/snapshot.py +0 -0
  57. {assertpy2-2.9.0 → assertpy2-2.9.1}/assertpy2/string.py +0 -0
  58. {assertpy2-2.9.0 → assertpy2-2.9.1}/assertpy2/warning.py +0 -0
  59. {assertpy2-2.9.0 → assertpy2-2.9.1}/docs/assertions.md +0 -0
  60. {assertpy2-2.9.0 → assertpy2-2.9.1}/docs/assets/diff-equal.png +0 -0
  61. {assertpy2-2.9.0 → assertpy2-2.9.1}/docs/assets/diff-equal.svg +0 -0
  62. {assertpy2-2.9.0 → assertpy2-2.9.1}/docs/assets/diff-match.svg +0 -0
  63. {assertpy2-2.9.0 → assertpy2-2.9.1}/docs/assets/diff-sequence.svg +0 -0
  64. {assertpy2-2.9.0 → assertpy2-2.9.1}/docs/assets/diff-set.svg +0 -0
  65. {assertpy2-2.9.0 → assertpy2-2.9.1}/docs/data.md +0 -0
  66. {assertpy2-2.9.0 → assertpy2-2.9.1}/docs/errors.md +0 -0
  67. {assertpy2-2.9.0 → assertpy2-2.9.1}/docs/extending.md +0 -0
  68. {assertpy2-2.9.0 → assertpy2-2.9.1}/docs/fluent.md +0 -0
  69. {assertpy2-2.9.0 → assertpy2-2.9.1}/docs/getting-started.md +0 -0
  70. {assertpy2-2.9.0 → assertpy2-2.9.1}/docs/index.md +0 -0
  71. {assertpy2-2.9.0 → assertpy2-2.9.1}/docs/integrations.md +0 -0
  72. {assertpy2-2.9.0 → assertpy2-2.9.1}/docs/logo-dark.svg +0 -0
  73. {assertpy2-2.9.0 → assertpy2-2.9.1}/docs/logo.svg +0 -0
  74. {assertpy2-2.9.0 → assertpy2-2.9.1}/docs/matchers.md +0 -0
  75. {assertpy2-2.9.0 → assertpy2-2.9.1}/docs/migration.md +0 -0
  76. {assertpy2-2.9.0 → assertpy2-2.9.1}/docs/testing.md +0 -0
  77. {assertpy2-2.9.0 → assertpy2-2.9.1}/mkdocs.yml +0 -0
  78. {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_async.py +0 -0
  79. {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_behave_matchers.py +0 -0
  80. {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_bool.py +0 -0
  81. {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_bytes.py +0 -0
  82. {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_class.py +0 -0
  83. {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_collection.py +0 -0
  84. {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_core.py +0 -0
  85. {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_custom_dict.py +0 -0
  86. {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_custom_list.py +0 -0
  87. {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_description.py +0 -0
  88. {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_dict.py +0 -0
  89. {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_dict_compare.py +0 -0
  90. /assertpy2-2.9.0/tests/test_dyn.py → /assertpy2-2.9.1/tests/test_dynamic.py +0 -0
  91. {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_equals.py +0 -0
  92. {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_errors.py +0 -0
  93. {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_expected_warning.py +0 -0
  94. {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_extensions.py +0 -0
  95. {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_extracting.py +0 -0
  96. {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_fail.py +0 -0
  97. {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_file.py +0 -0
  98. {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_grouped_soft.py +0 -0
  99. {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_in.py +0 -0
  100. {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_json.py +0 -0
  101. {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_matcher_registry.py +0 -0
  102. {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_mutation_hardening.py +0 -0
  103. {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_namedtuple.py +0 -0
  104. {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_none.py +0 -0
  105. {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_not.py +0 -0
  106. {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_numbers.py +0 -0
  107. {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_overloads.py +0 -0
  108. {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_pipeline.py +0 -0
  109. {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_property_based.py +0 -0
  110. {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_pytest_plugin.py +0 -0
  111. {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_readme.py +0 -0
  112. {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_recursive_compare.py +0 -0
  113. {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_recursive_compare_attrs.py +0 -0
  114. {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_recursive_compare_pydantic.py +0 -0
  115. {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_regex_groups.py +0 -0
  116. {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_rich_diff.py +0 -0
  117. {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_same_as.py +0 -0
  118. {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_snapshots.py +0 -0
  119. {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_soft.py +0 -0
  120. {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_soft_fail.py +0 -0
  121. {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_string.py +0 -0
  122. {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_traceback.py +0 -0
  123. {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_type.py +0 -0
  124. {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_typing.py +0 -0
  125. {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_warn.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: assertpy2
3
- Version: 2.9.0
3
+ Version: 2.9.1
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
@@ -62,6 +62,8 @@ Description-Content-Type: text/markdown
62
62
  <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>
63
63
  <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>
64
64
  <a href="https://www.bestpractices.dev/projects/12990"><img src="https://www.bestpractices.dev/projects/12990/badge" alt="OpenSSF Best Practices"></a>
65
+ <br>
66
+ <a href="https://solganis.github.io/assertpy2/type-safety/"><img src="https://img.shields.io/badge/type--checked-ty%20%7C%20mypy%20%7C%20pyright-2ea043" alt="public overloads type-checked by ty, mypy --strict, and pyright with zero suppressions"></a>
65
67
  </p>
66
68
 
67
69
  ---
@@ -24,6 +24,8 @@
24
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>
25
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>
26
26
  <a href="https://www.bestpractices.dev/projects/12990"><img src="https://www.bestpractices.dev/projects/12990/badge" alt="OpenSSF Best Practices"></a>
27
+ <br>
28
+ <a href="https://solganis.github.io/assertpy2/type-safety/"><img src="https://img.shields.io/badge/type--checked-ty%20%7C%20mypy%20%7C%20pyright-2ea043" alt="public overloads type-checked by ty, mypy --strict, and pyright with zero suppressions"></a>
27
29
  </p>
28
30
 
29
31
  ---
@@ -48,7 +48,7 @@ from .snapshot import SnapshotMixin
48
48
  from .string import StringMixin
49
49
  from .warning import WarningMixin
50
50
 
51
- __version__ = "2.9.0"
51
+ __version__ = "2.9.1"
52
52
 
53
53
  __tracebackhide__ = True # clean tracebacks via py.test integration
54
54
  contextlib.__tracebackhide__ = True # ty: ignore[unresolved-attribute] # pytest monkey-patch
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
  import re
4
4
  import threading
5
5
  import uuid as _uuid_mod
6
- from typing import TYPE_CHECKING, Any, Final, Protocol, runtime_checkable
6
+ from typing import TYPE_CHECKING, Any, Final, NamedTuple, Protocol, runtime_checkable
7
7
 
8
8
  if TYPE_CHECKING:
9
9
  from collections.abc import Callable
@@ -548,6 +548,20 @@ def _describe_spec_value(value: object) -> str:
548
548
  return f"<{value}>"
549
549
 
550
550
 
551
+ class _SpecMismatch(NamedTuple):
552
+ """One structural mismatch produced by a single spec walk.
553
+
554
+ ``expected_desc`` is the public "expected" half (as exposed by ``collect_mismatches``); ``detail``
555
+ carries a matcher's ``describe_mismatch(actual)`` so fail-fast failure text keeps its per-value
556
+ detail without a second traversal.
557
+ """
558
+
559
+ path: str
560
+ actual: object
561
+ expected_desc: str
562
+ detail: str | None
563
+
564
+
551
565
  class StructureMatcher(BaseMatcher):
552
566
  """Matches dicts against a structure spec where values are matchers, raw values, or nested dicts."""
553
567
 
@@ -557,7 +571,7 @@ class StructureMatcher(BaseMatcher):
557
571
  def matches(self, value: Any) -> bool:
558
572
  if not isinstance(value, dict):
559
573
  return False
560
- return self._match_recursive(value, self._spec, "", set()) is None
574
+ return not self._walk(value, self._spec, "", set())
561
575
 
562
576
  def describe(self) -> str:
563
577
  return f"a dict matching structure {_describe_spec_value(self._spec)}"
@@ -565,43 +579,17 @@ class StructureMatcher(BaseMatcher):
565
579
  def describe_mismatch(self, value: Any) -> str:
566
580
  if not isinstance(value, dict):
567
581
  return f"was not a dict: <{value}>"
568
- error = self._match_recursive(value, self._spec, "", set())
569
- if error:
570
- return error
571
- return f"was <{value}>"
572
-
573
- def _match_recursive(
574
- self, value: dict[Any, Any], spec: dict[Any, Any], path: str, seen: set[tuple[int, int]]
575
- ) -> str | None:
576
- pair_id = (id(value), id(spec))
577
- if pair_id in seen:
578
- return f"circular reference detected at <{path or 'root'}>"
579
- seen.add(pair_id)
580
- for key, expected in spec.items():
581
- current_path = f"{path}.{key}" if path else str(key)
582
- if key not in value:
583
- return f"missing key <{current_path}>"
584
- actual = value[key]
585
- if isinstance(expected, StructureMatcher):
586
- if not isinstance(actual, dict):
587
- return f"at <{current_path}>: expected a dict, but was <{actual}>"
588
- error = self._match_recursive(actual, expected._spec, current_path, seen)
589
- if error:
590
- return error
591
- elif isinstance(expected, Matcher):
592
- if not expected.matches(actual):
593
- return (
594
- f"at <{current_path}>: expected {expected.describe()}, but {expected.describe_mismatch(actual)}"
595
- )
596
- elif isinstance(expected, dict):
597
- if not isinstance(actual, dict):
598
- return f"at <{current_path}>: expected a dict, but was <{actual}>"
599
- error = self._match_recursive(actual, expected, current_path, seen)
600
- if error:
601
- return error
602
- elif actual != expected:
603
- return f"at <{current_path}>: expected <{expected}>, but was <{actual}>"
604
- return None
582
+ mismatches = self._walk(value, self._spec, "", set())
583
+ if not mismatches:
584
+ return f"was <{value}>"
585
+ first = mismatches[0]
586
+ if first.actual is _MISSING:
587
+ return f"missing key <{first.path}>"
588
+ if first.expected_desc == "<circular ref>": # cycle sentinel emitted by _walk
589
+ return f"circular reference detected at <{first.path}>"
590
+ if first.detail is not None:
591
+ return f"at <{first.path}>: expected {first.expected_desc}, but {first.detail}"
592
+ return f"at <{first.path}>: expected {first.expected_desc}, but was <{first.actual}>"
605
593
 
606
594
  def collect_mismatches(self, value: dict[Any, Any]) -> list[tuple[str, object, str]]:
607
595
  """Collect every structural mismatch as ``(path, actual, expected_description)``.
@@ -609,34 +597,36 @@ class StructureMatcher(BaseMatcher):
609
597
  Unlike :meth:`describe_mismatch`, this does not stop at the first failure and joins nested
610
598
  paths, so callers can build a path-level :class:`~assertpy2.errors.DiffResult`.
611
599
  """
612
- return self._collect(value, self._spec, "", set())
600
+ return [(m.path, m.actual, m.expected_desc) for m in self._walk(value, self._spec, "", set())]
613
601
 
614
- def _collect(
602
+ def _walk(
615
603
  self, value: dict[Any, Any], spec: dict[Any, Any], path: str, seen: set[tuple[int, int]]
616
- ) -> list[tuple[str, object, str]]:
604
+ ) -> list[_SpecMismatch]:
617
605
  pair_id = (id(value), id(spec))
618
606
  if pair_id in seen:
619
- return [(path or "root", "<circular ref>", "<circular ref>")]
607
+ return [_SpecMismatch(path or "root", "<circular ref>", "<circular ref>", None)]
620
608
  seen = seen | {pair_id}
621
- mismatches: list[tuple[str, object, str]] = []
609
+ mismatches: list[_SpecMismatch] = []
622
610
  for key, expected in spec.items():
623
611
  current_path = f"{path}.{key}" if path else str(key)
624
612
  if key not in value:
625
- mismatches.append((current_path, _MISSING, _describe_spec_value(expected)))
613
+ mismatches.append(_SpecMismatch(current_path, _MISSING, _describe_spec_value(expected), None))
626
614
  continue
627
615
  actual = value[key]
628
616
  if isinstance(expected, StructureMatcher) and isinstance(actual, dict):
629
- mismatches.extend(self._collect(actual, expected._spec, current_path, seen))
617
+ mismatches.extend(self._walk(actual, expected._spec, current_path, seen))
630
618
  elif isinstance(expected, Matcher):
631
619
  if not expected.matches(actual):
632
- mismatches.append((current_path, actual, expected.describe()))
620
+ mismatches.append(
621
+ _SpecMismatch(current_path, actual, expected.describe(), expected.describe_mismatch(actual))
622
+ )
633
623
  elif isinstance(expected, dict):
634
624
  if isinstance(actual, dict):
635
- mismatches.extend(self._collect(actual, expected, current_path, seen))
625
+ mismatches.extend(self._walk(actual, expected, current_path, seen))
636
626
  else:
637
- mismatches.append((current_path, actual, "a dict"))
627
+ mismatches.append(_SpecMismatch(current_path, actual, "a dict", None))
638
628
  elif actual != expected:
639
- mismatches.append((current_path, actual, f"<{expected}>"))
629
+ mismatches.append(_SpecMismatch(current_path, actual, f"<{expected}>", None))
640
630
  return mismatches
641
631
 
642
632
 
@@ -161,7 +161,7 @@ scan. The fluent form trades the zero-import convenience of `==` for a path-leve
161
161
 
162
162
  | | pytest assert | PyHamcrest | assertpy | dirty-equals | **assertpy2** |
163
163
  |---|:---:|:---:|:---:|:---:|:---:|
164
- | Latest release | built-in | 2.1.0 | 1.1 (2020) | 0.9.0 | **2.9.0** |
164
+ | Latest release | built-in | 2.1.0 | 1.1 (2020) | 0.9.0 | **2.9.1** |
165
165
  | Property-based tests | n/a | No | No | No | **Yes** |
166
166
  | Runtime dependencies | **none** | **none** | **none** | **none** | **none on 3.11+** |
167
167
  | License | MIT | BSD | BSD | MIT | BSD-3 |
@@ -42,9 +42,14 @@ assert_that("foo").is_positive() # type error: is_positive is not a strin
42
42
  assert_that(42).is_instance_of("int") # type error: expected `type`, got `str`
43
43
  ```
44
44
 
45
- [ty](https://github.com/astral-sh/ty), [Pyright](https://github.com/microsoft/pyright), and
46
- [Mypy](https://github.com/python/mypy) all report these in the editor and in CI, turning a class of test
47
- bugs into errors you see while typing.
45
+ [ty](https://github.com/astral-sh/ty), [mypy `--strict`](https://github.com/python/mypy), and
46
+ [Pyright](https://github.com/microsoft/pyright) all report these in the editor and in CI, turning a class
47
+ of test bugs into errors you see while typing.
48
+
49
+ Every public `assert_that` overload is pinned by an `assert_type` check in
50
+ [`tests/test_typing.py`](https://github.com/Solganis/assertpy2/blob/main/tests/test_typing.py); CI runs all
51
+ three checkers against that file on every push, with **zero suppressions**, so a regression that broadens or
52
+ changes a return type fails the build. `ty` additionally type-checks the whole package.
48
53
 
49
54
  !!! note "Callables and captured values stay typed too"
50
55
  `assert_that(func).raises(...).when_called_with(...)` exposes string assertions on the captured
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "assertpy2"
3
- version = "2.9.0"
3
+ version = "2.9.1"
4
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"
@@ -0,0 +1,48 @@
1
+ import pytest
2
+
3
+ from assertpy2 import assert_that
4
+
5
+
6
+ class TestIsCallable:
7
+ def test_callable_lambda(self):
8
+ assert_that(lambda: None).is_callable()
9
+
10
+ def test_callable_function(self):
11
+ assert_that(print).is_callable()
12
+
13
+ def test_callable_class(self):
14
+ assert_that(int).is_callable()
15
+
16
+ def test_callable_method(self):
17
+ assert_that("foo".upper).is_callable()
18
+
19
+ def test_callable_failure(self):
20
+ with pytest.raises(AssertionError) as exc_info:
21
+ assert_that(42).is_callable()
22
+ assert_that(str(exc_info.value)).is_equal_to("Expected <42> to be callable, but was not.")
23
+
24
+ def test_callable_string_failure(self):
25
+ with pytest.raises(AssertionError) as exc_info:
26
+ assert_that("foo").is_callable()
27
+ assert_that(str(exc_info.value)).is_equal_to("Expected <foo> to be callable, but was not.")
28
+
29
+
30
+ class TestIsNotCallable:
31
+ def test_not_callable_int(self):
32
+ assert_that(42).is_not_callable()
33
+
34
+ def test_not_callable_string(self):
35
+ assert_that("foo").is_not_callable()
36
+
37
+ def test_not_callable_none(self):
38
+ assert_that(None).is_not_callable()
39
+
40
+ def test_not_callable_failure(self):
41
+ with pytest.raises(AssertionError) as exc_info:
42
+ assert_that(print).is_not_callable()
43
+ assert_that(str(exc_info.value)).contains("to not be callable, but was.")
44
+
45
+ def test_not_callable_lambda_failure(self):
46
+ with pytest.raises(AssertionError) as exc_info:
47
+ assert_that(lambda: None).is_not_callable()
48
+ assert_that(str(exc_info.value)).contains("to not be callable, but was.")
@@ -0,0 +1,22 @@
1
+ import datetime
2
+
3
+ from assertpy2 import assert_that
4
+
5
+
6
+ class TestChaining:
7
+ def test_callable_chain(self):
8
+ assert_that(print).is_callable().is_not_none()
9
+
10
+ def test_satisfy_chain(self):
11
+ assert_that([1, 2, 3]).any_satisfy(lambda x: x > 2).none_satisfy(lambda x: x < 0)
12
+
13
+ def test_contains_exactly_chain(self):
14
+ assert_that([1, 2, 3]).contains_exactly(1, 2, 3).is_length(3)
15
+
16
+ def test_contains_in_order_chain(self):
17
+ assert_that([1, 2, 3]).contains_in_order(1, 3).is_not_empty()
18
+
19
+ def test_datetime_chain(self):
20
+ d1 = datetime.datetime(2020, 1, 1)
21
+ d2 = datetime.datetime(2020, 12, 31)
22
+ assert_that(d1).is_before_or_equal_to(d2).is_after_or_equal_to(d1)
@@ -506,3 +506,74 @@ def test_is_equal_to_ignoring_seconds_each_component_mismatch():
506
506
  ):
507
507
  with pytest.raises(AssertionError):
508
508
  assert_that(base).is_equal_to_ignoring_seconds(other)
509
+
510
+
511
+ class TestIsBeforeOrEqualTo:
512
+ def test_before(self):
513
+ d1 = datetime.datetime(2020, 1, 1)
514
+ d2 = datetime.datetime(2020, 1, 2)
515
+ assert_that(d1).is_before_or_equal_to(d2)
516
+
517
+ def test_equal(self):
518
+ d1 = datetime.datetime(2020, 1, 1, 12, 0, 0)
519
+ assert_that(d1).is_before_or_equal_to(d1)
520
+
521
+ def test_failure(self):
522
+ d1 = datetime.datetime(2020, 1, 2)
523
+ d2 = datetime.datetime(2020, 1, 1)
524
+ with pytest.raises(AssertionError) as exc_info:
525
+ assert_that(d1).is_before_or_equal_to(d2)
526
+ assert_that(str(exc_info.value)).contains("to be before or equal to")
527
+
528
+ def test_bad_val_type(self):
529
+ with pytest.raises(TypeError) as exc_info:
530
+ assert_that("foo").is_before_or_equal_to(datetime.datetime.now())
531
+ assert_that(str(exc_info.value)).contains("val must be datetime")
532
+
533
+ def test_bad_arg_type(self):
534
+ with pytest.raises(TypeError) as exc_info:
535
+ assert_that(datetime.datetime.now()).is_before_or_equal_to("foo")
536
+ assert_that(str(exc_info.value)).contains("given arg must be datetime")
537
+
538
+ def test_date_not_datetime_val(self):
539
+ with pytest.raises(TypeError) as exc_info:
540
+ assert_that(datetime.date(2020, 1, 1)).is_before_or_equal_to(datetime.datetime.now())
541
+ assert_that(str(exc_info.value)).contains("val must be datetime")
542
+
543
+ def test_date_not_datetime_arg(self):
544
+ with pytest.raises(TypeError) as exc_info:
545
+ assert_that(datetime.datetime.now()).is_before_or_equal_to(datetime.date(2020, 1, 1))
546
+ assert_that(str(exc_info.value)).contains("given arg must be datetime")
547
+
548
+
549
+ class TestIsAfterOrEqualTo:
550
+ def test_after(self):
551
+ d1 = datetime.datetime(2020, 1, 2)
552
+ d2 = datetime.datetime(2020, 1, 1)
553
+ assert_that(d1).is_after_or_equal_to(d2)
554
+
555
+ def test_equal(self):
556
+ d1 = datetime.datetime(2020, 1, 1, 12, 0, 0)
557
+ assert_that(d1).is_after_or_equal_to(d1)
558
+
559
+ def test_failure(self):
560
+ d1 = datetime.datetime(2020, 1, 1)
561
+ d2 = datetime.datetime(2020, 1, 2)
562
+ with pytest.raises(AssertionError) as exc_info:
563
+ assert_that(d1).is_after_or_equal_to(d2)
564
+ assert_that(str(exc_info.value)).contains("to be after or equal to")
565
+
566
+ def test_bad_val_type(self):
567
+ with pytest.raises(TypeError) as exc_info:
568
+ assert_that("foo").is_after_or_equal_to(datetime.datetime.now())
569
+ assert_that(str(exc_info.value)).contains("val must be datetime")
570
+
571
+ def test_bad_arg_type(self):
572
+ with pytest.raises(TypeError) as exc_info:
573
+ assert_that(datetime.datetime.now()).is_after_or_equal_to("foo")
574
+ assert_that(str(exc_info.value)).contains("given arg must be datetime")
575
+
576
+ def test_date_not_datetime_val(self):
577
+ with pytest.raises(TypeError) as exc_info:
578
+ assert_that(datetime.date(2020, 1, 1)).is_after_or_equal_to(datetime.datetime.now())
579
+ assert_that(str(exc_info.value)).contains("val must be datetime")
@@ -3,7 +3,7 @@ from io import StringIO
3
3
 
4
4
  import pytest
5
5
 
6
- from assertpy2 import WarningLoggingAdapter, assert_that, assert_warn
6
+ from assertpy2 import WarningLoggingAdapter, assert_that, assert_warn, soft_assertions
7
7
 
8
8
 
9
9
  def test_expected_exception():
@@ -177,3 +177,52 @@ def test_returned_without_return_value_fails():
177
177
  with pytest.raises(TypeError) as exc_info:
178
178
  assert_that(func_no_arg).raises(RuntimeError).when_called_with().returned()
179
179
  assert_that(str(exc_info.value)).contains("no return value captured")
180
+
181
+
182
+ class TestDoesNotRaise:
183
+ def test_no_exception(self):
184
+ def safe_func(x):
185
+ return x + 1
186
+
187
+ assert_that(safe_func).does_not_raise(ValueError).when_called_with(1)
188
+
189
+ def test_different_exception(self):
190
+ def raises_type_error():
191
+ raise TypeError("oops")
192
+
193
+ assert_that(raises_type_error).does_not_raise(ValueError).when_called_with()
194
+
195
+ def test_raises_expected_failure(self):
196
+ def raises_value_error():
197
+ raise ValueError("bad value")
198
+
199
+ with pytest.raises(AssertionError) as exc_info:
200
+ assert_that(raises_value_error).does_not_raise(ValueError).when_called_with()
201
+ assert_that(str(exc_info.value)).contains("to not raise <ValueError>")
202
+ assert_that(str(exc_info.value)).contains("but did raise")
203
+
204
+ def test_raises_subclass_failure(self):
205
+ def raises_file_not_found():
206
+ raise FileNotFoundError("missing")
207
+
208
+ with pytest.raises(AssertionError) as exc_info:
209
+ assert_that(raises_file_not_found).does_not_raise(OSError).when_called_with()
210
+ assert_that(str(exc_info.value)).contains("to not raise <OSError>")
211
+
212
+ def test_not_callable_failure(self):
213
+ with pytest.raises(TypeError) as exc_info:
214
+ assert_that(42).does_not_raise(ValueError)
215
+ assert_that(str(exc_info.value)).is_equal_to("val must be callable")
216
+
217
+ def test_not_exception_failure(self):
218
+ with pytest.raises(TypeError) as exc_info:
219
+ assert_that(lambda: None).does_not_raise(str)
220
+ assert_that(str(exc_info.value)).is_equal_to("given arg must be exception")
221
+
222
+ def test_raises_expected_soft_mode(self):
223
+ def raises_value_error():
224
+ raise ValueError("bad value")
225
+
226
+ with pytest.raises(AssertionError) as exc_info, soft_assertions():
227
+ assert_that(raises_value_error).does_not_raise(ValueError).when_called_with()
228
+ assert_that(str(exc_info.value)).contains("to not raise <ValueError>")
@@ -412,3 +412,73 @@ def test_list_of_dicts():
412
412
  class Person:
413
413
  def __init__(self, name):
414
414
  self.name = name
415
+
416
+
417
+ class TestContainsExactly:
418
+ def test_list(self):
419
+ assert_that([1, 2, 3]).contains_exactly(1, 2, 3)
420
+
421
+ def test_tuple(self):
422
+ assert_that((1, 2, 3)).contains_exactly(1, 2, 3)
423
+
424
+ def test_string_chars(self):
425
+ assert_that("abc").contains_exactly("a", "b", "c")
426
+
427
+ def test_wrong_order_failure(self):
428
+ with pytest.raises(AssertionError) as exc_info:
429
+ assert_that([1, 2, 3]).contains_exactly(1, 3, 2)
430
+ assert_that(str(exc_info.value)).contains("to contain exactly")
431
+
432
+ def test_missing_items_failure(self):
433
+ with pytest.raises(AssertionError) as exc_info:
434
+ assert_that([1, 2, 3]).contains_exactly(1, 2)
435
+ assert_that(str(exc_info.value)).contains("to contain exactly")
436
+
437
+ def test_extra_items_failure(self):
438
+ with pytest.raises(AssertionError) as exc_info:
439
+ assert_that([1, 2]).contains_exactly(1, 2, 3)
440
+ assert_that(str(exc_info.value)).contains("to contain exactly")
441
+
442
+ def test_empty_args_failure(self):
443
+ with pytest.raises(ValueError) as exc_info:
444
+ assert_that([1]).contains_exactly()
445
+ assert_that(str(exc_info.value)).is_equal_to("one or more args must be given")
446
+
447
+ def test_not_iterable_failure(self):
448
+ with pytest.raises(TypeError) as exc_info:
449
+ assert_that(42).contains_exactly(1)
450
+ assert_that(str(exc_info.value)).is_equal_to("val is not iterable")
451
+
452
+
453
+ class TestContainsInOrder:
454
+ def test_all_present(self):
455
+ assert_that([1, 5, 2, 8, 3]).contains_in_order(1, 2, 3)
456
+
457
+ def test_contiguous(self):
458
+ assert_that([1, 2, 3]).contains_in_order(1, 2, 3)
459
+
460
+ def test_strings(self):
461
+ assert_that(["a", "x", "b", "y", "c"]).contains_in_order("a", "b", "c")
462
+
463
+ def test_single_item(self):
464
+ assert_that([1, 2, 3]).contains_in_order(2)
465
+
466
+ def test_wrong_order_failure(self):
467
+ with pytest.raises(AssertionError) as exc_info:
468
+ assert_that([1, 2, 3]).contains_in_order(3, 1)
469
+ assert_that(str(exc_info.value)).contains("in order, but did not")
470
+
471
+ def test_missing_item_failure(self):
472
+ with pytest.raises(AssertionError) as exc_info:
473
+ assert_that([1, 2, 3]).contains_in_order(1, 4)
474
+ assert_that(str(exc_info.value)).contains("in order, but did not")
475
+
476
+ def test_empty_args_failure(self):
477
+ with pytest.raises(ValueError) as exc_info:
478
+ assert_that([1]).contains_in_order()
479
+ assert_that(str(exc_info.value)).is_equal_to("one or more args must be given")
480
+
481
+ def test_not_iterable_failure(self):
482
+ with pytest.raises(TypeError) as exc_info:
483
+ assert_that(42).contains_in_order(1)
484
+ assert_that(str(exc_info.value)).is_equal_to("val is not iterable")