Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -171,3 +171,6 @@ known_third_party = ["hypothesis", "pytest", "setuptools", "six"]
filename = "CHANGELOG.rst"
directory = "changelog.d"
issue_format = "`#{issue} <https://github.com/hamcrest/PyHamcrest/issues/{issue}>`_"

[dependency-groups]
dev = [ "PyHamcrest[dev]" ]
8 changes: 7 additions & 1 deletion src/hamcrest/core/core/allof.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Optional, TypeVar, Union
from typing import Optional, TypeVar, Union, overload

from hamcrest.core.base_matcher import BaseMatcher
from hamcrest.core.description import Description
Expand Down Expand Up @@ -42,6 +42,12 @@ def describe_to(self, description: Description) -> None:
description.append_list("(", " and ", ")", self.matchers)


@overload
def all_of(*items: Matcher[T]) -> Matcher[T]: ...
@overload
def all_of(*items: T) -> Matcher[T]: ...


def all_of(*items: Union[Matcher[T], T]) -> Matcher[T]:
"""Matches if all of the given matchers evaluate to ``True``.

Expand Down
8 changes: 7 additions & 1 deletion src/hamcrest/core/core/anyof.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import TypeVar, Union
from typing import TypeVar, Union, overload

from hamcrest.core.base_matcher import BaseMatcher
from hamcrest.core.description import Description
Expand Down Expand Up @@ -26,6 +26,12 @@ def describe_to(self, description: Description) -> None:
description.append_list("(", " or ", ")", self.matchers)


@overload
def any_of(*items: Matcher[T]) -> Matcher[T]: ...
@overload
def any_of(*items: T) -> Matcher[T]: ...


def any_of(*items: Union[Matcher[T], T]) -> Matcher[T]:
"""Matches if any of the given matchers evaluate to ``True``.

Expand Down
46 changes: 31 additions & 15 deletions src/hamcrest/library/collection/isdict_containingentries.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Any, Hashable, Mapping, Optional, TypeVar, Union, overload
from typing import Hashable, Mapping, Optional, TypeVar, Union, overload

from hamcrest.core.base_matcher import BaseMatcher
from hamcrest.core.description import Description
Expand All @@ -14,7 +14,7 @@


class IsDictContainingEntries(BaseMatcher[Mapping[K, V]]):
def __init__(self, value_matchers) -> None:
def __init__(self, value_matchers: Mapping[K, Matcher[V]]) -> None:
self.value_matchers = sorted(value_matchers.items())

