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