hamcrest / PyHamcrest

Hamcrest matchers for Python
http://hamcrest.org/
Other
766 stars 111 forks source link

Unexpected type warnings when using assert_that with sequence matchers #234

Open borislubimov opened 1 year ago

borislubimov commented 1 year ago

Currently, PyCharm highlights all usages of assert_that with sequence matchers due to unexpected type warnings. Is it possible to include an overloaded version of assert_that specifically designed for such cases? Would something like in the example below work?

# Python 3.10.0
# pyhamcrest 2.0.4
def test_assert():
    from hamcrest import assert_that, contains_exactly
    from typing import Sequence
    from typing import TypeVar
    from hamcrest.core.matcher import Matcher
    T = TypeVar("T")

    li = [1, 2, 3]
    ls = ['1', '2', '3']
    s = '123'

    # Native hamcrest asserts

    #  Unexpected type(s): (list[int], Matcher[Sequence]) Possible type(s): (bool, str) (list[int], Matcher[list[int]])
    assert_that(li, contains_exactly(*li))
    # Unexpected type(s): (list[str], Matcher[Sequence]) Possible type(s): (bool, str) (list[str], Matcher[list[str]])
    assert_that(ls, contains_exactly(*ls))
    assert_that(ls, contains_exactly(*s))
    # Unexpected type(s): (str, Matcher[Sequence]) Possible type(s): (bool, str) (str, Matcher[str])
    assert_that(s, contains_exactly(*s))
    assert_that(s, contains_exactly(*ls))

    # Overloaded assert_that

    def my_assert_that(actual_or_assertion: Sequence[T], matcher: Matcher[Sequence[T]], reason: str = "") -> None:
        assert_that(actual_or_assertion, matcher)

    # No Warnings
    my_assert_that(li, contains_exactly(*li))
    my_assert_that(ls, contains_exactly(*ls))
    my_assert_that(s, contains_exactly(*s))
    my_assert_that(ls, contains_exactly(*s))
    my_assert_that(s, contains_exactly(*ls))

How it looks in PyCharm:

assert_that
brunns commented 1 year ago

Is this the same issue as #222?

borislubimov commented 1 year ago

It's definitely related, at least changing type in either first or second argument of assert_that, fixes the issue. While changing types in both - returns the issue:

def test_assert_contravariant():
    from hamcrest import assert_that, contains_exactly
    from typing import TypeVar
    from hamcrest.core.matcher import Matcher

    li = [1, 2, 3]

    T = TypeVar("T")
    T_contravariant = TypeVar("T_contravariant", contravariant=True)

    # 1 case
    def my_assert_that_1(actual_or_assertion: T_contravariant, matcher: Matcher[T], reason: str = "") -> None:
        assert_that(actual_or_assertion, matcher, reason)

    my_assert_that_1(li, contains_exactly(*li))  # No Warnings

    # 2 case
    def my_assert_that_2(actual_or_assertion: T, matcher: Matcher[T_contravariant], reason: str = "") -> None:
        assert_that(actual_or_assertion, matcher, reason)

    my_assert_that_2(li, contains_exactly(*li))  # No Warnings

    # 3 case
    def my_assert_that_contr3(actual_or_assertion: T_contravariant, matcher: Matcher[T_contravariant],
                              reason: str = "") -> None:
        assert_that(actual_or_assertion, matcher, reason)

    # Expected type 'Matcher[list[int]]' (matched generic type 'Matcher[T_contravariant]'), got 'Matcher[Sequence]' instead
    my_assert_that_contr3(li, contains_exactly(*li))
brunns commented 1 year ago

As I said on #222, I wasn't able to get this working on the real codebase. I'd very much appreciate a PR if anyone else can!