def _not_a_dictionary(
Expand Down Expand Up @@ -56,7 +56,7 @@ def matches(
def describe_mismatch(self, item: Mapping[K, V], mismatch_description: Description) -> None:
self.matches(item, mismatch_description)

def describe_keyvalue(self, index: K, value: V, description: Description) -> None:
def describe_keyvalue(self, index: K, value: Matcher[V], description: Description) -> None:
"""Describes key-value pair at given index."""
description.append_description_of(index).append_text(": ").append_description_of(value)

Expand All @@ -73,20 +73,34 @@ def describe_to(self, description: Description) -> None:

# Keyword argument form
@overload
def has_entries(**keys_valuematchers: Union[Matcher[V], V]) -> Matcher[Mapping[str, V]]: ...
def has_entries(**matchermap: Matcher[V]) -> Matcher[Mapping[str, V]]: ...
@overload
def has_entries(**matchermap: V) -> Matcher[Mapping[str, V]]: ...


# Key to matcher dict form
@overload
def has_entries(keys_valuematchers: Mapping[K, Union[Matcher[V], V]]) -> Matcher[Mapping[K, V]]: ...
def has_entries(__matchermap: Mapping[K, Matcher[V]]) -> Matcher[Mapping[K, V]]: ...
@overload
def has_entries(__matchermap: Mapping[K, V]) -> Matcher[Mapping[K, V]]: ...


# Alternating key/matcher form
@overload
def has_entries(*keys_valuematchers: Any) -> Matcher[Mapping[Any, Any]]: ...
def has_entries(
__key: K,
__value: Matcher[V],
*key_or_valuematchers: Union[K, Matcher[V], V],
) -> Matcher[Mapping[K, V]]: ...
@overload
def has_entries(
__key: K,
__value: V,
*key_or_valuematchers: Union[K, Matcher[V], V],
) -> Matcher[Mapping[K, V]]: ...


def has_entries(*keys_valuematchers, **kv_args):
def has_entries(*key_or_valuematcher_or_matchermaps, **matchermap):
"""Matches if dictionary contains entries satisfying a dictionary of keys
and corresponding value matchers.

Expand Down Expand Up @@ -132,25 +146,27 @@ def has_entries(*keys_valuematchers, **kv_args):
has_entries('foo', 1, 'bar', 2)

"""
if len(keys_valuematchers) == 1:
if len(key_or_valuematcher_or_matchermaps) == 1:
matchermap0 = key_or_valuematcher_or_matchermaps[0]
try:
base_dict = keys_valuematchers[0].copy()
base_dict = matchermap0.copy()
for key in base_dict:
base_dict[key] = wrap_matcher(base_dict[key])
except AttributeError:
raise ValueError(
"single-argument calls to has_entries must pass a dict as the argument"
)
else:
if len(keys_valuematchers) % 2:
key_or_valuematchers = key_or_valuematcher_or_matchermaps
if len(key_or_valuematchers) % 2:
raise ValueError("has_entries requires key-value pairs")
base_dict = {}
for index in range(int(len(keys_valuematchers) / 2)):
base_dict[keys_valuematchers[2 * index]] = wrap_matcher(
keys_valuematchers[2 * index + 1]
)
for index in range(int(len(key_or_valuematchers) / 2)):
key = key_or_valuematchers[2 * index]
valuematcher = key_or_valuematchers[2 * index + 1]
base_dict[key] = wrap_matcher(valuematcher)

for key, value in kv_args.items():
for key, value in matchermap.items():
base_dict[key] = wrap_matcher(value)

return IsDictContainingEntries(base_dict)
14 changes: 13 additions & 1 deletion src/hamcrest/library/collection/issequence_containing.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Sequence, TypeVar, Union, cast
from typing import Sequence, TypeVar, Union, cast, overload

from hamcrest.core.base_matcher import BaseMatcher
from hamcrest.core.core.allof import all_of
Expand Down Expand Up @@ -54,6 +54,12 @@ def describe_to(self, description: Description) -> None:
self.matcher.describe_to(description)


@overload
def has_item(match: Matcher[T]) -> Matcher[Sequence[T]]: ...
@overload
def has_item(match: T) -> Matcher[Sequence[T]]: ...


def has_item(match: Union[Matcher[T], T]) -> Matcher[Sequence[T]]:
"""Matches if any element of sequence satisfies a given matcher.

Expand All @@ -72,6 +78,12 @@ def has_item(match: Union[Matcher[T], T]) -> Matcher[Sequence[T]]:
return IsSequenceContaining(wrap_matcher(match))


@overload
def has_items(*items: Matcher[T]) -> Matcher[Sequence[T]]: ...
@overload
def has_items(*items: T) -> Matcher[Sequence[T]]: ...


def has_items(*items: Union[Matcher[T], T]) -> Matcher[Sequence[T]]:
"""Matches if all of the given matchers are satisfied by any elements of
the sequence.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import MutableSequence, Optional, Sequence, TypeVar, Union, cast
from typing import MutableSequence, Optional, Sequence, TypeVar, Union, cast, overload

from hamcrest.core.base_matcher import BaseMatcher
from hamcrest.core.description import Description
Expand Down Expand Up @@ -79,6 +79,12 @@ def describe_to(self, description: Description) -> None:
).append_text(" in any order")


@overload
def contains_inanyorder(*items: Matcher[T]) -> Matcher[Sequence[T]]: ...
@overload
def contains_inanyorder(*items: T) -> Matcher[Sequence[T]]: ...


def contains_inanyorder(*items: Union[Matcher[T], T]) -> Matcher[Sequence[T]]:
"""Matches if sequences's elements, in any order, satisfy a given list of
matchers.
Expand Down
16 changes: 14 additions & 2 deletions src/hamcrest/library/collection/issequence_containinginorder.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import warnings
from typing import Optional, Sequence, TypeVar, Union
from typing import Optional, Sequence, Tuple, TypeVar, Union, cast, overload

from hamcrest.core.base_matcher import BaseMatcher
from hamcrest.core.description import Description
Expand Down Expand Up @@ -78,6 +78,12 @@ def describe_to(self, description: Description) -> None:
description.append_text("a sequence containing ").append_list("[", ", ", "]", self.matchers)


@overload
def contains_exactly(*items: Matcher[T]) -> Matcher[Sequence[T]]: ...
@overload
def contains_exactly(*items: T) -> Matcher[Sequence[T]]: ...


def contains_exactly(*items: Union[Matcher[T], T]) -> Matcher[Sequence[T]]:
"""Matches if sequence's elements satisfy a given list of matchers, in order.

Expand All @@ -97,7 +103,13 @@ def contains_exactly(*items: Union[Matcher[T], T]) -> Matcher[Sequence[T]]:
return IsSequenceContainingInOrder(matchers)


@overload
def contains(*items: Matcher[T]) -> Matcher[Sequence[T]]: ...
@overload
def contains(*items: T) -> Matcher[Sequence[T]]: ...


def contains(*items: Union[Matcher[T], T]) -> Matcher[Sequence[T]]:
"""Deprecated - use contains_exactly(*items)"""
warnings.warn("deprecated - use contains_exactly(*items)", DeprecationWarning)
return contains_exactly(*items)
return contains_exactly(*cast(Tuple[T, ...], items))
8 changes: 7 additions & 1 deletion src/hamcrest/library/collection/issequence_onlycontaining.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Sequence, TypeVar, Union
from typing import Sequence, TypeVar, Union, overload

from hamcrest.core.base_matcher import BaseMatcher
from hamcrest.core.core.anyof import any_of
Expand Down Expand Up @@ -35,6 +35,12 @@ def describe_to(self, description: Description) -> None:
)


