Skip to content

Commit 8ed3a80

Browse files
committed
typecheck: try to support Python 3.7+
1 parent 1164512 commit 8ed3a80

1 file changed

Lines changed: 61 additions & 21 deletions

File tree

unpythonic/typecheck.py

Lines changed: 61 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@
2424
import collections
2525
import typing
2626

27+
try:
28+
_MyGenericAlias = typing._GenericAlias
29+
except AttributeError: # Python 3.6 and earlier
30+
class _MyGenericAlias: # unused, but must be a class to support isinstance() check.
31+
pass
32+
2733
__all__ = ["isoftype"]
2834

2935
def isoftype(value, T):
@@ -92,14 +98,9 @@ def isoftype(value, T):
9298
if T is typing.Any:
9399
return True
94100

95-
def repr_matches(U, what):
96-
# Python 3.6 has e.g. "typing.TypeVar" as the repr,
97-
# but Python 3.7+ adds the "<class '...'>" around it.
98-
r = repr(U.__class__)
99-
return r == what or r == "<class '{}'>".format(what)
100-
101101
# AnyStr normalizes to TypeVar("AnyStr", str, bytes)
102-
if repr_matches(T, "typing.TypeVar"):
102+
# Python 3.6 has "typing.TypeVar" as the repr, but Python 3.7+ adds the "<class '...'>" around it.
103+
if repr(T.__class__) == "typing.TypeVar" or repr(T.__class__) == "<class 'typing.TypeVar'>":
103104
if not T.__constraints__: # just an abstract type name
104105
return True
105106
return any(isoftype(value, U) for U in T.__constraints__)
@@ -127,20 +128,58 @@ def repr_matches(U, what):
127128
# in `unpythonic.dispatch`. And see:
128129
# https://docs.python.org/3/library/typing.html#typing.get_type_hints
129130

130-
# TODO: Python 3.8 adds `typing.get_origin` and `typing.get_args`, which may be useful
131-
# TODO: to analyze generics once we bump our minimum Python to that.
131+
# TODO: Python 3.8 adds `typing.get_origin` and `typing.get_args`:
132132
# https://docs.python.org/3/library/typing.html#typing.get_origin
133-
# if repr(T).startswith("typing.Generic["):
134-
# pass
135-
136-
if repr_matches(T, "typing.Union"): # Optional normalizes to Union[argtype, NoneType].
133+
# TODO: We replicate them here so that we can use them in 3.7.
134+
# TODO: Delete the local copies once we start requiring Python 3.8.
135+
#
136+
# Used under the PSF license. Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010,
137+
# 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020 Python Software Foundation; All Rights Reserved
138+
# https://github.com/python/cpython/blob/3.8/LICENSE
139+
def get_origin(tp):
140+
"""Get the unsubscripted version of a type.
141+
This supports generic types, Callable, Tuple, Union, Literal, Final and ClassVar.
142+
Return None for unsupported types. Examples::
143+
get_origin(Literal[42]) is Literal
144+
get_origin(int) is None
145+
get_origin(ClassVar[int]) is ClassVar
146+
get_origin(Generic) is Generic
147+
get_origin(Generic[T]) is Generic
148+
get_origin(Union[T, int]) is Union
149+
get_origin(List[Tuple[T, T]][int]) == list
150+
"""
151+
if isinstance(tp, _MyGenericAlias):
152+
return tp.__origin__
153+
if tp is typing.Generic:
154+
return typing.Generic
155+
return None
156+
def get_args(tp):
157+
"""Get type arguments with all substitutions performed.
158+
For unions, basic simplifications used by Union constructor are performed.
159+
Examples::
160+
get_args(Dict[str, int]) == (str, int)
161+
get_args(int) == ()
162+
get_args(Union[int, Union[T, int], str][int]) == (int, str)
163+
get_args(Union[int, Tuple[T, int]][str]) == (int, Tuple[str, int])
164+
get_args(Callable[[], T][int]) == ([], int)
165+
"""
166+
if isinstance(tp, _MyGenericAlias) and not tp._special:
167+
res = tp.__args__
168+
if get_origin(tp) is collections.abc.Callable and res[0] is not Ellipsis:
169+
res = (list(res[:-1]), res[-1])
170+
return res
171+
return ()
172+
173+
# Optional normalizes to Union[argtype, NoneType].
174+
# Python 3.6 has the repr, 3.7+ use typing._GenericAlias.
175+
if repr(T.__class__) == "typing.Union" or get_origin(T) is typing.Union:
137176
if T.__args__ is None: # bare `typing.Union`; empty, has no types in it, so no value can match.
138177
return False
139178
if not any(isoftype(value, U) for U in T.__args__):
140179
return False
141180
return True
142181

