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.
- {assertpy2-2.9.0 → assertpy2-2.9.1}/PKG-INFO +3 -1
- {assertpy2-2.9.0 → assertpy2-2.9.1}/README.md +2 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/assertpy2/assertpy.py +1 -1
- {assertpy2-2.9.0 → assertpy2-2.9.1}/assertpy2/matchers.py +40 -50
- {assertpy2-2.9.0 → assertpy2-2.9.1}/docs/comparison.md +1 -1
- {assertpy2-2.9.0 → assertpy2-2.9.1}/docs/type-safety.md +8 -3
- {assertpy2-2.9.0 → assertpy2-2.9.1}/pyproject.toml +1 -1
- assertpy2-2.9.1/tests/test_callable.py +48 -0
- assertpy2-2.9.1/tests/test_chaining.py +22 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_datetime.py +71 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_expected_exception.py +50 -1
- {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_list.py +70 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_matchers.py +268 -0
- assertpy2-2.9.1/tests/test_satisfy.py +93 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_structural.py +10 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/uv.lock +1 -1
- assertpy2-2.9.0/tests/test_matchers_phase3.py +0 -269
- assertpy2-2.9.0/tests/test_phase2.py +0 -349
- {assertpy2-2.9.0 → assertpy2-2.9.1}/.codecov.yml +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/.gitattributes +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/.github/dependabot.yml +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/.github/workflows/ci.yml +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/.github/workflows/codeql.yml +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/.github/workflows/docs.yml +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/.github/workflows/mutation.yml +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/.github/workflows/publish.yml +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/.github/workflows/scorecard.yml +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/.github/workflows/zizmor.yml +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/.gitignore +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/CONTRIBUTING.md +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/LICENSE +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/SECURITY.md +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/assertpy2/__init__.py +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/assertpy2/_compat.py +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/assertpy2/_introspection.py +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/assertpy2/_mixin_base.py +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/assertpy2/_typing.py +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/assertpy2/async_assertions.py +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/assertpy2/base.py +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/assertpy2/behave_matchers.py +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/assertpy2/bytes_mixin.py +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/assertpy2/collection.py +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/assertpy2/contains.py +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/assertpy2/date.py +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/assertpy2/dict.py +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/assertpy2/dynamic.py +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/assertpy2/errors.py +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/assertpy2/exception.py +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/assertpy2/extracting.py +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/assertpy2/file.py +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/assertpy2/helpers.py +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/assertpy2/json_mixin.py +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/assertpy2/numeric.py +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/assertpy2/py.typed +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/assertpy2/pytest_plugin.py +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/assertpy2/snapshot.py +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/assertpy2/string.py +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/assertpy2/warning.py +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/docs/assertions.md +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/docs/assets/diff-equal.png +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/docs/assets/diff-equal.svg +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/docs/assets/diff-match.svg +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/docs/assets/diff-sequence.svg +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/docs/assets/diff-set.svg +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/docs/data.md +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/docs/errors.md +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/docs/extending.md +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/docs/fluent.md +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/docs/getting-started.md +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/docs/index.md +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/docs/integrations.md +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/docs/logo-dark.svg +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/docs/logo.svg +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/docs/matchers.md +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/docs/migration.md +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/docs/testing.md +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/mkdocs.yml +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_async.py +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_behave_matchers.py +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_bool.py +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_bytes.py +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_class.py +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_collection.py +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_core.py +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_custom_dict.py +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_custom_list.py +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_description.py +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_dict.py +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_dict_compare.py +0 -0
- /assertpy2-2.9.0/tests/test_dyn.py → /assertpy2-2.9.1/tests/test_dynamic.py +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_equals.py +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_errors.py +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_expected_warning.py +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_extensions.py +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_extracting.py +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_fail.py +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_file.py +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_grouped_soft.py +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_in.py +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_json.py +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_matcher_registry.py +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_mutation_hardening.py +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_namedtuple.py +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_none.py +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_not.py +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_numbers.py +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_overloads.py +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_pipeline.py +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_property_based.py +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_pytest_plugin.py +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_readme.py +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_recursive_compare.py +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_recursive_compare_attrs.py +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_recursive_compare_pydantic.py +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_regex_groups.py +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_rich_diff.py +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_same_as.py +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_snapshots.py +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_soft.py +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_soft_fail.py +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_string.py +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_traceback.py +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_type.py +0 -0
- {assertpy2-2.9.0 → assertpy2-2.9.1}/tests/test_typing.py +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
569
|
-
if
|
|
570
|
-
return
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
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.
|
|
600
|
+
return [(m.path, m.actual, m.expected_desc) for m in self._walk(value, self._spec, "", set())]
|
|
613
601
|
|
|
614
|
-
def
|
|
602
|
+
def _walk(
|
|
615
603
|
self, value: dict[Any, Any], spec: dict[Any, Any], path: str, seen: set[tuple[int, int]]
|
|
616
|
-
) -> list[
|
|
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[
|
|
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.
|
|
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(
|
|
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.
|
|
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.
|
|
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), [
|
|
46
|
-
[
|
|
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
|
|
@@ -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")
|