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
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
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.
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)
33from json5.parser import Parser
35# Used when encoding keys, below.
36_reserved_word_re: Optional[re.Pattern] = None
39def _get_mapping_types():
40 frozendict_type = getattr(builtins, 'frozendict', None)
41 return (dict,) if frozendict_type is None else (dict, frozendict_type)
44_mapping_types = _get_mapping_types()
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]')
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
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))
64 return _surrogate_pair_re.sub(combine, s)
67class QuoteStyle(enum.Enum):
68 """Controls how strings will be quoted during encoding.
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.
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 """
95 ALWAYS_DOUBLE = 'always_double'
96 ALWAYS_SINGLE = 'always_single'
97 PREFER_DOUBLE = 'prefer_double'
98 PREFER_SINGLE = 'prefer_single'
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.
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.
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.
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.
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 """
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
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.
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.
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 """
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
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.
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.
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.
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.
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.
318 Note that this does *not* raise a `ValueError`; instead any error is
319 returned as the second value in the tuple.
321 You can use this method to read in a series of values from a string
322 `s` as follows:
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]
342 """
343 assert cls is None, 'Custom decoders are not supported'
345 if isinstance(s, bytes):
346 encoding = encoding or 'utf-8'
347 s = s.decode(encoding)
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
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
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'))
386 def _parse_int(s):
387 if s.lstrip('+-').startswith(('0x', '0X')):
388 return int(s, 16)
389 return int(s)
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)
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)
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 )
411 return _walk_ast(ast, _dictify, parse_float, parse_int, parse_constant)
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
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.
478 Supports the same arguments as ``dumps()``, below.
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 """
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 )
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.
528 Supports the same arguments as ``json.dumps()``, except that:
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.
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.
563 *Note*: Strings that are being used as unquoted keys are not affected
564 by this parameter and remain unquoted.
566 *`quote_style` was added in version 0.10.0*.
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.
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.
579 For example:
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]'
600 ```
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 """
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)
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
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
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
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.
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)
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.
697 `seen` is used for duplicate object tracking when `check_circular`
698 is True.
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).
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.
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
721 if as_key:
722 raise TypeError(f'Invalid key f{obj}')
723 return self._encode_non_basic_type(obj, seen, level)
725 def _encode_basic_type(self, obj: Any, *, as_key: bool) -> Optional[str]:
726 """Returns None if the object is not a basic type."""
728 if isinstance(obj, str):
729 return self._encode_str(obj, as_key=as_key)
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'
741 if isinstance(obj, int):
742 return self._encode_int(obj, as_key=as_key)
744 if isinstance(obj, float):
745 return self._encode_float(obj, as_key=as_key)
747 return None
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
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)
767 if not allowed:
768 raise ValueError(f'Illegal JSON5 value: {obj}')
769 return f'"{s}"' if as_key else s
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
785 return self._encode_quoted_str(obj, self.quote_style)
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)
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)
841 if quote_style in (QuoteStyle.ALWAYS_DOUBLE, QuoteStyle.PREFER_DOUBLE):
842 return '"' + ''.join(ret) + '"'
843 return "'" + ''.join(ret) + "'"
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'
864 o = ord(ch)
865 if o < 65536:
866 return rf'\u{o:04x}'
868 val = o - 0x10000
869 high = 0xD800 + (val >> 10)
870 low = 0xDC00 + (val & 0x3FF)
871 return rf'\u{high:04x}\u{low:04x}'
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)
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
902 if self.check_circular:
903 seen.remove(i)
904 return s
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
911 if self.sort_keys:
912 keys = sorted(obj.keys())
913 else:
914 keys = obj.keys()
916 s = '{' + indent_str
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
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)
940 if first_key:
941 first_key = False
942 else:
943 s += item_sep
945 val_str = self.encode(obj[key], seen, level, as_key=False)
946 s += key_str + kv_sep + val_str
948 if first_key:
949 return '{}'
950 s += end_str + '}'
951 return s
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 '[]'
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 + ']'
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
982 def is_identifier(self, key: str) -> bool:
983 """Returns whether the string could be used as a legal
984 EcmaScript/JavaScript identifier.
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
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 )
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 )
1027 def is_reserved_word(self, key: str) -> bool:
1028 """Returns whether the key is a reserved word.
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
1096def _raise_type_error(obj) -> Any:
1097 raise TypeError(f'{obj!r} is not JSON5 serializable')