Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/json5/lib.py: 33%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

326 statements  

1# Copyright 2015 Google Inc. All rights reserved. 

2# 

3# Licensed under the Apache License, Version 2.0 (the "License"); 

4# you may not use this file except in compliance with the License. 

5# You may obtain a copy of the License at 

6# 

7# http://www.apache.org/licenses/LICENSE-2.0 

8# 

9# Unless required by applicable law or agreed to in writing, software 

10# distributed under the License is distributed on an "AS IS" BASIS, 

11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 

12# See the License for the specific language governing permissions and 

13# limitations under the License. 

14 

15import builtins 

16import enum 

17import math 

18import re 

19import unicodedata 

20from typing import ( 

21 IO, 

22 Any, 

23 Callable, 

24 Iterable, 

25 Mapping, 

26 Optional, 

27 Set, 

28 Tuple, 

29 Type, 

30 Union, 

31) 

32 

33from json5.parser import Parser 

34 

35# Used when encoding keys, below. 

36_reserved_word_re: Optional[re.Pattern] = None 

37 

38 

39def _get_mapping_types(): 

40 frozendict_type = getattr(builtins, 'frozendict', None) 

41 return (dict,) if frozendict_type is None else (dict, frozendict_type) 

42 

43 

44_mapping_types = _get_mapping_types() 

45 

46# A high surrogate (U+D800..U+DBFF) immediately followed by a low surrogate 

47# (U+DC00..U+DFFF). `\u` escapes decode one UTF-16 code unit each, so an 

48# astral character written as a surrogate pair arrives here as two separate 

49# code points that have to be recombined. 

50_surrogate_pair_re = re.compile('[\ud800-\udbff][\udc00-\udfff]') 

51 

52 

53def _join_surrogate_pairs(s: str) -> str: 

54 """Combine any UTF-16 surrogate pairs in a decoded string into the single 

55 code point they represent, matching the `json` module and ECMAScript 

56 string semantics. Lone surrogates are left untouched.""" 

57 if _surrogate_pair_re.search(s) is None: 

58 return s 

59 

60 def combine(m: re.Match) -> str: 

61 hi, lo = ord(m.group()[0]), ord(m.group()[1]) 

62 return chr(0x10000 + ((hi - 0xD800) << 10) + (lo - 0xDC00)) 

63 

64 return _surrogate_pair_re.sub(combine, s) 

65 

66 

67class QuoteStyle(enum.Enum): 

68 """Controls how strings will be quoted during encoding. 

69 

70 By default, for compatibility with the `json` module and older versions of 

71 `json5`, strings (not being used as keys and that are legal identifiers) 

72 will always be double-quoted, and any double quotes in the string will be 

73 escaped. This is `QuoteStyle.ALWAYS_DOUBLE`. If you pass 

74 `QuoteStyle.ALWAYS_SINGLE`, then strings will always be single-quoted, and 

75 any single quotes in the string will be escaped. If you pass 

76 `QuoteStyle.PREFER_DOUBLE`, then the behavior is the same as ALWAYS_DOUBLE 

77 and strings will be double-quoted *unless* the string contains more double 

78 quotes than single quotes, in which case the string will be single-quoted 

79 and single quotes will be escaped. If you pass `QuoteStyle.PREFER_SINGLE`, 

80 then the behavior is the same as ALWAYS_SINGLE and strings will be 

81 single-quoted *unless* the string contains more single quotes than double 

82 quotes, in which case the string will be double-quoted and any double 

83 quotes will be escaped. 

84 

85 *Note:* PREFER_DOUBLE and PREFER_SINGLE can impact performance, since in 

86 order to know which encoding to use you have to iterate over the entire 

87 string to count the number of single and double quotes. The codes guesses 

88 at an encoding while doing so, but if it guess wrong, the entire string has 

89 to be re-encoded, which will slow things down. If you are very concerned 

90 about performance (a) you probably shouldn't be using this library in the 

91 first place, because it just isn't very fast, and (b) you should use 

92 ALWAYS_DOUBLE or ALWAYS_SINGLE, which won't have this issue. 

93 """ 

94 

95 ALWAYS_DOUBLE = 'always_double' 

96 ALWAYS_SINGLE = 'always_single' 

97 PREFER_DOUBLE = 'prefer_double' 

98 PREFER_SINGLE = 'prefer_single' 

99 

100 

101def load( 

102 fp: Union[IO[str], IO[bytes]], 

103 *, 

104 encoding: Optional[str] = None, 

105 cls: Any = None, 

106 object_hook: Optional[Callable[[Mapping[str, Any]], Any]] = None, 

107 parse_float: Optional[Callable[[str], Any]] = None, 

108 parse_int: Optional[Callable[[str], Any]] = None, 

109 parse_constant: Optional[Callable[[str], Any]] = None, 

110 strict: bool = True, 

111 object_pairs_hook: Optional[ 

112 Callable[[Iterable[Tuple[str, Any]]], Any] 

113 ] = None, 

114 allow_duplicate_keys: bool = True, 

115 consume_trailing: bool = True, 

116 start: Optional[int] = None, 

117) -> Any: 

