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