|
25 | 25 | import typing |
26 | 26 |
|
27 | 27 | from .arity import resolve_bindings, _getfunc |
| 28 | +from .typecheck import match_value_to_typespec |
28 | 29 |
|
29 | 30 | def generic(f): |
30 | 31 | """Decorator. Make `f` a generic function (in the sense of CLOS or Julia). |
@@ -80,97 +81,14 @@ def generic(f): |
80 | 81 | # Dispatcher - this will replace the original f. |
81 | 82 | @wraps(f) |
82 | 83 | def multidispatch(*args, **kwargs): |
83 | | - # It's silly that while Python supports type annotations, the stdlib doesn't have a function to |
84 | | - # perform a type annotation based runtime type check. Fuck it, we'll just have to implement one. |
85 | | - # |
86 | | - # Many `typing` meta-utilities explicitly reject using isinstance and issubclass on them, |
87 | | - # so we hack those by inspecting the repr. |
88 | | - # |
89 | | - # value: value whose type to check |
90 | | - # |
91 | | - # spec: match value against this. |
92 | | - # Either a concrete type, or one of the meta-utilities from the `typing` module. |
93 | | - # |
94 | | - # Only the most fundamental meta-utilities (Any, TypeVar, Union, Tuple, Callable) |
95 | | - # are currently supported. |
96 | | - # |
97 | | - # TODO: This is a solved problem, we should use https://github.com/agronholm/typeguard |
98 | | - def match_type(value, spec): |
99 | | - # TODO: Python 3.8 adds `typing.get_origin` and `typing.get_args`, which may be useful |
100 | | - # TODO: once we bump our minimum to that. |
101 | | - # TODO: Right now we're accessing internal fields to get what we need. |
102 | | - # https://docs.python.org/3/library/typing.html#typing.get_origin |
103 | | - if spec is typing.Any: |
104 | | - return True |
105 | | - if repr(spec.__class__) == "TypeVar": # AnyStr gets normalized to TypeVar("AnyStr", str, bytes) |
106 | | - if not spec.__constraints__: # just an abstract type name |
107 | | - return True |
108 | | - return any(match_type(value, typ) for typ in spec.__constraints__) |
109 | | - # TODO: typing.Generic |
110 | | - # if repr(spec).startswith("typing.Generic["): |
111 | | - # pass |
112 | | - # TODO: Protocol, Type, Iterable, Iterator, Reversible, ... |
113 | | - # TODO: List, Set, FrozenSet, Dict, NamedTuple |
114 | | - # TODO: many, many others; see https://docs.python.org/3/library/typing.html |
115 | | - if repr(spec.__class__) == "typing.Union": # Optional gets normalized to Union[argtype, NoneType]. |
116 | | - if spec.__args__ is None: # bare `typing.Union`; has no types in it, so no value can match. |
117 | | - return False |
118 | | - if not any(match_type(value, typ) for typ in spec.__args__): |
119 | | - return False |
120 | | - return True |
121 | | - try: |
122 | | - if issubclass(spec, typing.Text): |
123 | | - return isinstance(value, str) |
124 | | - if issubclass(spec, typing.Tuple): |
125 | | - if not isinstance(value, tuple): |
126 | | - return False |
127 | | - if spec.__args__ is None: # bare `typing.Tuple`, any tuple matches. |
128 | | - return True |
129 | | - # homogeneous type, arbitrary length |
130 | | - if len(spec.__args__ == 2 and spec.__args__[1] is Ellipsis): |
131 | | - typ = spec.__args__[0] |
132 | | - return all(match_type(elt, typ) for elt in value) |
133 | | - # heterogeneous types, exact length |
134 | | - if len(value) == len(spec.__args__): |
135 | | - return False |
136 | | - return all(match_type(elt, typ) for typ, elt in zip(spec.__args__, value)) |
137 | | - if issubclass(spec, typing.Callable): |
138 | | - if not callable(value): |
139 | | - return False |
140 | | - return True |
141 | | - # # TODO: Callable[[a0, a1, ...], ret], Callable[..., ret]. |
142 | | - # if spec.__args__ is None: # bare `typing.Callable`, no restrictions on arg/return types. |
143 | | - # return True |
144 | | - # sig = typing.get_type_hints(value) |
145 | | - # *argtypes, rettype = spec.__args__ |
146 | | - # if len(argtypes) == 1 and argtypes[0] is Ellipsis: |
147 | | - # pass # argument types not specified |
148 | | - # else: |
149 | | - # # TODO: we need the names of the positional arguments of the `value` callable here. |
150 | | - # for a in argtypes: |
151 | | - # # TODO: Can't use match_type here; we're comparing two specs against each other, |
152 | | - # # TODO: not a value against a spec. Need to implement a compatible_specs function. |
153 | | - # if not compatible_specs(???, a): |
154 | | - # return False |
155 | | - # if not compatible_specs(sig["return"], rettype): |
156 | | - # return False |
157 | | - # return True |
158 | | - except TypeError: # probably one of those meta-utilities that hates issubclass for no reason |
159 | | - pass |
160 | | - # TODO: typing.Literal (Python 3.8) |
161 | | - # catch any meta-utilities we don't currently support |
162 | | - if hasattr(spec, "__module__") and spec.__module__ == "typing": |
163 | | - fullname = "{}.{}".format(spec.__module__, spec.__qualname__) |
164 | | - raise NotImplementedError("This simple runtime typechecker doesn't support {}".format(fullname)) |
165 | | - return isinstance(value, spec) # a concrete type parameter |
166 | 84 | # signature comes from typing.get_type_hints. |
167 | 85 | def match_argument_types(signature): |
168 | 86 | # TODO: handle *args (bindings["vararg"], bindings["vararg_name"]) |
169 | 87 | # TODO: handle **kwargs (bindings["kwarg"], bindings["kwarg_name"]) |
170 | 88 | for parameter, value in bindings["args"].items(): |
171 | 89 | assert parameter in signature # resolve_bindings should already TypeError when not. |
172 | 90 | expected_type = signature[parameter] |
173 | | - if not match_type(value, expected_type): |
| 91 | + if not match_value_to_typespec(value, expected_type): |
174 | 92 | return False |
175 | 93 | return True |
176 | 94 |
|
|
0 commit comments