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