diff --git a/docs/versionhistory.rst b/docs/versionhistory.rst index afedbaa..64e33d8 100644 --- a/docs/versionhistory.rst +++ b/docs/versionhistory.rst @@ -4,6 +4,11 @@ Version history This library adheres to `Semantic Versioning 2.0 `_. +**UNRELEASED** + +- Fixed basic support for intersection protocols + (`#490 `_; PR by @antonagestam) + **4.3.0** (2024-05-27) - Added support for checking against static protocols diff --git a/src/typeguard/_checkers.py b/src/typeguard/_checkers.py index 485bcb7..79b3af9 100644 --- a/src/typeguard/_checkers.py +++ b/src/typeguard/_checkers.py @@ -658,6 +658,8 @@ def check_protocol( ignored_attrs = set(dir(typing.Protocol)) | { "__annotations__", "__non_callable_proto_members__", + "__orig_bases__", + "__weakref__", } expected_methods: dict[str, tuple[Any, Any]] = {} expected_noncallable_members: dict[str, Any] = {} diff --git a/tests/test_checkers.py b/tests/test_checkers.py index f8b21d6..6620e33 100644 --- a/tests/test_checkers.py +++ b/tests/test_checkers.py @@ -16,14 +16,17 @@ Dict, ForwardRef, FrozenSet, + Iterable, Iterator, List, Literal, Mapping, MutableMapping, Optional, + Protocol, Sequence, Set, + Sized, TextIO, Tuple, Type, @@ -995,6 +998,59 @@ def test_text_real_file(self, tmp_path: Path): check_type(f, TextIO) +class TestIntersectingProtocol: + SIT = TypeVar("SIT", bound=object, covariant=True) + + class SizedIterable( + Sized, + Iterable[SIT], + Protocol[SIT], + ): ... + + @pytest.mark.parametrize( + ("subject", "predicate_type"), + ( + ((), SizedIterable), + (range(2), SizedIterable), + ((), SizedIterable[int]), + ((1, 2, 3), SizedIterable[int]), + (("1", "2", "3"), SizedIterable[str]), + ), + ) + def test_valid_member_passes(self, subject: object, predicate_type: type) -> None: + for _ in range(2): # Makes sure that the cache is also exercised + check_type(subject, predicate_type) + + xfail_nested_protocol_checks = pytest.mark.xfail( + reason="false negative due to missing support for nested protocol checks", + ) + + @pytest.mark.parametrize( + ("subject", "predicate_type"), + ( + ((1 for _ in ()), SizedIterable), + pytest.param( + range(2), + SizedIterable[str], + marks=xfail_nested_protocol_checks, + ), + pytest.param( + (1, 2, 3), + SizedIterable[str], + marks=xfail_nested_protocol_checks, + ), + pytest.param( + ("1", "2", "3"), + SizedIterable[int], + marks=xfail_nested_protocol_checks, + ), + ), + ) + def test_raises_for_non_member(self, subject: object, predicate_type: type) -> None: + with pytest.raises(TypeCheckError): + check_type(subject, predicate_type) + + @pytest.mark.parametrize( "instantiate, annotation", [