PyHamcrest
PyHamcrest copied to clipboard
Type hints for ´sized´ matchers do not allow for instances of subclasses of ´Sized´.
I didnt find this issue mentioned anywhere, so I'm sorry if i missed it, or if i'm doing something wrong.
When creating tests for collection objects I'm getting plenty of type errors, from mypy as well as the PyCharm builtin type checker.
Example: The following:
assert_that([], is_(empty()))
results in:
Unexpected type(s): (list, Matcher[Sized]) Possible types: (bool, str) (list, Matcher[list])
It seems to me to be because the type declaration of the empty
matcher is too strict/"working in the wrong direction".
This can be solved by using a TypeVar
instead of the static Matcher[Sized]
type declaration (see the ´empty2´ definition in the example below)
Code snippet for clarification:
from typing import TypeVar, Sized, List, Any
from hamcrest.core.assert_that import assert_that
from hamcrest.core.core.is_ import is_
from hamcrest.core.matcher import Matcher
from hamcrest.library.collection import empty
SizedT = TypeVar("SizedT", bound=Sized)
def empty2() -> Matcher[SizedT]:
"""
wraps the ´hamcrest.empty´ Matcher with type hints that support instances of subclasses of ´typing.Sized´.
"""
return empty() # type: ignore
some_list: List[Any] = [1]
""" Unexpected type(s): (list, Matcher[Sized]) Possible types: (bool, str) (list, Matcher[list]) """
assert_that(some_list, is_(empty()))
""" All good: """
assert_that(some_list, is_(empty2()))
The same issue exists for most of the other collection matchers as far as i can tell. (Sorry for accidentally hitting ´enter´ while writing my draft for the title, thus adding a issue without an explanation and a odd title at first).
Can you give an example? What do you mean by a sized matcher? Do you mean has_length()
?
Sorry i accidentally published the issue before finishing my description. Updated it.
has_length
as well as is_empty
are suffering the exact same problem, as they are typed in the same way
Ooooh, thanks. I'll take a look - or feel free to submit a PR.
I can try to submit a PR for that. I‘m not sure how to test for that nicely though. Any suggestions?
Hah - funnily enough I'm just looking into that now.
I'm playing with tests in #179, but weirdly, while I do see your problem in IntelliJ, mypy seems to be happy with assert_that([], is_(empty()))
. Hmmm.
Ah - I was missing the is_
- that does recreate the issue. I think there's an issue with is_()
itself - is_(empty())
fails all by itself!
Oh yea! I missed that initally. It really does seem to be a general problem with is_
, and specifically not Is
.
is_
just seems to expect a Matcher[<nothing>]
consistently, which does not throw any errors with the very generic matchers Matcher[Any]
(e.g. equal_to
, greater_than
, ...) since Any
includes nothing
. Trying a few others of the more specifc matchers, I do get the same error:
reveal_type(is_(greater_than(0))) # Revealed type is "hamcrest.core.matcher.Matcher[<nothing>]"
is_(greater_than(0)) # [All fine]
is_(contains_string('')) # Argument 1 to "is_" has incompatible type "Matcher[str]"; expected "Matcher[<nothing>]"
is_(instance_of(object)) # Argument 1 to "is_" has incompatible type "Matcher[object]"; expected "Matcher[<nothing>]"
Is(greater_than(0)) # [All fine]
Is(contains_string('')) # [All fine]
Is(instance_of(object)) # [All fine]
I dont really understand why but manually flattening the Union[Matcher[T], T]
seems to resolve this.
in src/hamcrest/core/core/is_.py
@overload
def is_(x: Union[Matcher[T], T]) -> Matcher[T]: ...
->
@overload
def is_(x: T) -> Matcher[T]: ...
@overload
def is_(x: Matcher[T]) -> Matcher[T]: ...
With that change, the correct type is revealed:
reveal_type(is_(greater_than(0))) # Revealed type is "hamcrest.core.matcher.Matcher[hamcrest.core.matcher.Matcher*[Any]]"
But, I dont understand why there would be an issue with Union[Matcher[T], T]
.
The PyCharm issue persists, but this might actually be a different problem.
But, I dont understand why there would be an issue with Union[Matcher[T], T].
Nor me - but if it works, it works.
The PyCharm issue persists, but this might actually be a different problem.
Yup - if mypy is happy, that's probably all we can do. (This makes #179 worthwhile, I think - we can easily test our annotations that way.) The PyCharm issue might even be a bug on their end.