From 7b86f4235d73b026e2e1af32be35f24ce4c84e7c Mon Sep 17 00:00:00 2001 From: Chris Rose Date: Sat, 6 Aug 2022 07:48:17 -0700 Subject: [PATCH] fix: provide correct type bounds on sequence matchers fixes #207 `Matcher[object]` is too tight of a bound on the output type; what we're asserting is that a matcher for `Any` type is what `has_properties` will do --- changelog.d/207.bugfix.rst | 1 + src/hamcrest/core/assert_that.py | 14 +++++----- src/hamcrest/library/object/hasproperty.py | 6 ++--- .../library/collection/test_empty.yml | 2 +- .../library/collection/test_generics.yml | 27 +++++++++++++++++++ 5 files changed, 39 insertions(+), 11 deletions(-) create mode 100644 changelog.d/207.bugfix.rst create mode 100644 tests/type-hinting/library/collection/test_generics.yml diff --git a/changelog.d/207.bugfix.rst b/changelog.d/207.bugfix.rst new file mode 100644 index 00000000..ecc2dac0 --- /dev/null +++ b/changelog.d/207.bugfix.rst @@ -0,0 +1 @@ +``has_properties`` now returns ``Matcher[Any]`` type, which addresses type checking errors when nested as a matcher. diff --git a/src/hamcrest/core/assert_that.py b/src/hamcrest/core/assert_that.py index c8882bb5..a31cf3a1 100644 --- a/src/hamcrest/core/assert_that.py +++ b/src/hamcrest/core/assert_that.py @@ -16,16 +16,16 @@ @overload -def assert_that(actual: T, matcher: Matcher[T], reason: str = "") -> None: +def assert_that(actual_or_assertion: T, matcher: Matcher[T], reason: str = "") -> None: ... @overload -def assert_that(assertion: bool, reason: str = "") -> None: +def assert_that(actual_or_assertion: bool, reason: str = "") -> None: ... -def assert_that(actual, matcher=None, reason=""): +def assert_that(actual_or_assertion, matcher=None, reason=""): """Asserts that actual value satisfies matcher. (Can also assert plain boolean condition.) @@ -55,11 +55,11 @@ def assert_that(actual, matcher=None, reason=""): """ if isinstance(matcher, Matcher): - _assert_match(actual=actual, matcher=matcher, reason=reason) + _assert_match(actual=actual_or_assertion, matcher=matcher, reason=reason) else: - if isinstance(actual, Matcher): - warnings.warn("arg1 should be boolean, but was {}".format(type(actual))) - _assert_bool(assertion=cast(bool, actual), reason=cast(str, matcher)) + if isinstance(actual_or_assertion, Matcher): + warnings.warn("arg1 should be boolean, but was {}".format(type(actual_or_assertion))) + _assert_bool(assertion=cast(bool, actual_or_assertion), reason=cast(str, matcher)) def _assert_match(actual: T, matcher: Matcher[T], reason: str) -> None: diff --git a/src/hamcrest/library/object/hasproperty.py b/src/hamcrest/library/object/hasproperty.py index b27036dc..ea2519f7 100644 --- a/src/hamcrest/library/object/hasproperty.py +++ b/src/hamcrest/library/object/hasproperty.py @@ -94,19 +94,19 @@ def has_property(name: str, match: Union[None, Matcher[V], V] = None) -> Matcher # Keyword argument form @overload -def has_properties(**keys_valuematchers: Union[Matcher[V], V]) -> Matcher[object]: +def has_properties(**keys_valuematchers: Union[Matcher[V], V]) -> Matcher[Any]: ... # Name to matcher dict form @overload -def has_properties(keys_valuematchers: Mapping[str, Union[Matcher[V], V]]) -> Matcher[object]: +def has_properties(keys_valuematchers: Mapping[str, Union[Matcher[V], V]]) -> Matcher[Any]: ... # Alternating name/matcher form @overload -def has_properties(*keys_valuematchers: Any) -> Matcher[object]: +def has_properties(*keys_valuematchers: Any) -> Matcher[Any]: ... diff --git a/tests/type-hinting/library/collection/test_empty.yml b/tests/type-hinting/library/collection/test_empty.yml index 5264802c..cf815c72 100644 --- a/tests/type-hinting/library/collection/test_empty.yml +++ b/tests/type-hinting/library/collection/test_empty.yml @@ -5,4 +5,4 @@ from hamcrest import assert_that, is_, empty assert_that([], empty()) - assert_that(99, empty()) # E: Cannot infer type argument 1 of "assert_that" \ No newline at end of file + assert_that(99, empty()) # E: Cannot infer type argument 1 of "assert_that" diff --git a/tests/type-hinting/library/collection/test_generics.yml b/tests/type-hinting/library/collection/test_generics.yml new file mode 100644 index 00000000..4666d5f1 --- /dev/null +++ b/tests/type-hinting/library/collection/test_generics.yml @@ -0,0 +1,27 @@ +- case: valid_has_items_has_properties + skip: platform.python_implementation() == "PyPy" + main: | + from dataclasses import dataclass + from hamcrest import assert_that, has_items, has_properties + + @dataclass + class Example: + name: str + + items = [Example("dave"), Example("wave")] + + a = assert_that(items, has_items(has_properties(name="dave"))) + +- case: valid_has_item_has_properties + skip: platform.python_implementation() == "PyPy" + main: | + from dataclasses import dataclass + from hamcrest import assert_that, has_item, has_properties + + @dataclass + class Example: + name: str + + items = [Example("dave"), Example("wave")] + matcher = has_item(has_properties(name="dave")) + a = assert_that(items, matcher)