Coverage for tallipoika/_factory.py: 67.41%
140 statements
« prev ^ index » next coverage.py v7.4.1, created at 2024-02-04 23:32:02 +00:00
« prev ^ index » next coverage.py v7.4.1, created at 2024-02-04 23:32:02 +00:00
1"""Special factory for iterencode function covering some use cases in the tallipoika JSONEncoder."""
3from typing import no_type_check
5import tallipoika.py2es6 as py2es6
7ENCODING_FOR_SORT = 'utf-16_be'
8SPACE = ' '
9OPEN_SB = '['
10CLOSE_SB = ']'
11EMPTY_ARRAY_REP = f'{OPEN_SB}{CLOSE_SB}'
12OPEN_CB = '{'
13CLOSE_CB = '}'
14EMPTY_OBJECT_REP = f'{OPEN_CB}{CLOSE_CB}'
15NL = '\n'
17JSON_FALSE_REP = 'false'
18JSON_NULL_REP = 'null'
19JSON_TRUE_REP = 'true'
20JSON_FNT_MAP = {None: JSON_NULL_REP, True: JSON_TRUE_REP, False: JSON_FALSE_REP}
23@no_type_check
24def make_iterencode(
25 markers,
26 _default,
27 _encoder,
28 _indent,
29 _floatstr,
30 _key_separator,
31 _item_separator,
32 _sort_keys,
33 _skip_keys,
34 _one_shot,
35 # HACK: hand-optimized bytecode; turn globals into locals
36 ValueError=ValueError,
37 dict=dict,
38 float=float,
39 id=id,
40 int=int,
41 isinstance=isinstance,
42 list=list,
43 str=str,
44 tuple=tuple,
45 _intstr=int.__str__,
46):
47 if _indent is not None and not isinstance(_indent, str): 47 ↛ 48line 47 didn't jump to line 48, because the condition on line 47 was never true
48 _indent = SPACE * _indent
50 @no_type_check
51 def _iterencode_list(seq, _current_indent_level: int):
52 if not seq:
53 yield EMPTY_ARRAY_REP
54 return
55 if markers is not None: 55 ↛ 60line 55 didn't jump to line 60, because the condition on line 55 was never false
56 marker_id = id(seq)
57 if marker_id in markers: 57 ↛ 58line 57 didn't jump to line 58, because the condition on line 57 was never true
58 raise ValueError('circular reference detected for sequence')
59 markers[marker_id] = seq
60 buf = OPEN_SB
61 if _indent is not None: 61 ↛ 62line 61 didn't jump to line 62, because the condition on line 61 was never true
62 _current_indent_level += 1
63 newline_indent = f'{NL}{_indent * _current_indent_level}'
64 separator = f'{_item_separator}{newline_indent}'
65 buf += newline_indent
66 else:
67 newline_indent = None
68 separator = _item_separator
69 is_first = True
70 for value in seq:
71 if is_first:
72 is_first = False
73 else:
74 buf = separator
75 if isinstance(value, str): 75 ↛ 76line 75 didn't jump to line 76, because the condition on line 75 was never true
76 yield buf + _encoder(value)
77 elif any(value is atom for atom in (False, None, True)):
78 yield buf + JSON_FNT_MAP[value]
79 elif isinstance(value, (float, int)):
80 # Subclasses of float and int may override __str__, but should still serialize as numbers in JSON.
81 # One example within the standard library is IntEnum.
82 yield buf + py2es6.serialize(value)
83 else:
84 yield buf
85 chunker = (
86 _iterencode_list
87 if isinstance(value, (list, tuple))
88 else (_iterencode_dict if isinstance(value, dict) else _iterencode)
89 )
90 yield from chunker(value, _current_indent_level)
91 if newline_indent is not None: 91 ↛ 92line 91 didn't jump to line 92, because the condition on line 91 was never true
92 _current_indent_level -= 1
93 yield f'{NL}{_indent * _current_indent_level}'
94 yield CLOSE_SB
95 if markers is not None: 95 ↛ exitline 95 didn't return from function '_iterencode_list', because the condition on line 95 was never false
96 try:
97 marker_id # noqa
98 except NameError:
99 pass
100 else:
101 del markers[marker_id]
103 @no_type_check
104 def _iterencode_dict(assoc, _current_indent_level):
105 if not assoc:
106 yield EMPTY_OBJECT_REP
107 return
108 if markers is not None: 108 ↛ 113line 108 didn't jump to line 113, because the condition on line 108 was never false
109 marker_id = id(assoc)
110 if marker_id in markers: 110 ↛ 111line 110 didn't jump to line 111, because the condition on line 110 was never true
111 raise ValueError('circular reference detected for dict')
112 markers[marker_id] = assoc
113 yield OPEN_CB
114 if _indent is not None: 114 ↛ 115line 114 didn't jump to line 115, because the condition on line 114 was never true
115 _current_indent_level += 1
116 newline_indent = f'{NL}{_indent * _current_indent_level}'
117 item_separator = f'{_item_separator}{newline_indent}'
118 yield newline_indent
119 else:
120 newline_indent = None
121 item_separator = _item_separator
122 is_first = True
123 items = sorted(assoc.items(), key=lambda kv: kv[0].encode(ENCODING_FOR_SORT)) if _sort_keys else assoc.items()
124 for key, value in items:
125 if isinstance(key, str): 125 ↛ 129line 125 didn't jump to line 129, because the condition on line 125 was never false
126 pass
127 # JavaScript is weakly typed for these, so it makes sense to also allow them.
128 # Many encoders seem to do something like this.
129 elif isinstance(key, (float, int)):
130 # see comment for float/int in _make_iterencode
131 key = py2es6.serialize(key)
132 elif any(value is atom for atom in (False, None, True)):
133 key = JSON_FNT_MAP[value]
134 elif _skip_keys:
135 continue
136 else:
137 raise TypeError(f'key {repr(key)} is not a string')
138 if is_first:
139 is_first = False
140 else:
141 yield item_separator
143 yield _encoder(key)
144 yield _key_separator
146 if isinstance(value, str):
147 yield _encoder(value)
148 elif any(value is atom for atom in (False, None, True)):
149 yield JSON_FNT_MAP[value]
150 elif isinstance(value, (float, int)):
151 # see comment for int/float in _make_iterencode
152 yield py2es6.serialize(value)
153 else:
154 chunker = (
155 _iterencode_list
156 if isinstance(value, (list, tuple))
157 else (_iterencode_dict if isinstance(value, dict) else _iterencode)
158 )
159 yield from chunker(value, _current_indent_level)
160 if newline_indent is not None: 160 ↛ 161line 160 didn't jump to line 161, because the condition on line 160 was never true
161 _current_indent_level -= 1
162 yield f'{NL}{_indent * _current_indent_level}'
163 yield CLOSE_CB
164 if markers is not None: 164 ↛ exitline 164 didn't return from function '_iterencode_dict', because the condition on line 164 was never false
165 try:
166 marker_id # noqa
167 except NameError:
168 pass
169 else:
170 del markers[marker_id]
172 @no_type_check
173 def _iterencode(obj, _current_indent_level):
174 if isinstance(obj, str): 174 ↛ 175line 174 didn't jump to line 175, because the condition on line 174 was never true
175 yield _encoder(obj)
176 elif any(obj is atom for atom in (False, None, True)): 176 ↛ 177line 176 didn't jump to line 177, because the condition on line 176 was never true
177 yield JSON_FNT_MAP[obj]
178 elif isinstance(obj, (float, int)): 178 ↛ 180line 178 didn't jump to line 180, because the condition on line 178 was never true
179 # see comment for float/int in _make_iterencode
180 yield py2es6.serialize(obj)
181 elif isinstance(obj, (list, tuple)):
182 yield from _iterencode_list(obj, _current_indent_level)
183 elif isinstance(obj, dict): 183 ↛ 186line 183 didn't jump to line 186, because the condition on line 183 was never false
184 yield from _iterencode_dict(obj, _current_indent_level)
185 else:
186 if markers is not None:
187 marker_id = id(obj)
188 if marker_id in markers:
189 raise ValueError('circular reference detected')
190 markers[marker_id] = obj
191 obj = _default(obj)
192 yield from _iterencode(obj, _current_indent_level)
193 if markers is not None:
194 try:
195 marker_id # noqa
196 except NameError:
197 pass
198 else:
199 del markers[marker_id]
201 return _iterencode