143-
# TODO: in Python 3.7+, what is this mysterious callable that doesn't have `__qualname__`?
182+
# TODO: in Python 3.7+, what is the mysterious callable that doesn't have `__qualname__`?
144183
if callable(T) and hasattr(T, "__qualname__") and T.__qualname__ == "NewType.<locals>.new_type":
145184
# This is the best we can do, because the static types created by `typing.NewType`
146185
# have a constructor that discards the type information at runtime:
@@ -176,7 +215,8 @@ def repr_matches(U, what):
176215
if issubclass(T, typing.Text): # https://docs.python.org/3/library/typing.html#typing.Text
177216
return isinstance(value, str) # alias for str
178217

179-
if issubclass(T, typing.Tuple):
218+
# Subclass test for Python 3.6 only. Python 3.7+ have typing._GenericAlias for the generics.
219+
if issubclass(T, typing.Tuple) or get_origin(T) is tuple:
180220
if not isinstance(value, tuple):
181221
return False
182222
# bare `typing.Tuple`, no restrictions on length or element type.
@@ -209,12 +249,12 @@ def ismapping(statictype, runtimetype):
209249
for statictype, runtimetype in ((typing.Dict, dict),
210250
(typing.MutableMapping, collections.abc.MutableMapping),
211251
(typing.Mapping, collections.abc.Mapping)):
212-
if issubclass(T, statictype):
252+
if issubclass(T, statictype) or get_origin(T) is runtimetype:
213253
return ismapping(statictype, runtimetype)
214254

215255
# ItemsView is a special-case mapping in that we must not call
216256
# `.items()` on `value`.
217-
if issubclass(T, typing.ItemsView):
257+
if issubclass(T, typing.ItemsView) or get_origin(T) is collections.abc.ItemsView:
218258
if not isinstance(value, collections.abc.ItemsView):
219259
return False
220260
if T.__args__ is None:
@@ -234,10 +274,10 @@ def ismapping(statictype, runtimetype):
234274
def iscollection(statictype, runtimetype):
235275
if not isinstance(value, runtimetype):
236276
return False
237-
if issubclass(statictype, typing.ByteString):
277+
if issubclass(statictype, typing.ByteString) or get_origin(statictype) is collections.abc.ByteString:
238278
# WTF? A ByteString is a Sequence[int], but only statically.
239279
# At run time, the `__args__` are actually empty - it looks
240-
# like a bare Sequence, which is invalid. Hack the special case.
280+
# like a bare Sequence, which is invalid. HACK the special case.
241281
typeargs = (int,)
242282
else:
243283
typeargs = T.__args__
@@ -265,10 +305,10 @@ def iscollection(statictype, runtimetype):
265305
(typing.MutableSequence, collections.abc.MutableSequence),
266306
(typing.MappingView, collections.abc.MappingView),
267307
(typing.Sequence, collections.abc.Sequence)):
268-
if issubclass(T, statictype):
308+
if issubclass(T, statictype) or get_origin(T) is runtimetype:
269309
return iscollection(statictype, runtimetype)
270310

271-
if issubclass(T, typing.Callable):
311+
if issubclass(T, typing.Callable) or get_origin(T) is collections.abc.Callable:
272312
if not callable(value):
273313
return False
274314
return True

0 commit comments

Comments
 (0)