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 _reduce(self, obj: Any, has_reduce: bool, has_reduce_ex: bool) -> Any:
583 """Return the object's __reduce__/__reduce_ex__ output, or None.
585 Many builtin types raise TypeError from these; treat that as
586 "no reduce available" rather than letting it propagate.
587 """
588 try:
589 if has_reduce and not has_reduce_ex:
590 return obj.__reduce__()
591 if has_reduce_ex:
592 # we're implementing protocol 2
593 return obj.__reduce_ex__(2)
594 except TypeError:
595 pass
596 return None
598 def _flatten_obj_instance(
599 self, obj: Any
600 ) -> dict[str, Any] | list[Any] | Any | None:
601 """Recursively flatten an instance and return a json-friendly dict"""
602 # we're generally not bothering to annotate parts that aren't part of the public API
603 # but this annotation alone saves us 3 mypy "errors"
604 data: dict[str, Any] = {}
605 has_class = hasattr(obj, "__class__")
606 has_dict = hasattr(obj, "__dict__")
607 has_slots = not has_dict and hasattr(obj, "__slots__")
608 has_getnewargs = util.has_method(obj, "__getnewargs__")
609 has_getnewargs_ex = util.has_method(obj, "__getnewargs_ex__")
610 has_getinitargs = util.has_method(obj, "__getinitargs__")
611 has_reduce, has_reduce_ex = util.has_reduce(obj)
612 exclude = set(getattr(obj, "_jsonpickle_exclude", ()))
614 # Support objects with __getstate__(); this ensures that
615 # both __setstate__() and __getstate__() are implemented
616 has_own_getstate = hasattr(type(obj), "__getstate__") and type(
617 obj
618 ).__getstate__ is not getattr(object, "__getstate__", None)
619 # not using has_method since __getstate__() is handled separately below
620 # Note: on Python 3.11+, all objects have __getstate__.
622 if has_class:
623 cls = obj.__class__
624 else:
625 cls = type(obj)
627 # Check for a custom handler
628 class_name = util.importable_name(cls)
629 handler = handlers.get(cls, handlers.get(class_name)) # type: ignore[arg-type]
630 if handler is not None:
631 if self.unpicklable:
632 data[tags.OBJECT] = class_name
633 handler_instance = handler(self)
634 result = self._call_handler_flatten(handler_instance, obj, data)
635 if result is None:
636 self._pickle_warning(obj)
637 return result
639 if self.include_properties:
640 data = self._flatten_properties(obj, data)
642 if self.unpicklable:
643 # test for a reduce implementation, and redirect before
644 # doing anything else if that is what reduce requests
645 reduce_val = self._reduce(obj, has_reduce, has_reduce_ex)
647 if reduce_val and isinstance(reduce_val, str):
648 try:
649 varpath = iter(reduce_val.split("."))
650 # curmod will be transformed by the
651 # loop into the value to pickle
652 curmod = sys.modules[next(varpath)]
653 for modname in varpath:
654 curmod = getattr(curmod, modname)
655 # replace obj with value retrieved
656 return self._flatten(curmod)
657 except KeyError:
658 # well, we can't do anything with that, so we ignore it
659 pass
661 elif reduce_val:
662 # at this point, reduce_val should be some kind of iterable
663 # pad out to len 5
664 rv_as_list = list(reduce_val)
665 insufficiency = 5 - len(rv_as_list)
666 if insufficiency:
667 rv_as_list += [None] * insufficiency
669 if getattr(rv_as_list[0], "__name__", "") == "__newobj__":
670 rv_as_list[0] = tags.NEWOBJ
672 f, args, state, listitems, dictitems = rv_as_list
674 # check that getstate/setstate is sane
675 if not (
676 state
677 and has_own_getstate
678 and not hasattr(obj, "__setstate__")
679 and not isinstance(obj, dict)
680 ):
681 # turn iterators to iterables for convenient serialization
682 if rv_as_list[3]:
683 rv_as_list[3] = tuple(rv_as_list[3])
685 if rv_as_list[4]:
686 rv_as_list[4] = tuple(rv_as_list[4])
688 reduce_args = list(map(self._flatten, rv_as_list))
689 last_index = len(reduce_args) - 1
690 while last_index >= 2 and reduce_args[last_index] is None:
691 last_index -= 1
692 data[tags.REDUCE] = reduce_args[: last_index + 1]
694 return data
696 if has_class and not isinstance(obj, types.ModuleType):
697 if self.unpicklable:
698 data[tags.OBJECT] = class_name
700 if has_getnewargs_ex:
701 data[tags.NEWARGSEX] = [
702 self._flatten(arg) for arg in obj.__getnewargs_ex__()
703 ]
705 if has_getnewargs and not has_getnewargs_ex:
706 data[tags.NEWARGS] = self._flatten(obj.__getnewargs__())
708 if has_getinitargs:
709 data[tags.INITARGS] = self._flatten(obj.__getinitargs__())
711 if has_own_getstate:
712 try:
713 state = obj.__getstate__()
714 except TypeError:
715 # Has getstate but it cannot be called, e.g. file descriptors
716 # in Python3
717 self._pickle_warning(obj)
718 return None
719 else:
720 if exclude and isinstance(state, dict):
721 state = {k: v for k, v in util.items(state, exclude=exclude)}
722 if state:
723 return self._getstate(state, data)
725 if isinstance(obj, types.ModuleType):
726 if self.unpicklable:
727 data[tags.MODULE] = "{name}/{name}".format(name=obj.__name__)
728 else:
729 # TODO: this causes a mypy assignment error, figure out
730 # if it's actually an error or a false alarm
731 data = str(obj) # type: ignore[assignment]
732 return data
734 if util._is_dictionary_subclass(obj):
735 self._flatten_dict_obj(obj, data, exclude=exclude)
736 return data
738 if util._is_sequence_subclass(obj):
739 return self._flatten_sequence_obj(obj, data)
741 if util._is_iterator(obj):
742 # force list in python 3
743 data[tags.ITERATOR] = list(map(self._flatten, islice(obj, self._max_iter)))
744 return data
746 if has_dict:
747 # Support objects that subclasses list and set
748 if util._is_sequence_subclass(obj):
749 return self._flatten_sequence_obj(obj, data)
751 # hack for zope persistent objects; this unghostifies the object
752 getattr(obj, "_", None)
753 return self._flatten_dict_obj(obj.__dict__, data, exclude=exclude)
755 if has_slots:
756 return self._flatten_newstyle_with_slots(obj, data, exclude=exclude)
758 # catchall return for data created above without a return
759 # (e.g. __getnewargs__ is not supposed to be the end of the story)
760 if data:
761 return data
763 # Objects whose state is only reachable through __reduce__/__reduce_ex__
764 # (e.g. datetime.timedelta) have no __dict__, __slots__ or __getstate__
765 # for the branches above to read, so nothing has been produced and they
766 # would otherwise become null. Emit a lossy view built from the reduce
767 # output instead: its state if present, else the constructor args. The
768 # string form and the listitems/dictitems slots (append/update-based
769 # reconstruction) are not represented and keep the previous behaviour.
770 if not self.unpicklable:
771 reduce_val = self._reduce(obj, has_reduce, has_reduce_ex)
772 if reduce_val is not None and not isinstance(reduce_val, str):
773 # reduce tuple: (callable, args, state, listitems, dictitems)
774 rv_as_list = list(reduce_val)
775 state = rv_as_list[2] if len(rv_as_list) > 2 else None
776 if state:
777 return self._flatten(state)
778 args = rv_as_list[1] if len(rv_as_list) > 1 else None
779 if args:
780 return self._flatten(args)
782 self._pickle_warning(obj)
783 return None
785 def _ref_obj_instance(self, obj: Any) -> dict[str, Any] | list[Any] | None:
786 """Reference an existing object or flatten if new"""
787 if self.unpicklable:
788 if self._mkref(obj):
789 # We've never seen this object so return its
790 # json representation.
791 return self._flatten_obj_instance(obj)
792 # We've seen this object before so place an object
793 # reference tag in the data. This avoids infinite recursion
794 # when processing cyclical objects.
795 return self._getref(obj)
796 else:
797 max_reached = self._max_reached()
798 in_cycle = _in_cycle(obj, self._objs, max_reached, False)
799 if in_cycle:
800 # A circular becomes None.
801 return None
803 self._mkref(obj)
804 return self._flatten_obj_instance(obj)
806 def _escape_key(self, k: Any) -> str:
807 return tags.JSON_KEY + encode(
808 k,
809 reset=False,
810 keys=True,
811 context=self,
812 backend=self.backend,
813 make_refs=self.make_refs,
814 )
816 def _flatten_non_string_key_value_pair(
817 self, k: Any, v: Any, data: dict[str, Any]
818 ) -> dict[str, Any]:
819 """Flatten only non-string key/value pairs"""
820 if not util._is_picklable(k, v):
821 return data
822 if self.keys and not isinstance(k, str):
823 k = self._escape_key(k)
824 data[k] = self._flatten(v)
825 return data
827 def _flatten_string_key_value_pair(
828 self, k: str, v: Any, data: dict[str, Any]
829 ) -> dict[str, Any]:
830 """Flatten string key/value pairs only."""
831 if (
832 isinstance(k, str)
833 and (k.startswith(tags.JSON_KEY) or k in tags.RESERVED)
834 and self.keys
835 ):
836 # Escape data keys colliding with the json:// prefix or a reserved
837 # wire tag; must run before _is_picklable, which drops RESERVED keys.
838 data[self._escape_key(k)] = self._flatten(v)
839 return data
840 if not util._is_picklable(k, v):
841 return data
842 if self.keys:
843 if not isinstance(k, str):
844 return data
845 else:
846 if k is None:
847 k = "null" # for compatibility with common json encoders
849 if self.numeric_keys and isinstance(k, (int, float)):
850 pass
851 elif not isinstance(k, str):
852 try:
853 k = repr(k)
854 except Exception:
855 k = str(k)
857 data[k] = self._flatten(v)
858 return data
860 def _flatten_dict_obj(
861 self,
862 obj: dict[Any, Any],
863 data: dict[Any, Any] | None = None,
864 exclude: Iterable[Any] = (),
865 ) -> dict[str, Any]:
866 """Recursively call flatten() and return json-friendly dict"""
867 if data is None:
868 data = obj.__class__()
870 # If we allow non-string keys then we have to do a two-phase
871 # encoding to ensure that the reference IDs are deterministic.
872 if self.keys:
873 # Phase 1: serialize regular objects, ignore fancy keys.
874 flatten = self._flatten_string_key_value_pair
875 for k, v in util.items(obj, exclude=exclude):
876 flatten(k, v, data)
878 # Phase 2: serialize non-string keys.
879 flatten = self._flatten_non_string_key_value_pair
880 for k, v in util.items(obj, exclude=exclude):
881 flatten(k, v, data)
882 else:
883 # If we have string keys only then we only need a single pass.
884 flatten = self._flatten_key_value_pair
885 for k, v in util.items(obj, exclude=exclude):
886 flatten(k, v, data)
888 # the collections.defaultdict protocol
889 if hasattr(obj, "default_factory") and callable(obj.default_factory):
890 factory = obj.default_factory
891 # i know that this string could be moved above the hasattr to reduce
892 # string duplication but mypy 1.18.2 complains and i don't want to use
893 # even more type: ignores
894 store_key = "default_factory"
895 if store_key in data:
896 store_key = tags.DEFAULT_FACTORY
897 value: Any
898 if util._is_type(factory):
899 # Reference the class/type
900 # in this case it's dict[str, str]
901 value = _mktyperef(factory)
902 else:
903 # The factory is not a type and could reference e.g. functions
904 # or even the object instance itself, which creates a cycle.
905 if self._mkref(factory):
906 # We've never seen this object before so pickle it in-place.
907 # Create an instance from the factory and assume that the
908 # resulting instance is a suitable exemplar.
909 value = self._flatten_obj_instance(handlers.CloneFactory(factory()))
910 else:
911 # We've seen this object before.
912 # Break the cycle by emitting a reference.
913 # in this case it's dict[str, int]
914 value = self._getref(factory)
915 data[store_key] = value
917 # Sub-classes of dict
918 if hasattr(obj, "__dict__") and self.unpicklable and obj != obj.__dict__:
919 if self._mkref(obj.__dict__):
920 dict_data = {}
921 self._flatten_dict_obj(obj.__dict__, dict_data, exclude=exclude)
922 data["__dict__"] = dict_data
923 else:
924 data["__dict__"] = self._getref(obj.__dict__)
926 return data
928 def _get_flattener(self, obj: Any) -> Callable[[Any], Any] | None:
929 if type(obj) in (list, dict):
930 if self._mkref(obj):
931 return (
932 self._list_recurse if type(obj) is list else self._flatten_dict_obj
933 )
934 else:
935 return self._getref
937 # We handle tuples and sets by encoding them in a "(tuple|set)dict"
938 elif type(obj) in (tuple, set):
939 if not self.unpicklable:
940 return self._list_recurse
941 return lambda obj: {
942 tags.TUPLE if type(obj) is tuple else tags.SET: [
943 self._flatten(v) for v in obj
944 ]
945 }
947 elif util._is_module_function(obj):
948 return self._flatten_function
950 elif util._is_object(obj):
951 return self._ref_obj_instance
953 elif util._is_type(obj):
954 return _mktyperef
956 # instance methods, lambdas, old style classes...
957 self._pickle_warning(obj)
958 return None
960 def _flatten_sequence_obj(
961 self, obj: Iterable[Any], data: dict[str, Any]
962 ) -> dict[str, Any] | list[Any]:
963 """Return a json-friendly dict for a sequence subclass."""
964 if hasattr(obj, "__dict__"):
965 self._flatten_dict_obj(obj.__dict__, data)
966 value = [self._flatten(v) for v in obj]
967 if self.unpicklable:
968 data[tags.SEQ] = value
969 else:
970 return value
971 return data