118 """Deserialize ``fp`` (a ``.read()``-supporting file-like object 

119 containing a JSON document) to a Python object. 

120 

121 Supports almost the same arguments as ``json.load()`` except that: 

122 - the `cls` keyword is ignored. 

123 - an extra `allow_duplicate_keys` parameter supports checking for 

124 duplicate keys in a object; by default, this is True for 

125 compatibility with ``json.load()``, but if set to False and 

126 the object contains duplicate keys, a ValueError will be raised. 

127 - an extra `consume_trailing` parameter specifies whether to 

128 consume any trailing characters after a valid object has been 

129 parsed. By default, this value is True and the only legal 

130 trailing characters are whitespace. If this value is set to False, 

131 parsing will stop when a valid object has been parsed and any 

132 trailing characters in the string will be ignored. 

133 - an extra `start` parameter specifies the zero-based offset into the 

134 file to start parsing at. If `start` is None, parsing will 

135 start at the current position in the file, and line number 

136 and column values will be reported as if starting from the 

137 beginning of the file; If `start` is not None, 

138 `load` will seek to zero and then read (and discard) the 

139 appropriate number of characters before beginning parsing; 

140 the file must be seekable for this to work correctly. 

141 - the `parse_int` parameter does not have the same semantics as 

142 in the `json` module: a custom callback is passed the integer 

143 string as written, and must be able to handle both decimal and 

144 hexadecimal literals (e.g. `0x1F`); only the default parser 

145 decodes hexadecimal literals automatically. 

146 - the `parse_constant` parameter does not have the same semantics 

147 as in the `json` module either: a custom callback is passed the 

148 constant's spelling as written, so in addition to `NaN`, 

149 `Infinity` and `-Infinity` it must also accept the `-NaN` form 

150 of negative NaN, which is legal JSON5. 

151 

152 `load()` reads the entire remaining stream into memory, even when 

153 `consume_trailing=False`. That option controls whether the parser 

154 validates trailing characters; it does not leave the stream positioned 

155 after the parsed value. Repeated calls cannot read successive values 

156 from a stream this way. 

157 

158 To read multiple values, read the input into a string once and call 

159 `parse(..., consume_trailing=False)` repeatedly, passing each returned 

160 position as the next `start`. See the example in `parse()`. These are 

161 decoded-character positions, not byte offsets or file seek positions. 

162 

163 Raises 

164 - `ValueError` if given an invalid document. This is different 

165 from the `json` module, which raises `json.JSONDecodeError`. 

166 - `UnicodeDecodeError` if given a byte string that is not a 

167 legal UTF-8 document (or the equivalent, if using a different 

168 `encoding`). This matches the `json` module. 

169 """ 

170 

171 if start is not None: 

172 fp.seek(0) 

173 s = fp.read() 

174 val, err, _ = parse( 

175 s, 

176 encoding=encoding, 

177 cls=cls, 

178 object_hook=object_hook, 

179 parse_float=parse_float, 

180 parse_int=parse_int, 

181 parse_constant=parse_constant, 

182 strict=strict, 

183 object_pairs_hook=object_pairs_hook, 

184 allow_duplicate_keys=allow_duplicate_keys, 

185 consume_trailing=consume_trailing, 

186 start=start, 

187 ) 

188 if err: 

189 raise ValueError(err) 

190 return val 

191 

192 

193def loads( 

194 s: Union[str, bytes], 

195 *, 

196 encoding: Optional[str] = None, 

197 cls: Any = None, 

198 object_hook: Optional[Callable[[Mapping[str, Any]], Any]] = None, 

199 parse_float: Optional[Callable[[str], Any]] = None, 

200 parse_int: Optional[Callable[[str], Any]] = None, 

201 parse_constant: Optional[Callable[[str], Any]] = None, 

202 strict: bool = True, 

203 object_pairs_hook: Optional[ 

204 Callable[[Iterable[Tuple[str, Any]]], Any] 

205 ] = None, 

206 allow_duplicate_keys: bool = True, 

207 consume_trailing: bool = True, 

208 start: Optional[int] = None, 

209) -> Any: 

210 """Deserialize ``s`` (a string containing a JSON5 document) to a Python 

211 object. 

212 

213 Supports the same arguments as ``json.loads()`` except that: 

214 - the `cls` keyword is ignored. 

215 - an extra `allow_duplicate_keys` parameter supports checking for 

216 duplicate keys in a object; by default, this is True for 

217 compatibility with ``json.load()``, but if set to False and 

218 the object contains duplicate keys, a ValueError will be raised. 

219 - an extra `consume_trailing` parameter specifies whether to 

220 consume any trailing characters after a valid object has been 

221 parsed. By default, this value is True and the only legal 

222 trailing characters are whitespace. If this value is set to False, 

223 parsing will stop when a valid object has been parsed and any 

224 trailing characters in the string will be ignored. 

225 - an extra `start` parameter specifies the zero-based offset into the 

226 string to start parsing at. 

227 - the `parse_int` parameter does not have the same semantics as 

228 in the `json` module: a custom callback is passed the integer 

229 string as written, and must be able to handle both decimal and 

230 hexadecimal literals (e.g. `0x1F`); only the default parser 

231 decodes hexadecimal literals automatically. 

232 - the `parse_constant` parameter does not have the same semantics 

233 as in the `json` module either: a custom callback is passed the 

234 constant's spelling as written, so in addition to `NaN`, 

235 `Infinity` and `-Infinity` it must also accept the `-NaN` form 

236 of negative NaN, which is legal JSON5. 

237 

238 Raises 

239 - `ValueError` if given an invalid document. This is different 

240 from the `json` module, which raises `json.JSONDecodeError`. 

241 - `UnicodeDecodeError` if given a byte string that is not a 

242 legal UTF-8 document (or the equivalent, if using a different 

243 `encoding`). This matches the `json` module. 

244 """ 

