|
7 | 7 |
|
8 | 8 | # NOTE: This module must support Python 2.7 in addition to Python 3.x
|
9 | 9 |
|
| 10 | +import sys |
| 11 | +# _type_check is NOT a part of public typing API, it is used here only to mimic |
| 12 | +# the (convenient) behavior of types provided by typing module. |
| 13 | +from typing import _type_check # type: ignore |
10 | 14 |
|
11 |
| -def TypedDict(typename, fields): |
12 |
| - """TypedDict creates a dictionary type that expects all of its |
| 15 | + |
| 16 | +def _check_fails(cls, other): |
| 17 | + try: |
| 18 | + if sys._getframe(1).f_globals['__name__'] not in ['abc', 'functools']: |
| 19 | + # Typed dicts are only for static structural subtyping. |
| 20 | + raise TypeError('TypedDict does not support instance and class checks') |
| 21 | + except (AttributeError, ValueError): |
| 22 | + pass |
| 23 | + return False |
| 24 | + |
| 25 | +def _dict_new(cls, *args, **kwargs): |
| 26 | + return dict(*args, **kwargs) |
| 27 | + |
| 28 | +def _typeddict_new(cls, _typename, _fields=None, **kwargs): |
| 29 | + if _fields is None: |
| 30 | + _fields = kwargs |
| 31 | + elif kwargs: |
| 32 | + raise TypeError("TypedDict takes either a dict or keyword arguments," |
| 33 | + " but not both") |
| 34 | + return _TypedDictMeta(_typename, (), {'__annotations__': dict(_fields)}) |
| 35 | + |
| 36 | +class _TypedDictMeta(type): |
| 37 | + def __new__(cls, name, bases, ns): |
| 38 | + # Create new typed dict class object. |
| 39 | + # This method is called directly when TypedDict is subclassed, |
| 40 | + # or via _typeddict_new when TypedDict is instantiated. This way |
| 41 | + # TypedDict supports all three syntaxes described in its docstring. |
| 42 | + # Subclasses and instanes of TypedDict return actual dictionaries |
| 43 | + # via _dict_new. |
| 44 | + ns['__new__'] = _typeddict_new if name == 'TypedDict' else _dict_new |
| 45 | + tp_dict = super(_TypedDictMeta, cls).__new__(cls, name, (dict,), ns) |
| 46 | + try: |
| 47 | + # Setting correct module is necessary to make typed dict classes pickleable. |
| 48 | + tp_dict.__module__ = sys._getframe(2).f_globals.get('__name__', '__main__') |
| 49 | + except (AttributeError, ValueError): |
| 50 | + pass |
| 51 | + anns = ns.get('__annotations__', {}) |
| 52 | + msg = "TypedDict('Name', {f0: t0, f1: t1, ...}); each t must be a type" |
| 53 | + anns = {n: _type_check(tp, msg) for n, tp in anns.items()} |
| 54 | + for base in bases: |
| 55 | + anns.update(base.__dict__.get('__annotations__', {})) |
| 56 | + tp_dict.__annotations__ = anns |
| 57 | + return tp_dict |
| 58 | + |
| 59 | + __instancecheck__ = __subclasscheck__ = _check_fails |
| 60 | + |
| 61 | + |
| 62 | +TypedDict = _TypedDictMeta('TypedDict', (dict,), {}) |
| 63 | +TypedDict.__module__ = __name__ |
| 64 | +TypedDict.__doc__ = \ |
| 65 | + """A simple typed name space. At runtime it is equivalent to a plain dict. |
| 66 | +
|
| 67 | + TypedDict creates a dictionary type that expects all of its |
13 | 68 | instances to have a certain set of keys, with each key
|
14 | 69 | associated with a value of a consistent type. This expectation
|
15 | 70 | is not checked at runtime but is only enforced by typecheckers.
|
16 |
| - """ |
17 |
| - def new_dict(*args, **kwargs): |
18 |
| - return dict(*args, **kwargs) |
| 71 | + Usage:: |
| 72 | +
|
| 73 | + Point2D = TypedDict('Point2D', {'x': int, 'y': int, 'label': str}) |
| 74 | + a: Point2D = {'x': 1, 'y': 2, 'label': 'good'} # OK |
| 75 | + b: Point2D = {'z': 3, 'label': 'bad'} # Fails type check |
| 76 | + assert Point2D(x=1, y=2, label='first') == dict(x=1, y=2, label='first') |
| 77 | +
|
| 78 | + The type info could be accessed via Point2D.__annotations__. TypedDict |
| 79 | + supports two additional equivalent forms:: |
19 | 80 |
|
20 |
| - new_dict.__name__ = typename |
21 |
| - new_dict.__supertype__ = dict |
22 |
| - return new_dict |
| 81 | + Point2D = TypedDict('Point2D', x=int, y=int, label=str) |
| 82 | +
|
| 83 | + class Point2D(TypedDict): |
| 84 | + x: int |
| 85 | + y: int |
| 86 | + label: str |
| 87 | +
|
| 88 | + The latter syntax is only supported in Python 3.6+, while two other |
| 89 | + syntax forms work for Python 2.7 and 3.2+ |
| 90 | + """ |
0 commit comments