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))}
368 #########################################
370 self._push()
371 return self._pop(self._flatten_obj(obj))
373 def _max_reached(self) -> bool:
374 return self._depth == self._max_depth
376 def _pickle_warning(self, obj: Any) -> None:
377 if self.warn:
378 warnings.warn(f"jsonpickle cannot pickle {obj}: replaced with None")
380 def _flatten_obj(self, obj: Any) -> Any:
381 self._seen.append(obj)
383 max_reached = self._max_reached()
385 try:
386 in_cycle = _in_cycle(obj, self._objs, max_reached, self.make_refs)
387 flatten_func: Callable[[Any], str] | None
388 if in_cycle:
389 # break the cycle
390 flatten_func = repr
391 else:
392 flatten_func = self._get_flattener(obj)
394 if flatten_func is None:
395 self._pickle_warning(obj)
396 return None
398 return flatten_func(obj)
400 except (KeyboardInterrupt, SystemExit):
401 raise
402 except Exception as e:
403 if self.fail_safe is None:
404 raise
405 else:
406 return self.fail_safe(e)
408 def _list_recurse(self, obj: Iterable[Any]) -> list[Any]:
409 return [self._flatten(v) for v in obj]
411 def _flatten_function(self, obj: Callable[..., Any]) -> dict[str, str] | None:
412 if self.unpicklable:
413 data = {tags.FUNCTION: util.importable_name(obj)}
414 else:
415 data = None
417 return data
419 def _getstate(self, obj: Any, data: dict[str, Any]) -> dict[str, Any]:
420 state = self._flatten(obj)
421 if self.unpicklable:
422 data[tags.STATE] = state
423 else:
424 data = state
425 return data
427 def _flatten_key_value_pair(
428 self, k: Any, v: Any, data: dict[str | Any, Any]
429 ) -> dict[str | Any, Any]:
430 """Flatten a key/value pair into the passed-in dictionary."""
431 if not util._is_picklable(k, v):
432 return data
433 # TODO: use inspect.getmembers_static on 3.11+ because it avoids dynamic
434 # attribute lookups
435 if (
436 self.handle_readonly
437 and k in {attr for attr, val in inspect.getmembers(self._original_object)}
438 and util._is_readonly(self._original_object, k, v)
439 ):
440 return data
442 if k is None:
443 k = "null" # for compatibility with common json encoders
445 if not isinstance(k, str):
446 try:
447 k = repr(k)
448 except Exception: # ruff: ignore[BLE001]
449 k = str(k)
451 data[k] = self._flatten(v)
452 return data
454 def _call_handler_flatten(
455 self, handler: handlers.BaseHandler, obj: Any, data: dict[str, Any]
456 ) -> Any:
457 kwargs: dict[str, Any] = {}
458 if (
459 self.handler_context is not None
460 and handlers.handler_accepts_handler_context(handler.flatten)
461 ):
462 kwargs["handler_context"] = self.handler_context
463 return handler.flatten(obj, data, **kwargs)
465 def _flatten_obj_attrs(
466 self,
467 obj: Any,
468 attrs: Iterable[str],
469 data: dict[str, Any],
470 exclude: Iterable[str] = (),
471 ) -> bool:
472 flatten = self._flatten_key_value_pair
473 ok = False
474 exclude = set(exclude)
475 for k in attrs:
476 if k in exclude:
477 continue
478 try:
479 if not k.startswith("__"):
480 value = getattr(obj, k)
481 else:
482 value = getattr(obj, f"_{obj.__class__.__name__}{k}")
483 flatten(k, value, data)
484 except AttributeError:
485 # The attribute may have been deleted
486 continue
487 ok = True
488 return ok
490 def _flatten_properties(
491 self,
492 obj: Any,
493 data: dict[str, Any],
494 allslots: Iterable[Sequence[str]] | None = None,
495 ) -> dict[str, Any]:
496 if allslots is None:
497 # setting a list as a default argument can lead to some weird errors
498 allslots = []
500 # convert to set in case there are a lot of slots
501 allslots_set = set(itertools.chain.from_iterable(allslots))
503 # i don't like lambdas
504 def valid_property(x: tuple[str, Any]) -> bool:
505 return not x[0].startswith("__") and x[0] not in allslots_set
507 properties = [
508 x[0] for x in inspect.getmembers(obj.__class__) if valid_property(x)
509 ]
511 properties_dict = {}
512 for p_name in properties:
513 p_val = getattr(obj, p_name)
514 if util._is_not_class(p_val):
515 properties_dict[p_name] = p_val
516 else:
517 properties_dict[p_name] = self._flatten(p_val)
519 data[tags.PROPERTY] = properties_dict
521 return data
523 def _flatten_newstyle_with_slots(
524 self,
525 obj: Any,
526 data: dict[str, Any],
527 exclude: Iterable[str] = (),
528 ) -> dict[str, Any]:
529 """Return a json-friendly dict for new-style objects with __slots__."""
530 allslots = [
531 _wrap_string_slot(getattr(cls, "__slots__", ()))
532 for cls in obj.__class__.mro()
533 ]
535 # add properties to the attribute list
536 if self.include_properties:
537 data = self._flatten_properties(obj, data, allslots)
539 if not self._flatten_obj_attrs(obj, chain(*allslots), data, exclude):
540 attrs = [
541 x for x in dir(obj) if not x.startswith("__") and not x.endswith("__")
542 ]
543 self._flatten_obj_attrs(obj, attrs, data, exclude)
545 return data
547 def _reduce(self, obj: Any, has_reduce: bool, has_reduce_ex: bool) -> Any:
548 """Return the object's __reduce__/__reduce_ex__ output, or None.
550 Many builtin types raise TypeError from these; treat that as
551 "no reduce available" rather than letting it propagate.
552 """
553 try:
554 if has_reduce and not has_reduce_ex:
555 return obj.__reduce__()
556 if has_reduce_ex:
557 return obj.__reduce_ex__(util.PICKLE_PROTOCOL)
558 except TypeError:
559 pass
560 return None
562 def _flatten_obj_instance(
563 self, obj: Any
564 ) -> dict[str, Any] | list[Any] | Any | None:
565 """Recursively flatten an instance and return a json-friendly dict"""
566 # we're generally not bothering to annotate parts that aren't part of the public API
567 # but this annotation alone saves us 3 mypy "errors"
568 data: dict[str, Any] = {}
569 has_class = hasattr(obj, "__class__")
570 has_dict = hasattr(obj, "__dict__")
571 has_slots = not has_dict and hasattr(obj, "__slots__")
572 has_getnewargs = util.has_method(obj, "__getnewargs__")
573 has_getnewargs_ex = util.has_method(obj, "__getnewargs_ex__")
574 has_getinitargs = util.has_method(obj, "__getinitargs__")
575 has_reduce, has_reduce_ex = util.has_reduce(obj)
576 exclude = set(getattr(obj, "_jsonpickle_exclude", ()))
578 # Support objects with __getstate__(); this ensures that
579 # both __setstate__() and __getstate__() are implemented
580 has_own_getstate = hasattr(type(obj), "__getstate__") and type(
581 obj
582 ).__getstate__ is not getattr(object, "__getstate__", None)
583 # not using has_method since __getstate__() is handled separately below
584 # Note: on Python 3.11+, all objects have __getstate__.
586 if has_class:
587 cls = obj.__class__
588 else:
589 cls = type(obj)
591 # Check for a custom handler
592 class_name = util.importable_name(cls)
593 handler = handlers.get(cls, handlers.get(class_name)) # type: ignore[arg-type]
594 if handler is not None:
595 if self.unpicklable:
596 data[tags.OBJECT] = class_name
597 handler_instance = handler(self)
598 result = self._call_handler_flatten(handler_instance, obj, data)
599 if result is None:
600 self._pickle_warning(obj)
601 return result
603 if self.include_properties:
604 data = self._flatten_properties(obj, data)
606 if self.unpicklable:
607 # test for a reduce implementation, and redirect before
608 # doing anything else if that is what reduce requests
609 reduce_val = self._reduce(obj, has_reduce, has_reduce_ex)
611 if reduce_val and isinstance(reduce_val, str):
612 try:
613 varpath = iter(reduce_val.split("."))
614 # curmod will be transformed by the
615 # loop into the value to pickle
616 curmod = sys.modules[next(varpath)]
617 for modname in varpath:
618 curmod = getattr(curmod, modname)
619 # replace obj with value retrieved
620 return self._flatten(curmod)
621 except KeyError:
622 # well, we can't do anything with that, so we ignore it
623 pass
625 elif reduce_val:
626 # at this point, reduce_val should be some kind of iterable
627 # pad out to len 6, for pickle protocol 5 support
628 rv_as_list = list(reduce_val)
629 insufficiency = 6 - len(rv_as_list)
630 if insufficiency:
631 rv_as_list += [None] * insufficiency
633 if getattr(rv_as_list[0], "__name__", "") == "__newobj__":
634 rv_as_list[0] = tags.NEWOBJ
636 _, args, state, _, _ = rv_as_list[:5]
638 # check that getstate/setstate is sane
639 if not (
640 state
641 and has_own_getstate
642 and not hasattr(obj, "__setstate__")
643 and not isinstance(obj, dict)
644 ):
645 # turn iterators to iterables for convenient serialization
646 if rv_as_list[3]:
647 rv_as_list[3] = tuple(rv_as_list[3])
649 if rv_as_list[4]:
650 rv_as_list[4] = tuple(rv_as_list[4])
652 reduce_args = list(map(self._flatten, rv_as_list))
653 last_index = len(reduce_args) - 1
654 while last_index >= 2 and reduce_args[last_index] is None:
655 last_index -= 1
656 data[tags.REDUCE] = reduce_args[: last_index + 1]
658 return data
660 if has_class and not isinstance(obj, types.ModuleType):
661 if self.unpicklable:
662 data[tags.OBJECT] = class_name
664 if has_getnewargs_ex:
665 data[tags.NEWARGSEX] = [
666 self._flatten(arg) for arg in obj.__getnewargs_ex__()
667 ]
669 if has_getnewargs and not has_getnewargs_ex:
670 data[tags.NEWARGS] = self._flatten(obj.__getnewargs__())
672 if has_getinitargs:
673 data[tags.INITARGS] = self._flatten(obj.__getinitargs__())
675 if has_own_getstate:
676 try:
677 state = obj.__getstate__()
678 except TypeError:
679 # Has getstate but it cannot be called, e.g. file descriptors
680 # in Python3
681 self._pickle_warning(obj)
682 return None
683 else:
684 if exclude and isinstance(state, dict):
685 state = {k: v for k, v in util.items(state, exclude=exclude)}
686 if state:
687 return self._getstate(state, data)
689 if isinstance(obj, types.ModuleType):
690 if self.unpicklable:
691 data[tags.MODULE] = f"{obj.__name__}/{obj.__name__}"
692 else:
693 # TODO: this causes a mypy assignment error, figure out
694 # if it's actually an error or a false alarm
695 data = str(obj) # type: ignore[assignment]
696 return data
698 if util._is_dictionary_subclass(obj):
699 self._flatten_dict_obj(obj, data, exclude=exclude)
700 return data
702 if util._is_sequence_subclass(obj):
703 return self._flatten_sequence_obj(obj, data)
705 if util._is_iterator(obj):
706 # force list in python 3
707 data[tags.ITERATOR] = list(map(self._flatten, obj))
708 return data
710 if has_dict:
711 # Support objects that subclasses list and set
712 if util._is_sequence_subclass(obj):
713 return self._flatten_sequence_obj(obj, data)
715 # hack for zope persistent objects; this unghostifies the object
716 getattr(obj, "_", None)
717 return self._flatten_dict_obj(obj.__dict__, data, exclude=exclude)
719 if has_slots:
720 return self._flatten_newstyle_with_slots(obj, data, exclude=exclude)
722 # catchall return for data created above without a return
723 # (e.g. __getnewargs__ is not supposed to be the end of the story)
724 if data:
725 return data
727 # Objects whose state is only reachable through __reduce__/__reduce_ex__
728 # (e.g. datetime.timedelta) have no __dict__, __slots__ or __getstate__
729 # for the branches above to read, so nothing has been produced and they
730 # would otherwise become null. Emit a lossy view built from the reduce
731 # output instead: its state if present, else the constructor args. The
732 # string form and the listitems/dictitems slots (append/update-based
733 # reconstruction) are not represented and keep the previous behaviour.
734 if not self.unpicklable:
735 reduce_val = self._reduce(obj, has_reduce, has_reduce_ex)
736 if reduce_val is not None and not isinstance(reduce_val, str):
737 # reduce tuple: (callable, args, state, listitems, dictitems)
738 rv_as_list = list(reduce_val)
739 state = rv_as_list[2] if len(rv_as_list) > 2 else None
740 if state:
741 return self._flatten(state)
742 args = rv_as_list[1] if len(rv_as_list) > 1 else None
743 if args:
744 return self._flatten(args)
746 self._pickle_warning(obj)
747 return None
749 def _ref_obj_instance(self, obj: Any) -> dict[str, Any] | list[Any] | None:
750 """Reference an existing object or flatten if new"""
751 if self.unpicklable:
752 if self._mkref(obj):
753 # We've never seen this object so return its
754 # json representation.
755 return self._flatten_obj_instance(obj)
756 # We've seen this object before so place an object
757 # reference tag in the data. This avoids infinite recursion
758 # when processing cyclical objects.
759 return self._getref(obj)
760 else:
761 max_reached = self._max_reached()
762 in_cycle = _in_cycle(obj, self._objs, max_reached, False)
763 if in_cycle:
764 # A circular becomes None.
765 return None
767 self._mkref(obj)
768 return self._flatten_obj_instance(obj)
770 def _escape_key(self, k: Any) -> str:
771 return tags.JSON_KEY + encode(
772 k,
773 reset=False,
774 keys=True,
775 context=self,
776 make_refs=self.make_refs,
777 )
779 def _flatten_non_string_key_value_pair(
780 self, k: Any, v: Any, data: dict[str, Any]
781 ) -> dict[str, Any]:
782 """Flatten only non-string key/value pairs"""
783 if not util._is_picklable(k, v):
784 return data
785 if self.keys and not isinstance(k, str):
786 k = self._escape_key(k)
787 data[k] = self._flatten(v)
788 return data
790 def _flatten_string_key_value_pair(
791 self, k: str, v: Any, data: dict[str, Any]
792 ) -> dict[str, Any]:
793 """Flatten string key/value pairs only."""
794 if (
795 isinstance(k, str)
796 and (k.startswith(tags.JSON_KEY) or k in tags.RESERVED)
797 and self.keys
798 ):
799 # Escape data keys colliding with the json:// prefix or a reserved
800 # wire tag; must run before _is_picklable, which drops RESERVED keys.
801 data[self._escape_key(k)] = self._flatten(v)
802 return data
803 if not util._is_picklable(k, v):
804 return data
805 if self.keys:
806 if not isinstance(k, str):
807 return data
808 else:
809 if k is None:
810 k = "null" # for compatibility with common json encoders
812 if not isinstance(k, str):
813 try:
814 k = repr(k)
815 except Exception: # ruff: ignore[BLE001]
816 k = str(k)
818 data[k] = self._flatten(v)
819 return data
821 def _flatten_dict_obj(
822 self,
823 obj: dict[Any, Any],
824 data: dict[Any, Any] | None = None,
825 exclude: Iterable[Any] = (),
826 ) -> dict[str, Any]:
827 """Recursively call flatten() and return json-friendly dict"""
828 if data is None:
829 data = obj.__class__()
831 # If we allow non-string keys then we have to do a two-phase
832 # encoding to ensure that the reference IDs are deterministic.
833 if self.keys:
834 # Phase 1: serialize regular objects, ignore fancy keys.
835 flatten = self._flatten_string_key_value_pair
836 for k, v in util.items(obj, exclude=exclude):
837 flatten(k, v, data)
839 # Phase 2: serialize non-string keys.
840 flatten = self._flatten_non_string_key_value_pair
841 for k, v in util.items(obj, exclude=exclude):
842 flatten(k, v, data)
843 else:
844 # If we have string keys only then we only need a single pass.
845 flatten = self._flatten_key_value_pair
846 for k, v in util.items(obj, exclude=exclude):
847 flatten(k, v, data)
849 # the collections.defaultdict protocol
850 if hasattr(obj, "default_factory") and callable(obj.default_factory):
851 factory = obj.default_factory
852 # i know that this string could be moved above the hasattr to reduce
853 # string duplication but mypy 1.18.2 complains and i don't want to use
854 # even more type: ignores
855 store_key = "default_factory"
856 if store_key in data:
857 store_key = tags.DEFAULT_FACTORY
858 value: Any
859 if util._is_type(factory):
860 # Reference the class/type
861 # in this case it's dict[str, str]
862 value = _mktyperef(factory)
863 else:
864 # The factory is not a type and could reference e.g. functions
865 # or even the object instance itself, which creates a cycle.
866 if self._mkref(factory):
867 # We've never seen this object before so pickle it in-place.
868 # Create an instance from the factory and assume that the
869 # resulting instance is a suitable exemplar.
870 value = self._flatten_obj_instance(handlers.CloneFactory(factory()))
871 else:
872 # We've seen this object before.
873 # Break the cycle by emitting a reference.
874 # in this case it's dict[str, int]
875 value = self._getref(factory)
876 data[store_key] = value
878 # Sub-classes of dict
879 if hasattr(obj, "__dict__") and self.unpicklable and obj != obj.__dict__:
880 if self._mkref(obj.__dict__):
881 dict_data = {}
882 self._flatten_dict_obj(obj.__dict__, dict_data, exclude=exclude)
883 data["__dict__"] = dict_data
884 else:
885 data["__dict__"] = self._getref(obj.__dict__)
887 return data
889 def _get_flattener(self, obj: Any) -> Callable[[Any], Any] | None:
890 if type(obj) in (list, dict):
891 if self._mkref(obj):
892 return (
893 self._list_recurse if type(obj) is list else self._flatten_dict_obj
894 )
895 else:
896 return self._getref
898 # We handle tuples and sets by encoding them in a "(tuple|set)dict"
899 elif type(obj) in (tuple, set):
900 if not self.unpicklable:
901 return self._list_recurse
902 return lambda obj: {
903 tags.TUPLE if type(obj) is tuple else tags.SET: [
904 self._flatten(v) for v in obj
905 ]
906 }
908 elif util._is_module_function(obj):
909 return self._flatten_function
911 elif util._is_object(obj):
912 return self._ref_obj_instance
914 elif util._is_type(obj):
915 return _mktyperef
917 # instance methods, lambdas, old style classes...
918 self._pickle_warning(obj)
919 return None
921 def _flatten_sequence_obj(
922 self, obj: Iterable[Any], data: dict[str, Any]
923 ) -> dict[str, Any] | list[Any]:
924 """Return a json-friendly dict for a sequence subclass."""
925 if hasattr(obj, "__dict__"):
926 self._flatten_dict_obj(obj.__dict__, data)
927 value = [self._flatten(v) for v in obj]
928 if self.unpicklable:
929 data[tags.SEQ] = value
930 else:
931 return value
932 return data