245 

246 val, err, _ = parse( 

247 s=s, 

248 encoding=encoding, 

249 cls=cls, 

250 object_hook=object_hook, 

251 parse_float=parse_float, 

252 parse_int=parse_int, 

253 parse_constant=parse_constant, 

254 strict=strict, 

255 object_pairs_hook=object_pairs_hook, 

256 allow_duplicate_keys=allow_duplicate_keys, 

257 consume_trailing=consume_trailing, 

258 start=start, 

259 ) 

260 if err: 

261 raise ValueError(err) 

262 return val 

263 

264 

265def parse( 

266 s: Union[str, bytes], 

267 *, 

268 encoding: Optional[str] = None, 

269 cls: Any = None, 

270 object_hook: Optional[Callable[[Mapping[str, Any]], Any]] = None, 

271 parse_float: Optional[Callable[[str], Any]] = None, 

272 parse_int: Optional[Callable[[str], Any]] = None, 

273 parse_constant: Optional[Callable[[str], Any]] = None, 

274 strict: bool = True, 

275 object_pairs_hook: Optional[ 

276 Callable[[Iterable[Tuple[str, Any]]], Any] 

277 ] = None, 

278 allow_duplicate_keys: bool = True, 

279 consume_trailing: bool = True, 

280 start: Optional[int] = None, 

281) -> Union[Tuple[Any, None, int], Tuple[None, str, int]]: 

282 """Parse ```s``, returning positional information along with a value. 

283 

284 This works exactly like `loads()`, except that (a) it returns the 

285 position in the string where the parsing stopped (either due to 

286 hitting an error or parsing a valid value) and any error as a string, 

287 (b) it takes an optional `consume_trailing` parameter that says whether 

288 to keep parsing the string after a valid value has been parsed; if True 

289 (the default), any trailing characters must be whitespace. If False, 

290 parsing stops when a valid value has been reached, (c) it takes an 

291 optional `start` parameter that specifies a zero-based offset to start 

292 parsing from in the string, (d) the `parse_int` parameter does not have 

293 the same semantics as in the `json` module (see `loads()` for details), 

294 (e) the `parse_constant` parameter also differs from the `json` module 

295 (see `loads()` for details), and (f) the return value is different, 

296 as described below. 

297 

298 `parse()` is useful if you have a string that might contain multiple 

299 values and you need to extract all of them; you can do so by repeatedly 

300 calling `parse`, setting `start` to the value returned in `position` 

301 from the previous call. 

302 

303 Returns a tuple of (value, error_string, position). If the string 

304 was a legal value, `value` will be the deserialized value, 

305 `error_string` will be `None`, and `position` will be one 

306 past the zero-based offset where the parser stopped reading. 

307 If the string was not a legal value, 

308 `value` will be `None`, `error_string` will be the string value 

309 of the exception that would've been raised, and `position` will 

310 be the zero-based farthest offset into the string where the parser 

311 hit an error. 

312 

313 Raises: 

314 - `UnicodeDecodeError` if given a byte string that is not a 

315 legal UTF-8 document (or the equivalent, if using a different 

316 `encoding`). This matches the `json` module. 

317 

318 Note that this does *not* raise a `ValueError`; instead any error is 

319 returned as the second value in the tuple. 

320 

321 You can use this method to read in a series of values from a string 

322 `s` as follows: 

323 

324 >>> import json5 

325 >>> s = '0 false null 4' 

326 >>> values = [] 

327 >>> start = 0 

328 >>> while True: 

329 ... v, err, pos = json5.parse(s, start=start, consume_trailing=False) 

330 ... if err is None: 

331 ... values.append(v) 

332 ... start = pos 

333 ... if start == len(s) or s[start:].isspace(): 

334 ... # Reached the end of the string (ignoring trailing 

335 ... # whitespace 

336 ... break 

337 ... continue 

338 ... raise ValueError(err) 

339 >>> values 

340 [0, False, None, 4] 

341 

342 """ 

343 assert cls is None, 'Custom decoders are not supported' 

344 

345 if isinstance(s, bytes): 

346 encoding = encoding or 'utf-8' 

347 s = s.decode(encoding) 

348 

349 if not s: 

350 return None, 'Empty strings are not legal JSON5', 0 

351 start = start or 0 

352 parser = Parser(s, '<string>', pos=start) 

353 ast, err, pos = parser.parse( 

354 global_vars={'_strict': strict, '_consume_trailing': consume_trailing} 

355 ) 

356 if err: 

357 return None, err, pos 

358 

359 try: 

360 value = _convert( 

361 ast, 

362 object_hook=object_hook, 

363 parse_float=parse_float, 

364 parse_int=parse_int, 

365 parse_constant=parse_constant, 

366 object_pairs_hook=object_pairs_hook, 

367 allow_duplicate_keys=allow_duplicate_keys, 

368 ) 

369 return value, None, pos 

370 except ValueError as e: 

371 return None, str(e), pos 

372 

373 

374def _convert( 

375 ast, 

376 object_hook, 

377 parse_float, 

378 parse_int, 

379 parse_constant, 

380 object_pairs_hook, 

381 allow_duplicate_keys, 

382): 

383 def _fp_constant_parser(s): 

