1- from .helpers import (
2- JSON2PY ,
3- PY2JSON ,
4- INSP_JSON ,
5- INSP_PY ,
6- PATTERN ,
7- SENTINEL ,
8- has_origin ,
9- identity ,
10- is_attrs_field_required ,
11- issub_safe ,
12- resolve_fwd_ref ,
13- )
1+ from .helpers import JSON2PY , PY2JSON , INSP_JSON , INSP_PY , PATTERN , has_origin , identity
142from .action_v1 import (
153 check_dict ,
164 check_isinst ,
208 convert_tuple_as_list ,
219)
2210from . import pattern as pat
11+ from .product import build_attribute_map , build_named_tuple_map , build_typed_dict_map
2312
2413from functools import partial
2514
15+ _SUPPORTED_VERBS = (JSON2PY , PY2JSON , INSP_PY , INSP_JSON , PATTERN )
16+
2617
2718def attrs_classes (
2819 verb ,
@@ -53,49 +44,26 @@ def attrs_classes(
5344 `__json_check__` may be used to completely override the `inspect_json` check generated
5445 for this class.
5546 """
56- if verb not in (JSON2PY , PY2JSON , INSP_PY , INSP_JSON , PATTERN ):
47+ if verb not in _SUPPORTED_VERBS :
48+ return
49+ inner_map = build_attribute_map (verb , typ , ctx , read_all = verb == PY2JSON )
50+ if inner_map is None :
5751 return
58- try :
59- fields = typ .__attrs_attrs__
60- except AttributeError :
61- try :
62- fields = typ .__dataclass_fields__
63- except AttributeError :
64- return
65- else :
66- fields = fields .values ()
6752
6853 if verb == INSP_PY :
6954 return partial (check_isinst , typ = typ )
7055
71- inner_map = []
72- for field in fields :
73- if field .init or verb == PY2JSON :
74- tup = (
75- field .name ,
76- ctx .lookup (
77- verb = verb , typ = resolve_fwd_ref (field .type , typ ), accept_missing = True
78- ),
79- )
80- if verb == PY2JSON :
81- tup += (field .default ,)
82- elif verb in (INSP_JSON , PATTERN ):
83- tup += (is_attrs_field_required (field ),)
84- inner_map .append (tup )
85-
8656 if verb == JSON2PY :
8757 pre_hook_method = getattr (typ , pre_hook , identity )
8858 return partial (
8959 convert_dict_to_attrs ,
9060 pre_hook = pre_hook_method ,
91- inner_map = tuple ( inner_map ) ,
61+ inner_map = inner_map ,
9262 con = typ ,
9363 )
9464 elif verb == PY2JSON :
9565 post_hook = post_hook if hasattr (typ , post_hook ) else None
96- return partial (
97- convert_attrs_to_dict , post_hook = post_hook , inner_map = tuple (inner_map )
98- )
66+ return partial (convert_attrs_to_dict , post_hook = post_hook , inner_map = inner_map )
9967 elif verb == INSP_JSON :
10068 check = getattr (typ , check , None )
10169 if check :
@@ -104,9 +72,26 @@ def attrs_classes(
10472 return partial (check_dict , inner_map = inner_map , pre_hook = pre_hook_method )
10573 elif verb == PATTERN :
10674 return pat .Object .exact (
107- (pat .String .exact (name ), inner or pat .Unkown )
108- for name , inner , req in inner_map
109- if req
75+ (pat .String .exact (attr .name ), attr .inner or pat .Unkown )
76+ for attr in inner_map
77+ if attr .is_required
78+ )
79+
80+
81+ def _simple_product (inner_map , verb , typ , ctx ):
82+ if verb == JSON2PY :
83+ return partial (
84+ convert_dict_to_attrs , pre_hook = identity , inner_map = inner_map , con = typ
85+ )
86+ elif verb == PY2JSON :
87+ return partial (convert_attrs_to_dict , post_hook = None , inner_map = inner_map )
88+ elif verb == INSP_JSON :
89+ return partial (check_dict , pre_hook = identity , inner_map = inner_map )
90+ elif verb == PATTERN :
91+ return pat .Object .exact (
92+ (pat .String .exact (attr .name ), attr .inner )
93+ for attr in inner_map
94+ if attr .is_required
11095 )
11196
11297
@@ -115,67 +100,53 @@ def named_tuples(verb, typ, ctx):
115100 Handle a ``NamedTuple(name, [('field', type), ('field', type)])`` type.
116101
117102 Also handles a ``collections.namedtuple`` if you have a fallback handler.
103+
104+ Warning: there's no clear runtime marker that something is a namedtuple; it's just
105+ a subclass of ``tuple`` that has some special fields.
118106 """
119- if verb not in (JSON2PY , PY2JSON , INSP_PY , INSP_JSON , PATTERN ) or not issub_safe (
120- typ , tuple
121- ):
107+ if verb not in _SUPPORTED_VERBS :
122108 return
123- try :
124- fields = typ ._field_types
125- except AttributeError :
126- try :
127- fields = typ ._fields
128- except AttributeError :
129- return
130- fields = [(name , None ) for name in fields ]
131- else :
132- fields = fields .items ()
109+
110+ inner_map = build_named_tuple_map (verb , typ , ctx )
111+ if inner_map is None :
112+ return
113+
133114 if verb == INSP_PY :
134115 return partial (check_isinst , typ = typ )
135116
136- defaults = {}
137- defaults .update (getattr (typ , "_fields_defaults" , ()))
138- defaults .update (getattr (typ , "_field_defaults" , ()))
139- inner_map = []
140- for name , inner in fields :
141- tup = (
142- name ,
143- ctx .lookup (verb = verb , typ = resolve_fwd_ref (inner , typ ), accept_missing = True ),
144- )
145- if verb == PY2JSON :
146- tup += (defaults .get (name , SENTINEL ),)
147- elif verb in (INSP_JSON , PATTERN ):
148- tup += (name not in defaults ,)
149- inner_map .append (tup )
117+ return _simple_product (inner_map , verb , typ , ctx )
150118
151- if verb == JSON2PY :
152- return partial (
153- convert_dict_to_attrs ,
154- pre_hook = identity ,
155- inner_map = tuple (inner_map ),
156- con = typ ,
157- )
158- elif verb == PY2JSON :
159- return partial (
160- convert_attrs_to_dict , post_hook = None , inner_map = tuple (inner_map )
161- )
162- elif verb == INSP_JSON :
163- return partial (check_dict , pre_hook = identity , inner_map = tuple (inner_map ))
164- elif verb == PATTERN :
165- return pat .Object .exact (
166- (pat .String .exact (name ), inner ) for name , inner , req in inner_map if req
167- )
119+
120+ def typed_dicts (verb , typ , ctx ):
121+ """
122+ Handle the TypedDict product type. This allows you to construct a dict with specific (string) keys, which
123+ is often how people really use dicts.
124+
125+ Both the class form and the functional form, ``TypedDict('Name', {'field': type, 'field': type})`` are
126+ supported.
127+ """
128+ if verb not in _SUPPORTED_VERBS :
129+ return
130+
131+ inner_map = build_typed_dict_map (verb , typ , ctx )
132+ if inner_map is None :
133+ return
134+
135+ if verb == INSP_PY :
136+ return partial (check_dict , inner_map = inner_map , pre_hook = identity )
137+
138+ # Note: we pass `dict` as the typ here because it's the correct constructor.
139+ return _simple_product (inner_map , verb , dict , ctx )
168140
169141
170142def tuples (verb , typ , ctx ):
171143 """
172144 Handle a ``Tuple[type, type, type]`` product type. Use a ``NamedTuple`` if you don't
173- want a list.
145+ want a list. Though, if possible, prefer ``attrs`` or ``dataclass``.
174146 """
175- if verb not in (JSON2PY , PY2JSON , INSP_PY , INSP_JSON , PATTERN ) or not has_origin (
176- typ , tuple
177- ):
147+ if verb not in _SUPPORTED_VERBS or not has_origin (typ , tuple ):
178148 return
149+
179150 args = typ .__args__
180151 if Ellipsis in args :
181152 # This is a homogeneous tuple, use the lists rule.
0 commit comments