11# -*- coding: utf-8; -*-
2- """Simplistic run-time type checker.
2+ """Lightweight run-time type checker.
33
4- This implements just a minimal feature set needed for checking function
5- arguments in typical uses of multiple dispatch (see `unpythonic.dispatch`).
6- That said, this DOES support many (but not all) features of the `typing` stdlib
7- module.
4+ Originally built for the minimal feature set needed by multiple dispatch
5+ (see `unpythonic.dispatch`), but designed as a general-purpose utility.
6+ Supports many (but not all) features of the `typing` stdlib module.
87
98We currently provide `isoftype` (cf. `isinstance`), but no `issubtype` (cf. `issubclass`).
109
1514"""
1615
1716import collections
17+ import sys
1818import types
1919import typing
2020
@@ -40,14 +40,21 @@ def isoftype(value, T):
4040 - `TypeVar`
4141 - `NewType` (any instance of the underlying actual type will match)
4242 - `Union[T1, T2, ..., TN]`
43+ - `NoReturn`, `Never` (no value matches; `Never` requires Python 3.11+)
44+ - `Literal[v1, v2, ...]`
45+ - `Type[X]` (value must be a class that is `X` or a subclass of `X`)
46+ - `ClassVar[T]`, `Final[T]` (wrapper stripped, inner type checked)
4347 - `Tuple`, `Tuple[T, ...]`, `Tuple[T1, T2, ..., TN]`, `Sequence[T]`
4448 - `List[T]`, `MutableSequence[T]`
4549 - `FrozenSet[T]`, `AbstractSet[T]`
4650 - `Set[T]`, `MutableSet[T]`
47- - `Dict[K, V]`, `MutableMapping[K, V]`, `Mapping[K, V]`
51+ - `Dict[K, V]`, `DefaultDict[K, V]`, `OrderedDict[K, V]`
52+ - `Counter[T]` (element type checked; value type is always `int`)
53+ - `ChainMap[K, V]`
54+ - `MutableMapping[K, V]`, `Mapping[K, V]`
4855 - `ItemsView[K, V]`, `KeysView[K]`, `ValuesView[V]`
4956 - `Callable` (argument and return value types currently NOT checked)
50- - `Text`
57+ - `Text` (deprecated since Python 3.11; will be removed at floor Python 3.12)
5158
5259 Any checks on the type arguments of the meta-utilities are performed
5360 recursively using `isoftype`, in order to allow compound specifications.
@@ -66,15 +73,22 @@ def isoftype(value, T):
6673 # Python provides no official public API for run-time type introspection.
6774 #
6875 # Unsupported typing features:
69- # NamedTuple, DefaultDict, Counter, ChainMap, OrderedDict,
70- # IO, TextIO, BinaryIO, Pattern, Match, Generic, Type,
76+ # NamedTuple,
77+ # IO, TextIO, BinaryIO, Pattern, Match,
7178 # Awaitable, Coroutine, AsyncIterable, AsyncIterator,
7279 # ContextManager, AsyncContextManager, Generator, AsyncGenerator,
73- # NoReturn, ClassVar, Final, Protocol, TypedDict, Literal , ForwardRef
80+ # Generic, Protocol, TypedDict, ForwardRef
7481
7582 if T is typing .Any :
7683 return True
7784
85+ # NoReturn means a function never returns — no value has this type.
86+ # Never (3.11+) is the bottom type; semantically the same for our purposes.
87+ if T is typing .NoReturn :
88+ return False
89+ if sys .version_info >= (3 , 11 ) and T is typing .Never :
90+ return False
91+
7892 # AnyStr normalizes to TypeVar("AnyStr", str, bytes)
7993 if isinstance (T , typing .TypeVar ):
8094 if not T .__constraints__ : # just an abstract type name
@@ -102,6 +116,28 @@ def isNewType(T):
102116 # print(type(i)) # int
103117 return isinstance (value , T .__supertype__ )
104118
119+ # Literal[v1, v2, ...] — value must be one of the listed constants.
120+ if typing .get_origin (T ) is typing .Literal :
121+ return value in T .__args__
122+
123+ # Type[X] — value must be a class that is X or a subclass of X.
124+ if typing .get_origin (T ) is type :
125+ if not isinstance (value , type ):
126+ return False
127+ args = getattr (T , "__args__" , None )
128+ if args is None :
129+ return True # bare Type, any class matches
130+ return issubclass (value , args [0 ])
131+
132+ # ClassVar[T] and Final[T] — these are declaration wrappers. At runtime,
133+ # we just strip the wrapper and check the inner type.
134+ for wrapper_origin in (typing .ClassVar , typing .Final ):
135+ if typing .get_origin (T ) is wrapper_origin :
136+ args = getattr (T , "__args__" , None )
137+ if args is None :
138+ return True # bare ClassVar or Final, no inner type constraint
139+ return isoftype (value , args [0 ])
140+
105141 # Some one-trick ponies.
106142 for U in (typing .Iterator , # can't non-destructively check element type
107143 typing .Iterable , # can't non-destructively check element type
@@ -128,8 +164,10 @@ def isNewType(T):
128164
129165 # We don't have a match yet, so T might still be one of those meta-utilities
130166 # that hate `issubclass` with a passion.
131- if safeissubclass (T , typing .Text ): # https://docs.python.org/3/library/typing.html#typing.Text
132- return isinstance (value , str ) # alias for str
167+ # DEPRECATED: typing.Text is deprecated since Python 3.11 (it's just an alias for str).
168+ # TODO: Remove this branch when the floor bumps to Python 3.12.
169+ if safeissubclass (T , typing .Text ):
170+ return isinstance (value , str )
133171
134172 if typing .get_origin (T ) is tuple :
135173 if not isinstance (value , tuple ):
@@ -162,7 +200,25 @@ def ismapping(runtimetype):
162200 return False
163201 K , V = args
164202 return all (isoftype (k , K ) and isoftype (v , V ) for k , v in value .items ())
165- for runtimetype in (dict , collections .abc .MutableMapping , collections .abc .Mapping ):
203+ # Counter[T] is a mapping (keys: T, values: int), but has only one type arg.
204+ if typing .get_origin (T ) is collections .Counter :
205+ if not isinstance (value , collections .Counter ):
206+ return False
207+ args = getattr (T , "__args__" , None )
208+ if args is None :
209+ args = (typing .TypeVar ("T" ),)
210+ assert len (args ) == 1
211+ if not value :
212+ return False
213+ U = args [0 ]
214+ return all (isoftype (k , U ) and isinstance (v , int ) for k , v in value .items ())
215+
216+ for runtimetype in (dict ,
217+ collections .defaultdict ,
218+ collections .OrderedDict ,
219+ collections .ChainMap ,
220+ collections .abc .MutableMapping ,
221+ collections .abc .Mapping ):
166222 if typing .get_origin (T ) is runtimetype :
167223 return ismapping (runtimetype )
168224
@@ -190,8 +246,12 @@ def iscollection(statictype, runtimetype):
190246 if not isinstance (value , runtimetype ):
191247 return False
192248 if typing .get_origin (statictype ) is collections .abc .ByteString :
249+ # DEPRECATED: typing.ByteString is deprecated since Python 3.12.
250+ # TODO: Remove this branch and the ByteString entry in the loop below
251+ # when the floor bumps to Python 3.12.
252+ #
193253 # WTF? A ByteString is a Sequence[int], but only statically.
194- # At run time, the `__args__` are actually empty - it looks
254+ # At run time, the `__args__` are actually empty — it looks
195255 # like a bare Sequence, which is invalid. HACK the special case.
196256 typeargs = (int ,)
197257 else :
@@ -209,7 +269,7 @@ def iscollection(statictype, runtimetype):
209269 (typing .FrozenSet , frozenset ),
210270 (typing .Set , set ),
211271 (typing .Deque , collections .deque ),
212- (typing .ByteString , collections .abc .ByteString ), # must check before Sequence
272+ (typing .ByteString , collections .abc .ByteString ), # DEPRECATED; must check before Sequence
213273 (typing .MutableSet , collections .abc .MutableSet ), # must check mutable first
214274 # because a mutable value has *also* the interface of the immutable variant
215275 # (e.g. MutableSet is a subtype of AbstractSet)
0 commit comments