Skip to content

Commit e02ab3a

Browse files
committed
Type checker: List, Set, FrozenSet, Dict
1 parent 8dee40f commit e02ab3a

File tree

2 files changed

+64
-3
lines changed

2 files changed

+64
-3
lines changed

unpythonic/test/test_typecheck.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,27 @@ def test():
6363
assert not isoftype((), typing.Tuple[int, ...]) # empty tuple has no element type
6464
assert not isoftype((), typing.Tuple[float, ...])
6565

66+
# typing.List
67+
assert isoftype([1, 2, 3], typing.List[int])
68+
assert not isoftype([1, 2, 3], typing.List[float])
69+
assert not isoftype((1, 2, 3), typing.List[int]) # it's a tuple, silly
70+
assert not isoftype(42, typing.List[int]) # try something that's not even a collection
71+
72+
# typing.Set
73+
assert isoftype({"cat", "fox", "python"}, typing.Set[str])
74+
assert not isoftype({1, 2, 3}, typing.Set[str])
75+
assert not isoftype(42, typing.Set[str])
76+
77+
# typing.FrozenSet
78+
assert isoftype(frozenset({"cat", "fox", "python"}), typing.FrozenSet[str])
79+
assert not isoftype(frozenset({1, 2, 3}), typing.FrozenSet[str])
80+
assert not isoftype(42, typing.FrozenSet[str])
81+
82+
# typing.Dict
83+
assert isoftype({17: "cat", 23: "fox", 42: "python"}, typing.Dict[int, str])
84+
assert not isoftype({"bar": "foo", "tavern": "a place"}, typing.Dict[int, str])
85+
assert not isoftype(42, typing.Dict[int, str])
86+
6687
# type alias (at run time, this is just an assignment)
6788
U = typing.Union[int, str]
6889
assert isoftype(42, U)

unpythonic/typecheck.py

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@ def isoftype(value, T):
4141
- `NewType` (any instance of the underlying actual type will match)
4242
- `Union[T1, T2, ..., TN]`
4343
- `Tuple`, `Tuple[T, ...]`, `Tuple[T1, T2, ..., TN]`
44+
- `List[T]`
45+
- `FrozenSet[T]`
46+
- `Set[T]`
47+
- `Dict[K, V]`
4448
- `Callable` (argument and return value types currently NOT checked)
4549
- `Text`
4650
@@ -87,9 +91,9 @@ def isoftype(value, T):
8791
return True
8892
return any(isoftype(value, U) for U in T.__constraints__)
8993

90-
# TODO: List, Set, FrozenSet, Dict, NamedTuple
91-
#
92-
# TODO: Protocol, Type, Iterable, Iterator, Reversible, ...
94+
# TODO: NamedTuple
95+
# TODO: Deque
96+
# TODO: Protocol, Type, Iterable, Sequence, Iterator, Reversible, ...
9397
#
9498
# TODO: typing.Generic
9599
# TODO: Python 3.8 adds `typing.get_origin` and `typing.get_args`, which may be useful
@@ -122,6 +126,15 @@ def isoftype(value, T):
122126
if issubclass(T, typing.Text): # https://docs.python.org/3/library/typing.html#typing.Text
123127
return isinstance(value, str) # alias for str
124128

129+
if issubclass(T, typing.List):
130+
if not isinstance(value, list):
131+
return False
132+
if T.__args__ is None:
133+
raise TypeError("Missing mandatory element type argument of `typing.List`")
134+
assert len(T.__args__) == 1 # judging by the docs: https://docs.python.org/3/library/typing.html#typing.List
135+
U = T.__args__[0]
136+
return all(isoftype(elt, U) for elt in value)
137+
125138
if issubclass(T, typing.Tuple):
126139
if not isinstance(value, tuple):
127140
return False
@@ -141,6 +154,33 @@ def isoftype(value, T):
141154
return False
142155
return all(isoftype(elt, U) for elt, U in zip(value, T.__args__))
143156

157+
if issubclass(T, typing.Set):
158+
if not isinstance(value, set):
159+
return False
160+
if T.__args__ is None:
161+
raise TypeError("Missing mandatory element type argument of `typing.Set`")
162+
assert len(T.__args__) == 1
163+
U = T.__args__[0]
164+
return all(isoftype(elt, U) for elt in value)
165+
166+
if issubclass(T, typing.FrozenSet):
167+
if not isinstance(value, frozenset):
168+
return False
169+
if T.__args__ is None:
170+
raise TypeError("Missing mandatory element type argument of `typing.FrozenSet`")
171+
assert len(T.__args__) == 1
172+
U = T.__args__[0]
173+
return all(isoftype(elt, U) for elt in value)
174+
175+
if issubclass(T, typing.Dict):
176+
if not isinstance(value, dict):
177+
return False
178+
if T.__args__ is None:
179+
raise TypeError("Missing mandatory key, value type arguments of `typing.Dict`")
180+
assert len(T.__args__) == 2
181+
K, V = T.__args__
182+
return all(isoftype(k, K) and isoftype(v, V) for k, v in value.items())
183+
144184
if issubclass(T, typing.Callable):
145185
if not callable(value):
146186
return False

0 commit comments

Comments
 (0)