diff --git a/docs/versionhistory.rst b/docs/versionhistory.rst index 4853af00..2fdecc4d 100644 --- a/docs/versionhistory.rst +++ b/docs/versionhistory.rst @@ -4,6 +4,13 @@ Version history This library adheres to `Semantic Versioning 2.0 `_. +**UNRELEASED** + +- Fixed the element type of ``Deque`` (``collections.deque``), ``MutableSequence`` and + ``MutableSet`` annotations not being checked, unlike the already supported ``List``, + ``Sequence``, ``Set`` and ``FrozenSet`` + (`#577 `_; PR by @sneha4175) + **4.6.0** (2026-07-26) - Added support for type checking against the PEP 661 ``sentinel`` type (built-in on diff --git a/src/typeguard/_checkers.py b/src/typeguard/_checkers.py index ba48103a..470288b3 100644 --- a/src/typeguard/_checkers.py +++ b/src/typeguard/_checkers.py @@ -7,7 +7,13 @@ import types import typing import warnings -from collections.abc import Mapping, MutableMapping, Sequence +from collections.abc import ( + Mapping, + MutableMapping, + MutableSequence, + MutableSet, + Sequence, +) from enum import Enum from inspect import Parameter, isclass from io import BufferedIOBase, IOBase, RawIOBase, TextIOBase @@ -330,7 +336,13 @@ def check_sequence( args: tuple[Any, ...], memo: TypeCheckMemo, ) -> None: - if not isinstance(value, collections.abc.Sequence): + if origin_type is collections.deque: + if not isinstance(value, collections.deque): + raise TypeCheckError("is not a deque") + elif origin_type is MutableSequence: + if not isinstance(value, MutableSequence): + raise TypeCheckError("is not a mutable sequence") + elif not isinstance(value, collections.abc.Sequence): raise TypeCheckError("is not a sequence") if args and args != (Any,): @@ -352,6 +364,9 @@ def check_set( if origin_type is frozenset: if not isinstance(value, frozenset): raise TypeCheckError("is not a frozenset") + elif origin_type is MutableSet: + if not isinstance(value, MutableSet): + raise TypeCheckError("is not a mutable set") elif not isinstance(value, AbstractSet): raise TypeCheckError("is not a set") @@ -1029,6 +1044,7 @@ def check_type_internal( Callable: check_callable, collections.abc.Callable: check_callable, complex: check_number, + collections.deque: check_sequence, dict: check_mapping, Dict: check_mapping, float: check_number, @@ -1039,9 +1055,13 @@ def check_type_internal( typing.Literal: check_literal, Mapping: check_mapping, MutableMapping: check_mapping, + MutableSequence: check_sequence, + MutableSet: check_set, None: check_none, collections.abc.Mapping: check_mapping, collections.abc.MutableMapping: check_mapping, + collections.abc.MutableSequence: check_sequence, + collections.abc.MutableSet: check_set, Sequence: check_sequence, collections.abc.Sequence: check_sequence, collections.abc.Set: check_set, diff --git a/tests/test_checkers.py b/tests/test_checkers.py index 30b101e3..08844d84 100644 --- a/tests/test_checkers.py +++ b/tests/test_checkers.py @@ -18,6 +18,7 @@ Collection, Concatenate, ContextManager, + Deque, Dict, ForwardRef, FrozenSet, @@ -27,6 +28,8 @@ Literal, Mapping, MutableMapping, + MutableSequence, + MutableSet, ParamSpec, Protocol, Sequence, @@ -803,6 +806,86 @@ def test_set_against_frozenset(self, sample_set: set): ) +class TestDeque: + def test_bad_type(self): + pytest.raises(TypeCheckError, check_type, 5, Deque[int]).match( + "int is not a deque" + ) + + def test_valid(self): + check_type(collections.deque([1, 2]), Deque[int]) + + def test_first_check_empty(self): + check_type(collections.deque(), Deque[int]) + + def test_first_check_fail(self): + pytest.raises( + TypeCheckError, check_type, collections.deque(["bb"]), Deque[int] + ).match("item 0 of collections.deque is not an instance of int") + + def test_full_check_fail(self): + pytest.raises( + TypeCheckError, + check_type, + collections.deque([1, 2, "bb"]), + Deque[int], + collection_check_strategy=CollectionCheckStrategy.ALL_ITEMS, + ).match("item 2 of collections.deque is not an instance of int") + + def test_list_against_deque(self): + pytest.raises(TypeCheckError, check_type, [1, 2], Deque[int]).match( + "list is not a deque" + ) + + +class TestMutableSequence: + def test_bad_type(self): + pytest.raises(TypeCheckError, check_type, 5, MutableSequence[int]).match( + "int is not a mutable sequence" + ) + + def test_valid(self): + check_type([1, 2], MutableSequence[int]) + + def test_full_check_fail(self): + pytest.raises( + TypeCheckError, + check_type, + [1, 2, "bb"], + MutableSequence[int], + collection_check_strategy=CollectionCheckStrategy.ALL_ITEMS, + ).match("item 2 of list is not an instance of int") + + def test_tuple_against_mutable_sequence(self): + pytest.raises(TypeCheckError, check_type, (1, 2), MutableSequence[int]).match( + "tuple is not a mutable sequence" + ) + + +class TestMutableSet: + def test_bad_type(self): + pytest.raises(TypeCheckError, check_type, 5, MutableSet[int]).match( + "int is not a mutable set" + ) + + def test_valid(self): + check_type({1, 2}, MutableSet[int]) + + def test_full_check_fail(self): + pytest.raises( + TypeCheckError, + check_type, + {1, 2, "bb"}, + MutableSet[int], + collection_check_strategy=CollectionCheckStrategy.ALL_ITEMS, + ).match("set is not an instance of int") + + def test_frozenset_against_mutable_set(self): + pytest.raises( + TypeCheckError, check_type, frozenset({1, 2}), MutableSet[int] + ).match("frozenset is not a mutable set") + + @pytest.mark.parametrize( "annotated_type", [