Skip to content

Commit

Permalink
Quickfix for intersection protocols
Browse files Browse the repository at this point in the history
This applies a quickfix solution for interesection protocols, opting to
fully ignore runtime checks of `__orig_bases__` and `__weakref__`
attributes. This has the effect of turning some false positives into
true negatives, but it also leaves some false negatives. To make that
clear, xfail test cases are added for the resulting false negatives.

A more robust fix would perhaps be to introduce recursive behavior
whenever a `__orig_bases__` attribute is found, making the type checker
check each inherited protocol individually. The resulting "top level"
intersection type would also need individual checking though, as it can
add required attributes on its own.
  • Loading branch information
antonagestam committed Sep 21, 2024
1 parent 2c035b3 commit 06aa3eb
Show file tree
Hide file tree
Showing 3 changed files with 63 additions and 0 deletions.
5 changes: 5 additions & 0 deletions docs/versionhistory.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ Version history
This library adheres to
`Semantic Versioning 2.0 <https://semver.org/#semantic-versioning-200>`_.

**UNRELEASED**

- Fixed basic support for intersection protocols
(`#490 <https://github.com/agronholm/typeguard/pull/490>`_; PR by @antonagestam)

**4.3.0** (2024-05-27)

- Added support for checking against static protocols
Expand Down
2 changes: 2 additions & 0 deletions src/typeguard/_checkers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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] = {}
Expand Down
56 changes: 56 additions & 0 deletions tests/test_checkers.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,17 @@
Dict,
ForwardRef,
FrozenSet,
Iterable,
Iterator,
List,
Literal,
Mapping,
MutableMapping,
Optional,
Protocol,
Sequence,
Set,
Sized,
TextIO,
Tuple,
Type,
Expand Down Expand Up @@ -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",
[
Expand Down

0 comments on commit 06aa3eb

Please sign in to comment.