@overload
def only_contains(*items: Matcher[T]) -> Matcher[Sequence[T]]: ...
@overload
def only_contains(*items: T) -> Matcher[Sequence[T]]: ...


def only_contains(*items: Union[Matcher[T], T]) -> Matcher[Sequence[T]]:
"""Matches if each element of sequence satisfies any of the given matchers.

Expand Down
51 changes: 26 additions & 25 deletions src/hamcrest/library/object/hasproperty.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,21 @@
from typing import Any, Mapping, TypeVar, Union, overload
from typing import Mapping, Union, overload

from hamcrest import described_as
from hamcrest.core import anything
from hamcrest.core.base_matcher import BaseMatcher
from hamcrest.core.core.allof import AllOf
from hamcrest.core.description import Description
from hamcrest.core.helpers.wrap_matcher import wrap_matcher as wrap_shortcut
from hamcrest.core.helpers.wrap_matcher import wrap_matcher
from hamcrest.core.matcher import Matcher
from hamcrest.core.string_description import StringDescription

__author__ = "Chris Rose"
__copyright__ = "Copyright 2011 hamcrest.org"
__license__ = "BSD, see License.txt"

V = TypeVar("V")


class IsObjectWithProperty(BaseMatcher[object]):
def __init__(self, property_name: str, value_matcher: Matcher[V]) -> None:
def __init__(self, property_name: str, value_matcher: Matcher[object]) -> None:
self.property_name = property_name
self.value_matcher = value_matcher

Expand Down Expand Up @@ -59,7 +57,7 @@ def __str__(self):
return str(d)


def has_property(name: str, match: Union[None, Matcher[V], V] = None) -> Matcher[object]:
def has_property(name: str, match: Union[None, Matcher[object], object] = None) -> Matcher[object]:
"""Matches if object has a property with a given name whose value satisfies
a given matcher.

Expand Down Expand Up @@ -89,25 +87,26 @@ def has_property(name: str, match: Union[None, Matcher[V], V] = None) -> Matcher
if match is None:
match = anything()

return IsObjectWithProperty(name, wrap_shortcut(match))


# Keyword argument form
@overload
def has_properties(**keys_valuematchers: Union[Matcher[V], V]) -> Matcher[Any]: ...
return IsObjectWithProperty(name, wrap_matcher(match))


# Name to matcher dict form
@overload
def has_properties(keys_valuematchers: Mapping[str, Union[Matcher[V], V]]) -> Matcher[Any]: ...
def has_properties(
__matchermap: Mapping[str, Union[Matcher[object], object]],
**matchermap: Union[Matcher[object], object],
) -> Matcher[object]: ...