384 return float(s.replace('Infinity', 'inf').replace('NaN', 'nan')) 

385 

386 def _parse_int(s): 

387 if s.lstrip('+-').startswith(('0x', '0X')): 

388 return int(s, 16) 

389 return int(s) 

390 

391 def _dictify(pairs): 

392 if not allow_duplicate_keys: 

393 keys = set() 

394 for key, _ in pairs: 

395 if key in keys: 

396 raise ValueError(f'Duplicate key "{key}" found in object') 

397 keys.add(key) 

398 

399 if object_pairs_hook is not None: 

400 return object_pairs_hook(pairs) 

401 if object_hook is not None: 

402 return object_hook(dict(pairs)) 

403 return dict(pairs) 

404 

405 parse_float = float if parse_float is None else parse_float 

406 parse_int = _parse_int if parse_int is None else parse_int 

407 parse_constant = ( 

408 _fp_constant_parser if parse_constant is None else parse_constant 

409 ) 

410 

411 return _walk_ast(ast, _dictify, parse_float, parse_int, parse_constant) 

412 

413 

414def _walk_ast( 

415 el, 

416 dictify: Callable[[Iterable[Tuple[str, Any]]], Any], 

417 parse_float, 

418 parse_int, 

419 parse_constant, 

420): 

421 if el == 'None': 

422 return None 

423 if el == 'True': 

424 return True 

425 if el == 'False': 

426 return False 

427 ty, v = el 

428 if ty == 'number': 

429 unsigned = v[1:] if v[:1] in ('-', '+') else v 

430 if unsigned.startswith(('0x', '0X')): 

431 return parse_int(v) 

432 if '.' in v or 'e' in v or 'E' in v: 

433 return parse_float(v) 

434 if 'Infinity' in v or 'NaN' in v: 

435 return parse_constant(v) 

436 return parse_int(v) 

437 if ty == 'string': 

438 return _join_surrogate_pairs(v) 

439 if ty == 'object': 

440 pairs = [] 

441 for key, val_expr in v: 

442 val = _walk_ast( 

443 val_expr, dictify, parse_float, parse_int, parse_constant 

444 ) 

445 pairs.append((_join_surrogate_pairs(key), val)) 

446 return dictify(pairs) 

447 if ty == 'array': 

448 return [ 

449 _walk_ast(el, dictify, parse_float, parse_int, parse_constant) 

450 for el in v 

451 ] 

452 raise ValueError('unknown el: ' + el) # pragma: no cover 

453 

454 

455def dump( 

456 obj: Any, 

457 fp: IO[str], 

458 *, 

459 skipkeys: bool = False, 

460 ensure_ascii: bool = True, 

461 check_circular: bool = True, 

462 allow_nan: bool = True, 

463 cls: Optional[Type['JSON5Encoder']] = None, 

464 indent: Optional[Union[int, str]] = None, 

465 separators: Optional[Tuple[str, str]] = None, 

466 default: Optional[Callable[[Any], Any]] = None, 

467 sort_keys: bool = False, 

468 quote_keys: bool = False, 

469 trailing_commas: bool = True, 

470 allow_duplicate_keys: bool = True, 

471 quote_style: QuoteStyle = QuoteStyle.ALWAYS_DOUBLE, 

472 use_duck_typing: bool = False, 

473 **kw, 

474): 

475 """Serialize ``obj`` to a JSON5-formatted stream to ``fp``, 

476 a ``.write()``-supporting file-like object. 

477 

478 Supports the same arguments as ``dumps()``, below. 

479 

480 Calling ``dump(obj, fp, quote_keys=True, trailing_commas=False, \ 

481 allow_duplicate_keys=True)`` 

482 should produce exactly the same output as ``json.dump(obj, fp).`` 

483 """ 

484 

485 fp.write( 

486 dumps( 

487 obj=obj, 

488 skipkeys=skipkeys, 

489 ensure_ascii=ensure_ascii, 

490 check_circular=check_circular, 

491 allow_nan=allow_nan, 

492 cls=cls, 

493 indent=indent, 

494 separators=separators, 

495 default=default, 

496 sort_keys=sort_keys, 

497 quote_keys=quote_keys, 

498 trailing_commas=trailing_commas, 

499 allow_duplicate_keys=allow_duplicate_keys, 

500 quote_style=quote_style, 

501 use_duck_typing=use_duck_typing, 

502 **kw, 

503 ) 

504 ) 

505 

506 

507def dumps( 

508 obj: Any, 

509 *, 

510 skipkeys: bool = False, 

511 ensure_ascii: bool = True, 

512 check_circular: bool = True, 

513 allow_nan: bool = True, 

514 cls: Optional[Type['JSON5Encoder']] = None, 

515 indent: Optional[Union[int, str]] = None, 

516 separators: Optional[Tuple[str, str]] = None, 

517 default: Optional[Callable[[Any], Any]] = None, 

518 sort_keys: bool = False, 

519 quote_keys: bool = False, 

520 trailing_commas: bool = True, 

521 allow_duplicate_keys: bool = True, 

522 quote_style: QuoteStyle = QuoteStyle.ALWAYS_DOUBLE, 

523 use_duck_typing: bool = False, 

524 **kw: Any, 

525): 

