1# Copyright (C) 2008 John Paulett (john -at- paulett.org)
2# Copyright (C) 2009-2024 David Aguilar (davvid -at- gmail.com)
3# All rights reserved.
4#
5# This software is licensed as described in the file COPYING, which
6# you should have received as part of this distribution.
7import decimal
8import inspect
9import itertools
10import sys
11import types
12import warnings
13from itertools import chain, islice
14from typing import (
15 Any,
16 Callable,
17 Dict,
18 Iterable,
19 List,
20 Optional,
21 Sequence,
22 Type,
23 Union,
24)
25
26from . import handlers, tags, util
27from .backend import JSONBackend, json
28
29
30def encode(
31 value: Any,
32 unpicklable: bool = True,
33 make_refs: bool = True,
34 keys: bool = False,
35 max_depth: Optional[int] = None,
36 reset: bool = True,
37 backend: Optional[JSONBackend] = None,
38 warn: bool = False,
39 context: Optional["Pickler"] = None,
40 max_iter: Optional[int] = None,
41 use_decimal: bool = False,
42 numeric_keys: bool = False,
43 use_base85: bool = False,
44 fail_safe: Optional[Callable[[Exception], Any]] = None,
45 indent: Optional[int] = None,
46 separators: Optional[Any] = None,
47 include_properties: bool = False,
48 handle_readonly: bool = False,
49) -> str:
50 """Return a JSON formatted representation of value, a Python object.
51
52 :param unpicklable: If set to ``False`` then the output will not contain the
53 information necessary to turn the JSON data back into Python objects,
54 but a simpler JSON stream is produced. It's recommended to set this
55 parameter to ``False`` when your code does not rely on two objects
56 having the same ``id()`` value, and when it is sufficient for those two
57 objects to be equal by ``==``, such as when serializing sklearn
58 instances. If you experience (de)serialization being incorrect when you
59 use numpy, pandas, or sklearn handlers, this should be set to ``False``.
60 If you want the output to not include the dtype for numpy arrays, add::
61
62 jsonpickle.register(
63 numpy.generic, UnpicklableNumpyGenericHandler, base=True
64 )
65
66 before your pickling code.
67 :param make_refs: If set to False jsonpickle's referencing support is
68 disabled. Objects that are id()-identical won't be preserved across
69 encode()/decode(), but the resulting JSON stream will be conceptually
70 simpler. jsonpickle detects cyclical objects and will break the cycle
71 by calling repr() instead of recursing when make_refs is set False.
72 :param keys: If set to True then jsonpickle will encode non-string
73 dictionary keys instead of coercing them into strings via `repr()`.
74 This is typically what you want if you need to support Integer or
75 objects as dictionary keys.
76 :param max_depth: If set to a non-negative integer then jsonpickle will
77 not recurse deeper than 'max_depth' steps into the object. Anything
78 deeper than 'max_depth' is represented using a Python repr() of the
79 object.
80 :param reset: Custom pickle handlers that use the `Pickler.flatten` method or
81 `jsonpickle.encode` function must call `encode` with `reset=False`
82 in order to retain object references during pickling.
83 This flag is not typically used outside of a custom handler or
84 `__getstate__` implementation.
85 :param backend: If set to an instance of jsonpickle.backend.JSONBackend,
86 jsonpickle will use that backend for deserialization.
87 :param warn: If set to True then jsonpickle will warn when it
88 returns None for an object which it cannot pickle
89 (e.g. file descriptors).
90 :param context: Supply a pre-built Pickler or Unpickler object to the
91 `jsonpickle.encode` and `jsonpickle.decode` machinery instead
92 of creating a new instance. The `context` represents the currently
93 active Pickler and Unpickler objects when custom handlers are
94 invoked by jsonpickle.
95 :param max_iter: If set to a non-negative integer then jsonpickle will
96 consume at most `max_iter` items when pickling iterators.
97 :param use_decimal: If set to True jsonpickle will allow Decimal
98 instances to pass-through, with the assumption that the simplejson
99 backend will be used in `use_decimal` mode. In order to use this mode
100 you will need to configure simplejson::
101
102 jsonpickle.set_encoder_options('simplejson',
103 use_decimal=True, sort_keys=True)
104 jsonpickle.set_decoder_options('simplejson',
105 use_decimal=True)
106 jsonpickle.set_preferred_backend('simplejson')
107
108 NOTE: A side-effect of the above settings is that float values will be
109 converted to Decimal when converting to json.
110 :param numeric_keys: Only use this option if the backend supports integer
111 dict keys natively. This flag tells jsonpickle to leave numeric keys
112 as-is rather than conforming them to json-friendly strings.
113 Using ``keys=True`` is the typical solution for integer keys, so only
114 use this if you have a specific use case where you want to allow the
115 backend to handle serialization of numeric dict keys.
116 :param use_base85:
117 If possible, use base85 to encode binary data. Base85 bloats binary data
118 by 1/4 as opposed to base64, which expands it by 1/3. This argument is
119 ignored on Python 2 because it doesn't support it.
120 :param fail_safe: If set to a function exceptions are ignored when pickling
121 and if a exception happens the function is called and the return value
122 is used as the value for the object that caused the error
123 :param indent: When `indent` is a non-negative integer, then JSON array
124 elements and object members will be pretty-printed with that indent
125 level. An indent level of 0 will only insert newlines. ``None`` is
126 the most compact representation. Since the default item separator is
127 ``(', ', ': ')``, the output might include trailing whitespace when
128 ``indent`` is specified. You can use ``separators=(',', ': ')`` to
129 avoid this. This value is passed directly to the active JSON backend
130 library and not used by jsonpickle directly.
131 :param separators:
132 If ``separators`` is an ``(item_separator, dict_separator)`` tuple
133 then it will be used instead of the default ``(', ', ': ')``
134 separators. ``(',', ':')`` is the most compact JSON representation.
135 This value is passed directly to the active JSON backend library and
136 not used by jsonpickle directly.
137 :param include_properties:
138 Include the names and values of class properties in the generated json.
139 Properties are unpickled properly regardless of this setting, this is
140 meant to be used if processing the json outside of Python. Certain types
141 such as sets will not pickle due to not having a native-json equivalent.
142 Defaults to ``False``.
143 :param handle_readonly:
144 Handle objects with readonly methods, such as Django's SafeString. This
145 basically prevents jsonpickle from raising an exception for such objects.
146 You MUST set ``handle_readonly=True`` for the decoding if you encode with
147 this flag set to ``True``.
148
149 >>> encode('my string') == '"my string"'
150 True
151 >>> encode(36) == '36'
152 True
153 >>> encode({'foo': True}) == '{"foo": true}'
154 True
155 >>> encode({'foo': [1, 2, [3, 4]]}, max_depth=1)
156 '{"foo": "[1, 2, [3, 4]]"}'
157
158 """
159 backend = backend or json
160 context = context or Pickler(
161 unpicklable=unpicklable,
162 make_refs=make_refs,
163 keys=keys,
164 backend=backend,
165 max_depth=max_depth,
166 warn=warn,
167 max_iter=max_iter,
168 numeric_keys=numeric_keys,
169 use_decimal=use_decimal,
170 use_base85=use_base85,
171 fail_safe=fail_safe,
172 include_properties=include_properties,
173 handle_readonly=handle_readonly,
174 original_object=value,
175 )
176 return backend.encode(
177 context.flatten(value, reset=reset), indent=indent, separators=separators
178 )
179
180
181def _in_cycle(
182 obj: Any, objs: Dict[int, int], max_reached: bool, make_refs: bool
183) -> bool:
184 """Detect cyclic structures that would lead to infinite recursion"""
185 return (
186 (max_reached or (not make_refs and id(obj) in objs))
187 and not util._is_primitive(obj)
188 and not util._is_enum(obj)
189 )
190
191
192def _mktyperef(obj: Type[Any]) -> Dict[str, str]:
193 """Return a typeref dictionary
194
195 >>> _mktyperef(AssertionError) == {'py/type': 'builtins.AssertionError'}
196 True
197
198 """
199 return {tags.TYPE: util.importable_name(obj)}
200
201
202def _wrap_string_slot(string: Union[str, Sequence[str]]) -> Sequence[str]:
203 """Converts __slots__ = 'a' into __slots__ = ('a',)"""
204 if isinstance(string, str):
205 return (string,)
206 return string
207
208
209class Pickler:
210 def __init__(
211 self,
212 unpicklable: bool = True,
213 make_refs: bool = True,
214 max_depth: Optional[int] = None,
215 backend: Optional[JSONBackend] = None,
216 keys: bool = False,
217 warn: bool = False,
218 max_iter: Optional[int] = None,
219 numeric_keys: bool = False,
220 use_decimal: bool = False,
221 use_base85: bool = False,
222 fail_safe: Optional[Callable[[Exception], Any]] = None,
223 include_properties: bool = False,
224 handle_readonly: bool = False,
225 original_object: Optional[Any] = None,
226 ) -> None:
227 self.unpicklable = unpicklable
228 self.make_refs = make_refs
229 self.backend = backend or json
230 self.keys = keys
231 self.warn = warn
232 self.numeric_keys = numeric_keys
233 self.use_base85 = use_base85
234 # The current recursion depth
235 self._depth = -1
236 # The maximal recursion depth
237 self._max_depth = max_depth
238 # Maps id(obj) to reference IDs
239 self._objs = {}
240 # Avoids garbage collection
241 self._seen = []
242 # maximum amount of items to take from a pickled iterator
243 self._max_iter = max_iter
244 # Whether to allow decimals to pass-through
245 self._use_decimal = use_decimal
246 # A cache of objects that have already been flattened.
247 self._flattened = {}
248 # Used for util._is_readonly, see +483
249 self.handle_readonly = handle_readonly
250
251 if self.use_base85:
252 self._bytes_tag = tags.B85
253 self._bytes_encoder = util.b85encode
254 else:
255 self._bytes_tag = tags.B64
256 self._bytes_encoder = util.b64encode
257
258 # ignore exceptions
259 self.fail_safe = fail_safe
260 self.include_properties = include_properties
261
262 self._original_object = original_object
263
264 def _determine_sort_keys(self) -> bool:
265 for _, options in getattr(self.backend, '_encoder_options', {}).values():
266 if options.get('sort_keys', False):
267 # the user has set one of the backends to sort keys
268 return True
269 return False
270
271 def _sort_attrs(self, obj: Any) -> Any:
272 if hasattr(obj, '__slots__') and self.warn:
273 # Slots are read-only by default, the only way
274 # to sort keys is to do it in a subclass
275 # and that would require calling the init function
276 # of the parent again. That could cause issues
277 # so we refuse to handle it.
278 raise TypeError(
279 'Objects with __slots__ cannot have their keys reliably sorted by '
280 'jsonpickle! Please sort the keys in the __slots__ definition instead.'
281 )
282 # Somehow some classes don't have slots or dict
283 elif hasattr(obj, '__dict__'):
284 try:
285 obj.__dict__ = dict(sorted(obj.__dict__.items()))
286 except (TypeError, AttributeError):
287 # Can't set attributes of builtin/extension type
288 pass
289 return obj
290
291 def reset(self) -> None:
292 self._objs = {}
293 self._depth = -1
294 self._seen = []
295 self._flattened = {}
296
297 def _push(self) -> None:
298 """Steps down one level in the namespace."""
299 self._depth += 1
300
301 def _pop(self, value: Any) -> Any:
302 """Step up one level in the namespace and return the value.
303 If we're at the root, reset the pickler's state.
304 """
305 self._depth -= 1
306 if self._depth == -1:
307 self.reset()
308 return value
309
310 def _log_ref(self, obj: Any) -> bool:
311 """
312 Log a reference to an in-memory object.
313 Return True if this object is new and was assigned
314 a new ID. Otherwise return False.
315 """
316 objid = id(obj)
317 is_new = objid not in self._objs
318 if is_new:
319 new_id = len(self._objs)
320 self._objs[objid] = new_id
321 return is_new
322
323 def _mkref(self, obj: Any) -> bool:
324 """
325 Log a reference to an in-memory object, and return
326 if that object should be considered newly logged.
327 """
328 is_new = self._log_ref(obj)
329 # Pretend the object is new
330 pretend_new = not self.unpicklable or not self.make_refs
331 return pretend_new or is_new
332
333 def _getref(self, obj: Any) -> Dict[str, int]:
334 """Return a "py/id" entry for the specified object"""
335 return {tags.ID: self._objs.get(id(obj))} # type: ignore[dict-item]
336
337 def _flatten(self, obj: Any) -> Any:
338 """Flatten an object and its guts into a json-safe representation"""
339 if self.unpicklable and self.make_refs:
340 result = self._flatten_impl(obj)
341 else:
342 try:
343 result = self._flattened[id(obj)]
344 except KeyError:
345 result = self._flattened[id(obj)] = self._flatten_impl(obj)
346 return result
347
348 def flatten(self, obj: Any, reset: bool = True) -> Any:
349 """Takes an object and returns a JSON-safe representation of it.
350
351 Simply returns any of the basic builtin datatypes
352
353 >>> p = Pickler()
354 >>> p.flatten('hello world') == 'hello world'
355 True
356 >>> p.flatten(49)
357 49
358 >>> p.flatten(350.0)
359 350.0
360 >>> p.flatten(True)
361 True
362 >>> p.flatten(False)
363 False
364 >>> r = p.flatten(None)
365 >>> r is None
366 True
367 >>> p.flatten(False)
368 False
369 >>> p.flatten([1, 2, 3, 4])
370 [1, 2, 3, 4]
371 >>> p.flatten((1,2,))[tags.TUPLE]
372 [1, 2]
373 >>> p.flatten({'key': 'value'}) == {'key': 'value'}
374 True
375 """
376 if reset:
377 self.reset()
378 if self._determine_sort_keys():
379 obj = self._sort_attrs(obj)
380 return self._flatten(obj)
381
382 def _flatten_bytestring(self, obj: bytes) -> Dict[str, str]:
383 return {self._bytes_tag: self._bytes_encoder(obj)}
384
385 def _flatten_impl(self, obj: Any) -> Any:
386 #########################################
387 # if obj is nonrecursive return immediately
388 # for performance reasons we don't want to do recursive checks
389 if type(obj) is bytes:
390 return self._flatten_bytestring(obj)
391
392 # Decimal is a primitive when use_decimal is True
393 if type(obj) in (str, bool, int, float, type(None)) or (
394 self._use_decimal and isinstance(obj, decimal.Decimal)
395 ):
396 return obj
397 #########################################
398
399 self._push()
400 return self._pop(self._flatten_obj(obj))
401
402 def _max_reached(self) -> bool:
403 return self._depth == self._max_depth
404
405 def _pickle_warning(self, obj: Any) -> None:
406 if self.warn:
407 msg = 'jsonpickle cannot pickle %r: replaced with None' % obj
408 warnings.warn(msg)
409
410 def _flatten_obj(self, obj: Any) -> Any:
411 self._seen.append(obj)
412
413 max_reached = self._max_reached()
414
415 try:
416 in_cycle = _in_cycle(obj, self._objs, max_reached, self.make_refs)
417 if in_cycle:
418 # break the cycle
419 flatten_func = repr
420 else:
421 flatten_func = self._get_flattener(obj) # type: ignore[assignment]
422
423 if flatten_func is None:
424 self._pickle_warning(obj)
425 return None
426
427 return flatten_func(obj)
428
429 except (KeyboardInterrupt, SystemExit) as e:
430 raise e
431 except Exception as e:
432 if self.fail_safe is None:
433 raise e
434 else:
435 return self.fail_safe(e)
436
437 def _list_recurse(self, obj: Iterable[Any]) -> List[Any]:
438 return [self._flatten(v) for v in obj]
439
440 def _flatten_function(self, obj: Callable[..., Any]) -> Optional[Dict[str, str]]:
441 if self.unpicklable:
442 data = {tags.FUNCTION: util.importable_name(obj)}
443 else:
444 data = None
445
446 return data
447
448 def _getstate(self, obj: Any, data: Dict[str, Any]):
449 state = self._flatten(obj)
450 if self.unpicklable:
451 data[tags.STATE] = state
452 else:
453 data = state
454 return data
455
456 def _flatten_key_value_pair(
457 self, k: Any, v: Any, data: Dict[Union[str, Any], Any]
458 ) -> Dict[Union[str, Any], Any]:
459 """Flatten a key/value pair into the passed-in dictionary."""
460 if not util._is_picklable(k, v):
461 return data
462 # TODO: use inspect.getmembers_static on 3.11+ because it avoids dynamic
463 # attribute lookups
464 if (
465 self.handle_readonly
466 and k in {attr for attr, val in inspect.getmembers(self._original_object)}
467 and util._is_readonly(self._original_object, k, v)
468 ):
469 return data
470
471 if k is None:
472 k = 'null' # for compatibility with common json encoders
473
474 if self.numeric_keys and isinstance(k, (int, float)):
475 pass
476 elif not isinstance(k, str):
477 try:
478 k = repr(k)
479 except Exception:
480 k = str(k)
481
482 data[k] = self._flatten(v)
483 return data
484
485 def _flatten_obj_attrs(
486 self, obj: Any, attrs: Iterable[str], data: Dict[str, Any]
487 ) -> bool:
488 flatten = self._flatten_key_value_pair
489 ok = False
490 for k in attrs:
491 try:
492 if not k.startswith('__'):
493 value = getattr(obj, k)
494 else:
495 value = getattr(obj, f"_{obj.__class__.__name__}{k}")
496 flatten(k, value, data)
497 except AttributeError:
498 # The attribute may have been deleted
499 continue
500 ok = True
501 return ok
502
503 def _flatten_properties(
504 self,
505 obj: Any,
506 data: Dict[str, Any],
507 allslots: Optional[Iterable[Sequence[str]]] = None,
508 ) -> Dict[str, Any]:
509 if allslots is None:
510 # setting a list as a default argument can lead to some weird errors
511 allslots = []
512
513 # convert to set in case there are a lot of slots
514 allslots_set = set(itertools.chain.from_iterable(allslots))
515
516 # i don't like lambdas
517 def valid_property(x):
518 return not x[0].startswith('__') and x[0] not in allslots_set
519
520 properties = [
521 x[0] for x in inspect.getmembers(obj.__class__) if valid_property(x)
522 ]
523
524 properties_dict = {}
525 for p_name in properties:
526 p_val = getattr(obj, p_name)
527 if util._is_not_class(p_val):
528 properties_dict[p_name] = p_val
529 else:
530 properties_dict[p_name] = self._flatten(p_val)
531
532 data[tags.PROPERTY] = properties_dict
533
534 return data
535
536 def _flatten_newstyle_with_slots(
537 self, obj: Any, data: Dict[str, Any]
538 ) -> Dict[str, Any]:
539 """Return a json-friendly dict for new-style objects with __slots__."""
540 allslots = [
541 _wrap_string_slot(getattr(cls, '__slots__', tuple()))
542 for cls in obj.__class__.mro()
543 ]
544
545 # add properties to the attribute list
546 if self.include_properties:
547 data = self._flatten_properties(obj, data, allslots)
548
549 if not self._flatten_obj_attrs(obj, chain(*allslots), data):
550 attrs = [
551 x for x in dir(obj) if not x.startswith('__') and not x.endswith('__')
552 ]
553 self._flatten_obj_attrs(obj, attrs, data)
554
555 return data
556
557 def _flatten_obj_instance(
558 self, obj: Any
559 ) -> Optional[Union[Dict[str, Any], List[Any]]]:
560 """Recursively flatten an instance and return a json-friendly dict"""
561 # we're generally not bothering to annotate parts that aren't part of the public API
562 # but this annotation alone saves us 3 mypy "errors"
563 data: Dict[str, Any] = {}
564 has_class = hasattr(obj, '__class__')
565 has_dict = hasattr(obj, '__dict__')
566 has_slots = not has_dict and hasattr(obj, '__slots__')
567 has_getnewargs = util.has_method(obj, '__getnewargs__')
568 has_getnewargs_ex = util.has_method(obj, '__getnewargs_ex__')
569 has_getinitargs = util.has_method(obj, '__getinitargs__')
570 has_reduce, has_reduce_ex = util.has_reduce(obj)
571 exclude = set(getattr(obj, '_jsonpickle_exclude', ()))
572
573 # Support objects with __getstate__(); this ensures that
574 # both __setstate__() and __getstate__() are implemented
575 has_own_getstate = hasattr(type(obj), '__getstate__') and type(
576 obj
577 ).__getstate__ is not getattr(object, '__getstate__', None)
578 # not using has_method since __getstate__() is handled separately below
579 # Note: on Python 3.11+, all objects have __getstate__.
580
581 if has_class:
582 cls = obj.__class__
583 else:
584 cls = type(obj)
585
586 # Check for a custom handler
587 class_name = util.importable_name(cls)
588 handler = handlers.get(cls, handlers.get(class_name)) # type: ignore[arg-type]
589 if handler is not None:
590 if self.unpicklable:
591 data[tags.OBJECT] = class_name
592 result = handler(self).flatten(obj, data)
593 if result is None:
594 self._pickle_warning(obj)
595 return result
596
597 reduce_val = None
598
599 if self.include_properties:
600 data = self._flatten_properties(obj, data)
601
602 if self.unpicklable:
603 if has_reduce and not has_reduce_ex:
604 try:
605 reduce_val = obj.__reduce__()
606 except TypeError:
607 # A lot of builtin types have a reduce which
608 # just raises a TypeError
609 # we ignore those
610 pass
611
612 # test for a reduce implementation, and redirect before
613 # doing anything else if that is what reduce requests
614 elif has_reduce_ex:
615 try:
616 # we're implementing protocol 2
617 reduce_val = obj.__reduce_ex__(2)
618 except TypeError:
619 # A lot of builtin types have a reduce which
620 # just raises a TypeError
621 # we ignore those
622 pass
623
624 if reduce_val and isinstance(reduce_val, str):
625 try:
626 varpath = iter(reduce_val.split('.'))
627 # curmod will be transformed by the
628 # loop into the value to pickle
629 curmod = sys.modules[next(varpath)]
630 for modname in varpath:
631 curmod = getattr(curmod, modname)
632 # replace obj with value retrieved
633 return self._flatten(curmod)
634 except KeyError:
635 # well, we can't do anything with that, so we ignore it
636 pass
637
638 elif reduce_val:
639 # at this point, reduce_val should be some kind of iterable
640 # pad out to len 5
641 rv_as_list = list(reduce_val)
642 insufficiency = 5 - len(rv_as_list)
643 if insufficiency:
644 rv_as_list += [None] * insufficiency
645
646 if getattr(rv_as_list[0], '__name__', '') == '__newobj__':
647 rv_as_list[0] = tags.NEWOBJ
648
649 f, args, state, listitems, dictitems = rv_as_list
650
651 # check that getstate/setstate is sane
652 if not (
653 state
654 and has_own_getstate
655 and not hasattr(obj, '__setstate__')
656 and not isinstance(obj, dict)
657 ):
658 # turn iterators to iterables for convenient serialization
659 if rv_as_list[3]:
660 rv_as_list[3] = tuple(rv_as_list[3])
661
662 if rv_as_list[4]:
663 rv_as_list[4] = tuple(rv_as_list[4])
664
665 reduce_args = list(map(self._flatten, rv_as_list))
666 last_index = len(reduce_args) - 1
667 while last_index >= 2 and reduce_args[last_index] is None:
668 last_index -= 1
669 data[tags.REDUCE] = reduce_args[: last_index + 1]
670
671 return data
672
673 if has_class and not isinstance(obj, types.ModuleType):
674 if self.unpicklable:
675 data[tags.OBJECT] = class_name
676
677 if has_getnewargs_ex:
678 data[tags.NEWARGSEX] = [
679 self._flatten(arg) for arg in obj.__getnewargs_ex__()
680 ]
681
682 if has_getnewargs and not has_getnewargs_ex:
683 data[tags.NEWARGS] = self._flatten(obj.__getnewargs__())
684
685 if has_getinitargs:
686 data[tags.INITARGS] = self._flatten(obj.__getinitargs__())
687
688 if has_own_getstate:
689 try:
690 state = obj.__getstate__()
691 except TypeError:
692 # Has getstate but it cannot be called, e.g. file descriptors
693 # in Python3
694 self._pickle_warning(obj)
695 return None
696 else:
697 if state:
698 return self._getstate(state, data)
699
700 if isinstance(obj, types.ModuleType):
701 if self.unpicklable:
702 data[tags.MODULE] = '{name}/{name}'.format(name=obj.__name__)
703 else:
704 # TODO: this causes a mypy assignment error, figure out
705 # if it's actually an error or a false alarm
706 data = str(obj) # type: ignore[assignment]
707 return data
708
709 if util._is_dictionary_subclass(obj):
710 self._flatten_dict_obj(obj, data, exclude=exclude)
711 return data
712
713 if util._is_sequence_subclass(obj):
714 return self._flatten_sequence_obj(obj, data)
715
716 if util._is_iterator(obj):
717 # force list in python 3
718 data[tags.ITERATOR] = list(map(self._flatten, islice(obj, self._max_iter)))
719 return data
720
721 if has_dict:
722 # Support objects that subclasses list and set
723 if util._is_sequence_subclass(obj):
724 return self._flatten_sequence_obj(obj, data)
725
726 # hack for zope persistent objects; this unghostifies the object
727 getattr(obj, '_', None)
728 return self._flatten_dict_obj(obj.__dict__, data, exclude=exclude)
729
730 if has_slots:
731 return self._flatten_newstyle_with_slots(obj, data)
732
733 # catchall return for data created above without a return
734 # (e.g. __getnewargs__ is not supposed to be the end of the story)
735 if data:
736 return data
737
738 self._pickle_warning(obj)
739 return None
740
741 def _ref_obj_instance(self, obj: Any) -> Optional[Union[Dict[str, Any], List[Any]]]:
742 """Reference an existing object or flatten if new"""
743 if self.unpicklable:
744 if self._mkref(obj):
745 # We've never seen this object so return its
746 # json representation.
747 return self._flatten_obj_instance(obj)
748 # We've seen this object before so place an object
749 # reference tag in the data. This avoids infinite recursion
750 # when processing cyclical objects.
751 return self._getref(obj)
752 else:
753 max_reached = self._max_reached()
754 in_cycle = _in_cycle(obj, self._objs, max_reached, False)
755 if in_cycle:
756 # A circular becomes None.
757 return None
758
759 self._mkref(obj)
760 return self._flatten_obj_instance(obj)
761
762 def _escape_key(self, k: Any) -> str:
763 return tags.JSON_KEY + encode(
764 k,
765 reset=False,
766 keys=True,
767 context=self,
768 backend=self.backend,
769 make_refs=self.make_refs,
770 )
771
772 def _flatten_non_string_key_value_pair(
773 self, k: Any, v: Any, data: Dict[str, Any]
774 ) -> Dict[str, Any]:
775 """Flatten only non-string key/value pairs"""
776 if not util._is_picklable(k, v):
777 return data
778 if self.keys and not isinstance(k, str):
779 k = self._escape_key(k)
780 data[k] = self._flatten(v)
781 return data
782
783 def _flatten_string_key_value_pair(self, k: str, v: Any, data: Dict[str, Any]):
784 """Flatten string key/value pairs only."""
785 if not util._is_picklable(k, v):
786 return data
787 if self.keys:
788 if not isinstance(k, str):
789 return data
790 elif k.startswith(tags.JSON_KEY):
791 k = self._escape_key(k)
792 else:
793 if k is None:
794 k = 'null' # for compatibility with common json encoders
795
796 if self.numeric_keys and isinstance(k, (int, float)):
797 pass
798 elif not isinstance(k, str):
799 try:
800 k = repr(k)
801 except Exception:
802 k = str(k)
803
804 data[k] = self._flatten(v)
805 return data
806
807 def _flatten_dict_obj(
808 self,
809 obj: dict,
810 data: Optional[Dict[Any, Any]] = None,
811 exclude: Iterable[Any] = (),
812 ) -> Dict[str, Any]:
813 """Recursively call flatten() and return json-friendly dict"""
814 if data is None:
815 data = obj.__class__()
816
817 # If we allow non-string keys then we have to do a two-phase
818 # encoding to ensure that the reference IDs are deterministic.
819 if self.keys:
820 # Phase 1: serialize regular objects, ignore fancy keys.
821 flatten = self._flatten_string_key_value_pair
822 for k, v in util.items(obj, exclude=exclude):
823 flatten(k, v, data)
824
825 # Phase 2: serialize non-string keys.
826 flatten = self._flatten_non_string_key_value_pair
827 for k, v in util.items(obj, exclude=exclude):
828 flatten(k, v, data)
829 else:
830 # If we have string keys only then we only need a single pass.
831 flatten = self._flatten_key_value_pair
832 for k, v in util.items(obj, exclude=exclude):
833 flatten(k, v, data)
834
835 # the collections.defaultdict protocol
836 if hasattr(obj, 'default_factory') and callable(obj.default_factory):
837 factory = obj.default_factory
838 if util._is_type(factory):
839 # Reference the class/type
840 # in this case it's Dict[str, str]
841 value: Dict[str, str] = _mktyperef(factory)
842 else:
843 # The factory is not a type and could reference e.g. functions
844 # or even the object instance itself, which creates a cycle.
845 if self._mkref(factory):
846 # We've never seen this object before so pickle it in-place.
847 # Create an instance from the factory and assume that the
848 # resulting instance is a suitable exemplar.
849 value: Dict[str, Any] = self._flatten_obj_instance(handlers.CloneFactory(factory())) # type: ignore[no-redef]
850 else:
851 # We've seen this object before.
852 # Break the cycle by emitting a reference.
853 # in this case it's Dict[str, int]
854 value: Dict[str, int] = self._getref(factory) # type: ignore[no-redef]
855 data['default_factory'] = value
856
857 # Sub-classes of dict
858 if hasattr(obj, '__dict__') and self.unpicklable and obj != obj.__dict__:
859 if self._mkref(obj.__dict__):
860 dict_data = {}
861 self._flatten_dict_obj(obj.__dict__, dict_data, exclude=exclude)
862 data['__dict__'] = dict_data
863 else:
864 data['__dict__'] = self._getref(obj.__dict__)
865
866 return data
867
868 def _get_flattener(self, obj: Any) -> Optional[Callable[[Any], Any]]:
869 if type(obj) in (list, dict):
870 if self._mkref(obj):
871 return (
872 self._list_recurse if type(obj) is list else self._flatten_dict_obj
873 )
874 else:
875 return self._getref
876
877 # We handle tuples and sets by encoding them in a "(tuple|set)dict"
878 elif type(obj) in (tuple, set):
879 if not self.unpicklable:
880 return self._list_recurse
881 return lambda obj: {
882 tags.TUPLE if type(obj) is tuple else tags.SET: [
883 self._flatten(v) for v in obj
884 ]
885 }
886
887 elif util._is_module_function(obj):
888 return self._flatten_function
889
890 elif util._is_object(obj):
891 return self._ref_obj_instance
892
893 elif util._is_type(obj):
894 return _mktyperef
895
896 # instance methods, lambdas, old style classes...
897 self._pickle_warning(obj)
898 return None
899
900 def _flatten_sequence_obj(
901 self, obj: Iterable[Any], data: Dict[str, Any]
902 ) -> Union[Dict[str, Any], List[Any]]:
903 """Return a json-friendly dict for a sequence subclass."""
904 if hasattr(obj, '__dict__'):
905 self._flatten_dict_obj(obj.__dict__, data)
906 value = [self._flatten(v) for v in obj]
907 if self.unpicklable:
908 data[tags.SEQ] = value
909 else:
910 return value
911 return data