# Alternating name/matcher form
@overload
def has_properties(*keys_valuematchers: Any) -> Matcher[Any]: ...
def has_properties(
*key_or_valuematchers: Union[str, Matcher[object], object],
**matchermap: Union[Matcher[object], object],
) -> Matcher[object]: ...


def has_properties(*keys_valuematchers, **kv_args):
def has_properties(*key_or_valuematcher_or_matchermaps, **matchermap):
"""Matches if an object has properties satisfying all of a dictionary
of string property names and corresponding value matchers.

Expand Down Expand Up @@ -153,26 +152,28 @@ def has_properties(*keys_valuematchers, **kv_args):
has_properties('foo', 1, 'bar', 2)

"""
if len(keys_valuematchers) == 1:
if len(key_or_valuematcher_or_matchermaps) == 1:
matchermap0 = key_or_valuematcher_or_matchermaps[0]
try:
base_dict = keys_valuematchers[0].copy()
base_dict = matchermap0.copy()
for key in base_dict:
base_dict[key] = wrap_shortcut(base_dict[key])
base_dict[key] = wrap_matcher(base_dict[key])
except AttributeError:
raise ValueError(
"single-argument calls to has_properties must pass a dict as the argument"
)
else:
if len(keys_valuematchers) % 2:
key_or_valuematchers = key_or_valuematcher_or_matchermaps
if len(key_or_valuematchers) % 2:
raise ValueError("has_properties requires key-value pairs")
base_dict = {}
for index in range(int(len(keys_valuematchers) / 2)):
base_dict[keys_valuematchers[2 * index]] = wrap_shortcut(
keys_valuematchers[2 * index + 1]
)
for index in range(int(len(key_or_valuematchers) / 2)):
key = key_or_valuematchers[2 * index]
valuematcher = key_or_valuematchers[2 * index + 1]
base_dict[key] = wrap_matcher(valuematcher)

for key, value in kv_args.items():
base_dict[key] = wrap_shortcut(value)
for key, value in matchermap.items():
base_dict[key] = wrap_matcher(value)

if len(base_dict) > 1:
description = StringDescription().append_text("an object with properties ")
Expand Down
14 changes: 14 additions & 0 deletions tests/type-hinting/core/core/test_allof.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
- case: all_of
# pypy + mypy doesn't work. See https://foss.heptapod.net/pypy/pypy/-/issues/3526
skip: platform.python_implementation() == "PyPy"
main: |
from hamcrest import is_, all_of

a: int = 99
b = a

reveal_type(all_of(a)) # N: Revealed type is "hamcrest.core.matcher.Matcher[builtins.int]"
reveal_type(all_of(is_(a))) # N: Revealed type is "hamcrest.core.matcher.Matcher[builtins.int]"
reveal_type(all_of(a, b)) # N: Revealed type is "hamcrest.core.matcher.Matcher[builtins.int]"
reveal_type(all_of(is_(a), is_(b))) # N: Revealed type is "hamcrest.core.matcher.Matcher[builtins.int]"
reveal_type(all_of(a, is_(b))) # N: Revealed type is "hamcrest.core.matcher.Matcher[builtins.object]"
11 changes: 11 additions & 0 deletions tests/type-hinting/core/core/test_anyof.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
- case: any_of
# pypy + mypy doesn't work. See https://foss.heptapod.net/pypy/pypy/-/issues/3526
skip: platform.python_implementation() == "PyPy"
main: |
from hamcrest import is_, any_of

reveal_type(any_of(98)) # N: Revealed type is "hamcrest.core.matcher.Matcher[builtins.int]"
reveal_type(any_of(is_(98))) # N: Revealed type is "hamcrest.core.matcher.Matcher[builtins.int]"
reveal_type(any_of(98, 99)) # N: Revealed type is "hamcrest.core.matcher.Matcher[builtins.int]"
reveal_type(any_of(is_(98), is_(99))) # N: Revealed type is "hamcrest.core.matcher.Matcher[builtins.int]"
reveal_type(any_of(98, is_(99))) # N: Revealed type is "hamcrest.core.matcher.Matcher[builtins.object]"
Loading
Loading