526 """Serialize ``obj`` to a JSON5-formatted string. 

527 

528 Supports the same arguments as ``json.dumps()``, except that: 

529 

530 - The ``encoding`` keyword is ignored; Unicode strings are always written. 

531 - By default, object keys that are legal identifiers are not quoted; if you 

532 pass ``quote_keys=True``, they will be. 

533 - By default, if lists and objects span multiple lines of output (i.e., 

534 when ``indent`` >=0), the last item will have a trailing comma after it. 

535 If you pass ``trailing_commas=False``, it will not. 

536 - If you use a number, a boolean, or ``None`` as a key value in a dict, it 

537 will be converted to the corresponding JSON string value, e.g. "1", 

538 "true", or "null". By default, ``dump()`` will match the `json` modules 

539 behavior and produce malformed JSON if you mix keys of different types 

540 that have the same converted value; e.g., ``{1: "foo", "1": "bar"}`` 

541 produces '{"1": "foo", "1": "bar"}', an object with duplicated keys. If 

542 you pass ``allow_duplicate_keys=False``, an exception will be raised 

543 instead. 

544 - If `quote_keys` is true, then keys of objects will be enclosed in quotes, 

545 as in regular JSON. Otheriwse, keys will not be enclosed in quotes unless 

546 they contain whitespace. 

547 - If `trailing_commas` is false, then commas will not be inserted after the 

548 final elements of objects and arrays, as in regular JSON. Otherwise, 

549 such commas will be inserted. 

550 - If `allow_duplicate_keys` is false, then only the last entry with a given 

551 key will be written. Otherwise, all entries with the same key will be 

552 written. 

553 - `quote_style` controls how strings are encoded. See the documentation 

554 for the `QuoteStyle` class, above, for how this is used. 

555 

556 - If `use_duck_typing` is true, objects with `keys` and `__getitem__` 

557 methods are encoded as objects, and objects with `__getitem__` and 

558 `__iter__` methods are encoded as arrays. Otherwise, only `dict` 

559 instances are encoded as objects and `list` or `tuple` instances are 

560 encoded as arrays, matching the standard `json` module. On Python 3.15 

561 and later, the built-in `frozendict` is also encoded as an object. 

562 

563 *Note*: Strings that are being used as unquoted keys are not affected 

564 by this parameter and remain unquoted. 

565 

566 *`quote_style` was added in version 0.10.0*. 

567 

568 Other keyword arguments are allowed and will be passed to the 

569 encoder so custom encoders can get them, but otherwise they will 

570 be ignored in an attempt to provide some amount of forward-compatibility. 

571 

572 *Note:* the standard JSON module explicitly calls `int.__repr(obj)__` 

573 and `float.__repr(obj)__` to encode ints and floats, thereby bypassing 

574 any custom representations you might have for objects that are subclasses 

575 of ints and floats, and, for compatibility, JSON5 does the same thing. 

576 To override this behavior, create a subclass of JSON5Encoder 

577 that overrides `encode()` and handles your custom representation. 

578 

579 For example: 

580 

581 ``` 

582 >>> import json5 

583 >>> from typing import Any, Set 

584 >>> 

585 >>> class Hex(int): 

586 ... def __repr__(self): 

587 ... return hex(self) 

588 >>> 

589 >>> class CustomEncoder(json5.JSON5Encoder): 

590 ... def encode( 

591 ... self, obj: Any, seen: Set, level: int, *, as_key: bool 

592 ... ) -> str: 

593 ... if isinstance(obj, Hex): 

594 ... return repr(obj) 

595 ... return super().encode(obj, seen, level, as_key=as_key) 

596 ... 

597 >>> json5.dumps([20, Hex(20)], cls=CustomEncoder) 

598 '[20, 0x14]' 

599 

600 ``` 

601 

602 *Note:* calling ``dumps(obj, quote_keys=True, trailing_commas=False, \ 

603 allow_duplicate_keys=True)`` 

604 should produce exactly the same output as ``json.dumps(obj).`` 

605 """ 

606 

607 cls = cls or JSON5Encoder 

608 enc = cls( 

609 skipkeys=skipkeys, 

610 ensure_ascii=ensure_ascii, 

611 check_circular=check_circular, 

612 allow_nan=allow_nan, 

613 indent=indent, 

614 separators=separators, 

615 default=default, 

616 sort_keys=sort_keys, 

617 quote_keys=quote_keys, 

618 trailing_commas=trailing_commas, 

619 allow_duplicate_keys=allow_duplicate_keys, 

620 quote_style=quote_style, 

621 use_duck_typing=use_duck_typing, 

622 **kw, 

623 ) 

624 return enc.encode(obj, seen=set(), level=0, as_key=False) 

625 

626 

627class JSON5Encoder: 

628 def __init__( 

629 self, 

630 *, 

631 skipkeys: bool = False, 

632 ensure_ascii: bool = True, 

633 check_circular: bool = True, 

634 allow_nan: bool = True, 

635 indent: Optional[Union[int, str]] = None, 

636 separators: Optional[Tuple[str, str]] = None, 

637 default: Optional[Callable[[Any], Any]] = None, 

638 sort_keys: bool = False, 

639 quote_keys: bool = False, 

640 trailing_commas: bool = True, 

641 allow_duplicate_keys: bool = True, 

642 quote_style: QuoteStyle = QuoteStyle.ALWAYS_DOUBLE, 

643 use_duck_typing: bool = False, 

644 **kw, 

645 ): 

646 """Provides a class that may be overridden to customize the behavior 

647 of `dumps()`. The keyword args are the same as for that function. 

648 *Added in version 0.10.0""" 

