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

1"""Special factory for iterencode function covering some use cases in the tallipoika JSONEncoder.""" 

2 

3from typing import no_type_check 

4 

5import tallipoika.py2es6 as py2es6 

6 

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' 

16 

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} 

21 

22 

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 

49 

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] 

102 

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 

142 

143 yield _encoder(key) 

144 yield _key_separator 

145 

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] 

171 

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] 

200 

201 return _iterencode