Skip to content

Commit 12a9392

Browse files
committed
Type checker: add mapping view types
1 parent 3c7af2d commit 12a9392

2 files changed

Lines changed: 58 additions & 21 deletions

File tree

unpythonic/test/test_typecheck.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ def test():
154154
assert isoftype((1, 2, 3), typing.Hashable)
155155
assert isoftype([1, 2, 3], typing.Sized)
156156
assert not isoftype([1, 2, 3], typing.Hashable)
157+
# TODO: test SupportsComplex, SupportsBytes
157158

158159
# For these it's impossible, in general, to non-destructively check the
159160
# element type, so this run-time type checker ignores making that check.
@@ -164,6 +165,21 @@ def test():
164165
assert isoftype([1, 2, 3], typing.Container)
165166
assert isoftype([1, 2, 3], typing.Collection) # Sized Iterable Container
166167

168+
# KeysView, ValuesView, MappingView, ItemsView
169+
d = {17: "cat", 23: "fox", 42: "python"}
170+
assert isoftype(d.keys(), typing.KeysView[int])
171+
assert isoftype(d.values(), typing.ValuesView[str])
172+
assert isoftype(d.items(), typing.ItemsView[int, str])
173+
174+
# TODO: test MappingView
175+
# The language docs don't exactly make it clear what MappingView is for.
176+
# All these documentation pages only talk about `.keys()`, `.values()`
177+
# and `.items()`, which correspond to the other three view types.
178+
# https://docs.python.org/3/library/typing.html#typing.MappingView
179+
# https://docs.python.org/3/library/collections.abc.html#collections.abc.MappingView
180+
# https://docs.python.org/3/glossary.html#term-dictionary-view
181+
# https://docs.python.org/3/library/stdtypes.html#dict-views
182+
167183
print("All tests PASSED")
168184

169185
if __name__ == '__main__':

unpythonic/typecheck.py

Lines changed: 42 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ def isoftype(value, T):
4949
- `FrozenSet[T]`, `AbstractSet[T]`
5050
- `Set[T]`, `MutableSet[T]`
5151
- `Dict[K, V]`, `MutableMapping[K, V]`, `Mapping[K, V]`
52+
- `ItemsView[K, V]`, `KeysView[K]`, `ValuesView[V]`
5253
- `Callable` (argument and return value types currently NOT checked)
5354
- `Text`
5455
@@ -101,20 +102,23 @@ def isoftype(value, T):
101102
# TODO: If you add a feature to the type checker, please update this list.
102103
#
103104
# Python 3.6+:
104-
# Generic, NamedTuple, DefaultDict, Counter, ChainMap, Type,
105-
# KeysView, ItemsView, ValuesView,
105+
# NamedTuple, DefaultDict, Counter, ChainMap,
106+
# IO, TextIO, BinaryIO,
107+
# Pattern, Match, (regular expressions)
108+
# Generic, Type,
106109
# Awaitable, Coroutine, AsyncIterable, AsyncIterator,
107110
# ContextManager, AsyncContextManager,
108111
# Generator, AsyncGenerator,
109-
# IO, TextIO, BinaryIO,
110-
# Pattern, Match, (regular expressions)
111112
# NoReturn (callable return value only),
112113
# ClassVar, Final
113114
#
114115
# Python 3.7+: OrderedDict
115116
# Python 3.8+: Protocol, SupportsIndex, TypedDict, Literal
116117
#
117118
# TODO: Do we need to support `typing.ForwardRef`?
119+
# No, if `get_type_hints` already resolves that. Consider our main use case,
120+
# in `unpythonic.dispatch`. And see:
121+
# https://docs.python.org/3/library/typing.html#typing.get_type_hints
118122

119123
# TODO: Python 3.8 adds `typing.get_origin` and `typing.get_args`, which may be useful
120124
# TODO: to analyze generics once we bump our minimum Python to that.
@@ -183,6 +187,36 @@ def isoftype(value, T):
183187
return False
184188
return all(isoftype(elt, U) for elt, U in zip(value, T.__args__))
185189

190+
# Check mapping types that allow non-destructive iteration.
191+
def ismapping(statictype, runtimetype):
192+
if not isinstance(value, runtimetype):
193+
return False
194+
if T.__args__ is None:
195+
raise TypeError("Missing mandatory key, value type arguments of `{}`".format(statictype))
196+
assert len(T.__args__) == 2
197+
if not value: # An empty dict has no key and value types.
198+
return False
199+
K, V = T.__args__
200+
return all(isoftype(k, K) and isoftype(v, V) for k, v in value.items())
201+
for statictype, runtimetype in ((typing.Dict, dict),
202+
(typing.MutableMapping, collections.abc.MutableMapping),
203+
(typing.Mapping, collections.abc.Mapping)):
204+
if issubclass(T, statictype):
205+
return ismapping(statictype, runtimetype)
206+
207+
# ItemsView is a special-case mapping in that we must not call
208+
# `.items()` on `value`.
209+
if issubclass(T, typing.ItemsView):
210+
if not isinstance(value, collections.abc.ItemsView):
211+
return False
212+
if T.__args__ is None:
213+
raise TypeError("Missing mandatory key, value type arguments of `{}`".format(statictype))
214+
assert len(T.__args__) == 2
215+
if not value: # An empty dict has no key and value types.
216+
return False
217+
K, V = T.__args__
218+
return all(isoftype(k, K) and isoftype(v, V) for k, v in value)
219+
186220
# Check iterable types that allow non-destructive iteration.
187221
#
188222
# Special-case strings; they match typing.Sequence, but they're not
@@ -216,29 +250,16 @@ def iscollection(statictype, runtimetype):
216250
(typing.MutableSet, collections.abc.MutableSet), # must check mutable first
217251
# because a mutable value has *also* the interface of the immutable variant
218252
# (e.g. MutableSet is a subtype of AbstractSet)
253+
(typing.KeysView, collections.abc.KeysView),
254+
(typing.ValuesView, collections.abc.ValuesView),
255+
(typing.MappingView, collections.abc.MappingView), # MappingView has one type argument so it goes here?
219256
(typing.AbstractSet, collections.abc.Set),
220257
(typing.MutableSequence, collections.abc.MutableSequence),
258+
(typing.MappingView, collections.abc.MappingView),
221259
(typing.Sequence, collections.abc.Sequence)):
222260
if issubclass(T, statictype):
223261
return iscollection(statictype, runtimetype)
224262

225-
# Check mapping types that allow non-destructive iteration.
226-
def ismapping(statictype, runtimetype):
227-
if not isinstance(value, runtimetype):
228-
return False
229-
if T.__args__ is None:
230-
raise TypeError("Missing mandatory key, value type arguments of `{}`".format(statictype))
231-
assert len(T.__args__) == 2
232-
if not value: # An empty dict has no key and value types.
233-
return False
234-
K, V = T.__args__
235-
return all(isoftype(k, K) and isoftype(v, V) for k, v in value.items())
236-
for statictype, runtimetype in ((typing.Dict, dict),
237-
(typing.MutableMapping, collections.abc.MutableMapping),
238-
(typing.Mapping, collections.abc.Mapping)):
239-
if issubclass(T, statictype):
240-
return ismapping(statictype, runtimetype)
241-
242263
if issubclass(T, typing.Callable):
243264
if not callable(value):
244265
return False

0 commit comments

Comments
 (0)