649 # Ignore unrecognized keyword arguments in the hope of providing 

650 # some level of backwards- and forwards-compatibility. 

651 del kw 

652 

653 self.skipkeys = skipkeys 

654 self.ensure_ascii = ensure_ascii 

655 self.check_circular = check_circular 

656 self.allow_nan = allow_nan 

657 self.indent = indent 

658 self.separators = separators 

659 if separators is None: 

660 separators = (', ', ': ') if indent is None else (',', ': ') 

661 self.item_separator, self.kv_separator = separators 

662 self.default_fn = _raise_type_error if default is None else default 

663 self.sort_keys = sort_keys 

664 self.quote_keys = quote_keys 

665 self.trailing_commas = trailing_commas 

666 self.allow_duplicate_keys = allow_duplicate_keys 

667 

668 if not isinstance(quote_style, QuoteStyle): 

669 raise TypeError( 

670 'quote_style argument must be a json5.QuoteStyle value' 

671 ) 

672 self.quote_style = quote_style 

673 self.use_duck_typing = use_duck_typing 

674 

675 def default(self, obj: Any) -> Any: 

676 """Provides a last-ditch option to encode a value that the encoder 

677 doesn't otherwise recognize, by converting `obj` to a value that 

678 *can* (and will) be serialized by the other methods in the class. 

679 

680 Note: this must not return a serialized value (i.e., string) 

681 directly, as that'll result in a doubly-encoded value.""" 

682 return self.default_fn(obj) 

683 

684 def encode( 

685 self, 

686 obj: Any, 

687 seen: Set, 

688 level: int, 

689 *, 

690 as_key: bool, 

691 ) -> str: 

692 """Returns an JSON5-encoded version of an arbitrary object. This can 

693 be used to provide customized serialization of objects. Overridden 

694 methods of this class should handle their custom objects and then 

695 fall back to super.encode() if they've been passed a normal object. 

696 

697 `seen` is used for duplicate object tracking when `check_circular` 

698 is True. 

699 

700 `level` represents the current indentation level, which increases 

701 by one for each recursive invocation of encode (i.e., whenever 

702 we're encoding the values of a dict or a list). 

703 

704 May raise `TypeError` if the object is the wrong type to be 

705 encoded (i.e., your custom routine can't handle it either), and 

706 `ValueError` if there's something wrong with the value, e.g. 

707 a float value of NaN when `allow_nan` is false. 

708 

709 If `as_key` is true, the return value should be a double-quoted string 

710 representation of the object, unless obj is a string that can be an 

711 identifier (and quote_keys is false and obj isn't a reserved word). 

712 If the object should not be used as a key, `TypeError` should be 

713 raised; that allows the base implementation to implement `skipkeys` 

714 properly. 

715 """ 

716 seen = seen or set() 

717 s = self._encode_basic_type(obj, as_key=as_key) 

718 if s is not None: 

719 return s 

720 

721 if as_key: 

722 raise TypeError(f'Invalid key f{obj}') 

723 return self._encode_non_basic_type(obj, seen, level) 

724 

725 def _encode_basic_type(self, obj: Any, *, as_key: bool) -> Optional[str]: 

726 """Returns None if the object is not a basic type.""" 

727 

728 if isinstance(obj, str): 

729 return self._encode_str(obj, as_key=as_key) 

730 

731 # Check for True/False before ints because True and False are 

732 # also considered ints and so would be represented as 1 and 0 

733 # if we did ints first. 

734 if obj is True: 

735 return '"true"' if as_key else 'true' 

736 if obj is False: 

737 return '"false"' if as_key else 'false' 

738 if obj is None: 

739 return '"null"' if as_key else 'null' 

740 

741 if isinstance(obj, int): 

742 return self._encode_int(obj, as_key=as_key) 

743 

744 if isinstance(obj, float): 

745 return self._encode_float(obj, as_key=as_key) 

746 

747 return None 

748 

749 def _encode_int(self, obj: int, *, as_key: bool) -> str: 

750 s = int.__repr__(obj) 

751 return f'"{s}"' if as_key else s 

752 

753 def _encode_float(self, obj: float, *, as_key: bool) -> str: 

754 if obj == float('inf'): 

755 allowed = self.allow_nan 

756 s = 'Infinity' 

757 elif obj == float('-inf'): 

758 allowed = self.allow_nan 

759 s = '-Infinity' 

760 elif math.isnan(obj): 

761 allowed = self.allow_nan 

762 s = 'NaN' 

763 else: 

764 allowed = True 

765 s = float.__repr__(obj) 

766 

767 if not allowed: 

768 raise ValueError(f'Illegal JSON5 value: {obj}') 

769 return f'"{s}"' if as_key else s 

770 

771 def _encode_str(self, obj: str, *, as_key: bool) -> str: 

772 if ( 

773 as_key 

774 and self.is_identifier(obj) 

775 and not self.quote_keys 

776 and not self.is_reserved_word(obj) 

777 and not (self.ensure_ascii and any(ord(ch) > 0xFFFF for ch in obj)) 

778 ): 

779 if self.ensure_ascii: 

780 return ''.join( 

781 self._escape_ch(ch) if ord(ch) > 127 else ch for ch in obj 

782 ) 

783 return obj 

784 

785 return self._encode_quoted_str(obj, self.quote_style) 

786 

787 def _encode_quoted_str(self, obj: str, quote_style: QuoteStyle) -> str: 

788 """Returns a quoted string with a minimal number of escaped quotes.""" 

789 ret = [] 

790 double_quotes_seen = 0 

791 single_quotes_seen = 0 

792 sq = "'" 

793 dq = '"' 

794 for ch in obj: 

795 if ch == dq: 

796 # At first we will guess at which quotes to escape. If 

797 # we guess wrong, we reencode the string below. 

798 double_quotes_seen += 1 

799 if quote_style in ( 

800 QuoteStyle.ALWAYS_DOUBLE, 

801 QuoteStyle.PREFER_DOUBLE, 

802 ): 

803 encoded_ch = self._escape_ch(dq) 

804 else: 

805 encoded_ch = dq 

806 elif ch == sq: 

807 single_quotes_seen += 1 

808 if quote_style in ( 

809 QuoteStyle.ALWAYS_SINGLE, 

810 QuoteStyle.PREFER_SINGLE, 

811 ): 

812 encoded_ch = self._escape_ch(sq) 

813 else: 

814 encoded_ch = sq 

815 elif ch == '\\': 

816 encoded_ch = self._escape_ch(ch) 

817 else: 

818 o = ord(ch) 

819 if o < 32: 

820 encoded_ch = self._escape_ch(ch) 

821 elif o < 127 or ( 

822 not self.ensure_ascii and ch not in ('\u2028', '\u2029') 

823 ): 

824 encoded_ch = ch 

825 else: 

826 encoded_ch = self._escape_ch(ch) 

827 ret.append(encoded_ch) 

828 

829 # We may have guessed wrong and need to reencode the string. 

830 if ( 

831 double_quotes_seen > single_quotes_seen 

832 and quote_style == QuoteStyle.PREFER_DOUBLE 

833 ): 

834 return self._encode_quoted_str(obj, QuoteStyle.ALWAYS_SINGLE) 

835 if ( 

836 single_quotes_seen > double_quotes_seen 

837 and quote_style == QuoteStyle.PREFER_SINGLE 

838 ): 

839 return self._encode_quoted_str(obj, QuoteStyle.ALWAYS_DOUBLE) 

840 

841 if quote_style in (QuoteStyle.ALWAYS_DOUBLE, QuoteStyle.PREFER_DOUBLE): 

842 return '"' + ''.join(ret) + '"' 

843 return "'" + ''.join(ret) + "'" 

844 

845 def _escape_ch(self, ch: str) -> str: 

846 """Returns the backslash-escaped representation of the char.""" 

847 if ch == '\\': 

848 return '\\\\' 

849 if ch == "'": 

850 return r'\'' 

851 if ch == '"': 

852 return r'\"' 

853 if ch == '\n': 

854 return r'\n' 

855 if ch == '\r': 

856 return r'\r' 

857 if ch == '\t': 

858 return r'\t' 

859 if ch == '\b': 

860 return r'\b' 

861 if ch == '\f': 

862 return r'\f' 

863 

864 o = ord(ch) 

865 if o < 65536: 

866 return rf'\u{o:04x}' 

867 

868 val = o - 0x10000 

869 high = 0xD800 + (val >> 10) 

870 low = 0xDC00 + (val & 0x3FF) 

871 return rf'\u{high:04x}\u{low:04x}' 

872 

873 def _encode_non_basic_type(self, obj, seen: Set, level: int) -> str: 

874 # Basic types can't be recursive so we only check for circularity 

875 # on non-basic types. If for some reason the caller was using a 

876 # subclass of a basic type and wanted to check circularity on it, 

877 # it'd have to do so directly in a subclass of JSON5Encoder. 

878 if self.check_circular: 

879 i = id(obj) 

880 if i in seen: 

881 raise ValueError('Circular reference detected.') 

882 seen.add(i) 

883 

884 is_mapping = isinstance(obj, _mapping_types) or ( 

885 self.use_duck_typing 

886 and hasattr(obj, 'keys') 

887 and hasattr(obj, '__getitem__') 

888 ) 

889 is_array = isinstance(obj, (list, tuple)) or ( 

890 self.use_duck_typing 

891 and hasattr(obj, '__getitem__') 

892 and hasattr(obj, '__iter__') 

893 ) 

894 if is_mapping: 

895 s = self._encode_dict(obj, seen, level + 1) 

896 elif is_array: 

897 s = self._encode_array(obj, seen, level + 1) 

898 else: 

899 s = self.encode(self.default(obj), seen, level, as_key=False) 

900 assert s is not None 

901 

902 if self.check_circular: 

903 seen.remove(i) 

904 return s 

905 

906 def _encode_dict(self, obj: Any, seen: set, level: int) -> str: 

907 indent_str, end_str = self._spacers(level) 

908 item_sep = self.item_separator + indent_str 

909 kv_sep = self.kv_separator 

910 

911 if self.sort_keys: 

912 keys = sorted(obj.keys()) 

913 else: 

914 keys = obj.keys() 

915 

916 s = '{' + indent_str 

917 

918 first_key = True 

919 new_keys = set() 

920 for key in keys: 

921 try: 

922 key_str = self.encode(key, seen, level, as_key=True) 

923 except TypeError: 

924 if self.skipkeys: 

925 continue 

926 raise 

927 

928 if not self.allow_duplicate_keys: 

929 # Compare the keys as they will be decoded, not as encoded: 

930 # e.g. 1 and '1' are written as "1" and '1' with single 

931 # quotes, and float('inf') and 'Infinity' are written as 

932 # "Infinity" and Infinity, but each pair reads back as the 

933 # same key. Non-string keys are always encoded as a plain 

934 # double-quoted string. 

935 key_name = key if isinstance(key, str) else key_str[1:-1] 

936 if key_name in new_keys: 

937 raise ValueError(f'duplicate key {key!r}') 

938 new_keys.add(key_name) 

939 

940 if first_key: 

941 first_key = False 

942 else: 

943 s += item_sep 

944 

945 val_str = self.encode(obj[key], seen, level, as_key=False) 

946 s += key_str + kv_sep + val_str 

947 

948 if first_key: 

949 return '{}' 

950 s += end_str + '}' 

951 return s 

952 

953 def _encode_array(self, obj: Any, seen: Set, level: int) -> str: 

954 items = [self.encode(el, seen, level, as_key=False) for el in obj] 

955 if not items: 

956 return '[]' 

957 

958 indent_str, end_str = self._spacers(level) 

959 item_sep = self.item_separator + indent_str 

960 return '[' + indent_str + item_sep.join(items) + end_str + ']' 

961 

962 def _spacers(self, level: int) -> Tuple[str, str]: 

963 if self.indent is not None: 

964 end_str = '' 

965 if self.trailing_commas: 

966 end_str = ',' 

967 if isinstance(self.indent, int): 

968 if self.indent > 0: 

969 indent_str = '\n' + ' ' * self.indent * level 

970 end_str += '\n' + ' ' * self.indent * (level - 1) 

971 else: 

972 indent_str = '\n' 

973 end_str += '\n' 

974 else: 

975 indent_str = '\n' + self.indent * level 

976 end_str += '\n' + self.indent * (level - 1) 

977 else: 

978 indent_str = '' 

979 end_str = '' 

980 return indent_str, end_str 

981 

982 def is_identifier(self, key: str) -> bool: 

983 """Returns whether the string could be used as a legal 

984 EcmaScript/JavaScript identifier. 

985 

986 There should normally be no reason to override this, unless 

987 the definition of identifiers change in later versions of the 

988 JSON5 spec and this implementation hasn't been updated to handle 

989 the changes yet.""" 

990 if ( 

991 not key 

992 or not self._is_id_start(key[0]) 

993 and key[0] not in ('$', '_') 

994 ): 

995 return False 

996 for ch in key[1:]: 

997 if not self._is_id_continue(ch) and ch not in ('$', '_'): 

998 return False 

999 return True 

1000 

1001 def _is_id_start(self, ch: str) -> bool: 

1002 return unicodedata.category(ch) in ( 

1003 'Lu', 

1004 'Ll', 

1005 'Li', 

1006 'Lt', 

1007 'Lm', 

1008 'Lo', 

1009 'Nl', 

1010 ) 

1011 

1012 def _is_id_continue(self, ch: str) -> bool: 

1013 return unicodedata.category(ch) in ( 

1014 'Lu', 

1015 'Ll', 

1016 'Li', 

1017 'Lt', 

1018 'Lm', 

1019 'Lo', 

1020 'Nl', 

1021 'Nd', 

1022 'Mn', 

1023 'Mc', 

1024 'Pc', 

1025 ) 

1026 

1027 def is_reserved_word(self, key: str) -> bool: 

1028 """Returns whether the key is a reserved word. 

1029 

1030 There should normally be no need to override this, unless there 

1031 have been reserved words added in later versions of the JSON5 

1032 spec and this implementation has not yet been updated to handle 

1033 the changes yet.""" 

1034 global _reserved_word_re 

1035 if _reserved_word_re is None: 

1036 # List taken from section 7.6.1 of ECMA-262, version 5.1. 

1037 # https://262.ecma-international.org/5.1/#sec-7.6.1. 

1038 # This includes currently reserved words, words reserved 

1039 # for future use (both as of 5.1), null, true, and false. 

1040 _reserved_word_re = re.compile( 

1041 '(' 

1042 + '|'.join( # noqa: FLY002 don't want an f-string here. 

1043 [ 

1044 'break', 

1045 'case', 

1046 'catch', 

1047 'class', 

1048 'const', 

1049 'continue', 

1050 'debugger', 

1051 'default', 

1052 'delete', 

1053 'do', 

1054 'else', 

1055 'enum', 

1056 'export', 

1057 'extends', 

1058 'false', 

1059 'finally', 

1060 'for', 

1061 'function', 

1062 'if', 

1063 'implements', 

1064 'import', 

1065 'in', 

1066 'instanceof', 

1067 'interface', 

1068 'let', 

1069 'new', 

1070 'null', 

1071 'package', 

1072 'private', 

1073 'protected', 

1074 'public', 

1075 'return', 

1076 'static', 

1077 'super', 

1078 'switch', 

1079 'this', 

1080 'throw', 

1081 'true', 

1082 'try', 

1083 'typeof', 

1084 'var', 

1085 'void', 

1086 'while', 

1087 'with', 

1088 'yield', 

1089 ] 

1090 ) 

1091 + ')$' 

1092 ) 

1093 return _reserved_word_re.match(key) is not None 

1094 

1095 

1096def _raise_type_error(obj) -> Any: 

1097 raise TypeError(f'{obj!r} is not JSON5 serializable')