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 # reduce_val is either an absolute "module.attr" path to an object to
619 # store in obj's place, or (as in pickle) the bare name of a module-level
620 # global whose value is obj, in which case we must find the module ourselves
621 top, _, path = reduce_val.partition(".")
622 target = (
623 util._getattr_path(sys.modules[top], path)
624 if path and sys.modules.get(top) is not None
625 else util._MISSING
626 )
627 if target is obj:
628 return {tags.MODULE: f"{top}/{reduce_val}"}
629 if target is not util._MISSING:
630 return self._flatten(target)
631 module = util._find_global_module(obj, reduce_val)
632 if module is not None:
633 return {tags.MODULE: f"{module}/{module}.{reduce_val}"}
635 elif reduce_val:
636 # at this point, reduce_val should be some kind of iterable
637 # pad out to len 6, for pickle protocol 5 support
638 rv_as_list = list(reduce_val)
639 insufficiency = 6 - len(rv_as_list)
640 if insufficiency:
641 rv_as_list += [None] * insufficiency
643 if getattr(rv_as_list[0], "__name__", "") == "__newobj__":
644 rv_as_list[0] = tags.NEWOBJ
646 _, args, state, _, _ = rv_as_list[:5]
648 # check that getstate/setstate is sane
649 if not (
650 state
651 and has_own_getstate
652 and not hasattr(obj, "__setstate__")
653 and not isinstance(obj, dict)
654 ):
655 # turn iterators to iterables for convenient serialization
656 if rv_as_list[3]:
657 rv_as_list[3] = tuple(rv_as_list[3])
659 if rv_as_list[4]:
660 rv_as_list[4] = tuple(rv_as_list[4])
662 reduce_args = list(map(self._flatten, rv_as_list))
663 last_index = len(reduce_args) - 1
664 while last_index >= 2 and reduce_args[last_index] is None:
665 last_index -= 1
666 data[tags.REDUCE] = reduce_args[: last_index + 1]
668 return data
670 if has_class and not isinstance(obj, types.ModuleType):
671 if self.unpicklable:
672 data[tags.OBJECT] = class_name
674 if has_getnewargs_ex:
675 data[tags.NEWARGSEX] = [
676 self._flatten(arg) for arg in obj.__getnewargs_ex__()
677 ]
679 if has_getnewargs and not has_getnewargs_ex:
680 data[tags.NEWARGS] = self._flatten(obj.__getnewargs__())
682 if has_getinitargs:
683 data[tags.INITARGS] = self._flatten(obj.__getinitargs__())
685 if has_own_getstate:
686 try:
687 state = obj.__getstate__()
688 except TypeError:
689 # Has getstate but it cannot be called, e.g. file descriptors
690 # in Python3
691 self._pickle_warning(obj)
692 return None
693 else:
694 if exclude and isinstance(state, dict):
695 state = {k: v for k, v in util.items(state, exclude=exclude)}
696 if state:
697 return self._getstate(state, data)
699 if isinstance(obj, types.ModuleType):
700 if self.unpicklable:
701 data[tags.MODULE] = f"{obj.__name__}/{obj.__name__}"
702 else:
703 # TODO: this causes a mypy assignment error, figure out
704 # if it's actually an error or a false alarm
705 data = str(obj) # type: ignore[assignment]
706 return data
708 if util._is_dictionary_subclass(obj):
709 self._flatten_dict_obj(obj, data, exclude=exclude)
710 return data
712 if util._is_sequence_subclass(obj):
713 return self._flatten_sequence_obj(obj, data)
715 if util._is_iterator(obj):
716 # force list in python 3
717 data[tags.ITERATOR] = list(map(self._flatten, obj))
718 return data
720 if has_dict:
721 # Support objects that subclasses list and set
722 if util._is_sequence_subclass(obj):
723 return self._flatten_sequence_obj(obj, data)
725 # hack for zope persistent objects; this unghostifies the object
726 getattr(obj, "_", None)
727 return self._flatten_dict_obj(obj.__dict__, data, exclude=exclude)
729 if has_slots:
730 return self._flatten_newstyle_with_slots(obj, data, exclude=exclude)
732 # catchall return for data created above without a return
733 # (e.g. __getnewargs__ is not supposed to be the end of the story)
734 if data:
735 return data
737 # Objects whose state is only reachable through __reduce__/__reduce_ex__
738 # (e.g. datetime.timedelta) have no __dict__, __slots__ or __getstate__
739 # for the branches above to read, so nothing has been produced and they
740 # would otherwise become null. Emit a lossy view built from the reduce
741 # output instead: its state if present, else the constructor args. The
742 # string form and the listitems/dictitems slots (append/update-based
743 # reconstruction) are not represented and keep the previous behaviour.
744 if not self.unpicklable:
745 reduce_val = self._reduce(obj, has_reduce, has_reduce_ex)
746 if reduce_val is not None and not isinstance(reduce_val, str):
747 # reduce tuple: (callable, args, state, listitems, dictitems)
748 rv_as_list = list(reduce_val)
749 state = rv_as_list[2] if len(rv_as_list) > 2 else None
750 if state:
751 return self._flatten(state)
752 args = rv_as_list[1] if len(rv_as_list) > 1 else None
753 if args:
754 return self._flatten(args)
756 self._pickle_warning(obj)
757 return None
759 def _ref_obj_instance(self, obj: Any) -> dict[str, Any] | list[Any] | None:
760 """Reference an existing object or flatten if new"""
761 if self.unpicklable:
762 if self._mkref(obj):
763 # We've never seen this object so return its
764 # json representation.
765 return self._flatten_obj_instance(obj)
766 # We've seen this object before so place an object
767 # reference tag in the data. This avoids infinite recursion
768 # when processing cyclical objects.
769 return self._getref(obj)
770 else:
771 max_reached = self._max_reached()
772 in_cycle = _in_cycle(obj, self._objs, max_reached, False)
773 if in_cycle:
774 # A circular becomes None.
775 return None
777 self._mkref(obj)
778 return self._flatten_obj_instance(obj)
780 def _escape_key(self, k: Any) -> str:
781 return tags.JSON_KEY + encode(
782 k,
783 reset=False,
784 keys=True,
785 context=self,
786 make_refs=self.make_refs,
787 )
789 def _flatten_non_string_key_value_pair(
790 self, k: Any, v: Any, data: dict[str, Any]
791 ) -> dict[str, Any]:
792 """Flatten only non-string key/value pairs"""
793 if not util._is_picklable(k, v):
794 return data
795 if self.keys and not isinstance(k, str):
796 k = self._escape_key(k)
797 data[k] = self._flatten(v)
798 return data
800 def _flatten_string_key_value_pair(
801 self, k: str, v: Any, data: dict[str, Any]
802 ) -> dict[str, Any]:
803 """Flatten string key/value pairs only."""
804 if (
805 isinstance(k, str)
806 and (k.startswith(tags.JSON_KEY) or k in tags.RESERVED)
807 and self.keys
808 ):
809 # Escape data keys colliding with the json:// prefix or a reserved
810 # wire tag; must run before _is_picklable, which drops RESERVED keys.
811 data[self._escape_key(k)] = self._flatten(v)
812 return data
813 if not util._is_picklable(k, v):
814 return data
815 if self.keys:
816 if not isinstance(k, str):
817 return data
818 else:
819 if k is None:
820 k = "null" # for compatibility with common json encoders
822 if not isinstance(k, str):
823 try:
824 k = repr(k)
825 except Exception: # ruff: ignore[BLE001]
826 k = str(k)
828 data[k] = self._flatten(v)
829 return data
831 def _flatten_dict_obj(
832 self,
833 obj: dict[Any, Any],
834 data: dict[Any, Any] | None = None,
835 exclude: Iterable[Any] = (),
836 ) -> dict[str, Any]:
837 """Recursively call flatten() and return json-friendly dict"""
838 if data is None:
839 data = obj.__class__()
841 # If we allow non-string keys then we have to do a two-phase
842 # encoding to ensure that the reference IDs are deterministic.
843 if self.keys:
844 # Phase 1: serialize regular objects, ignore fancy keys.
845 flatten = self._flatten_string_key_value_pair
846 for k, v in util.items(obj, exclude=exclude):
847 flatten(k, v, data)
849 # Phase 2: serialize non-string keys.
850 flatten = self._flatten_non_string_key_value_pair
851 for k, v in util.items(obj, exclude=exclude):
852 flatten(k, v, data)
853 else:
854 # If we have string keys only then we only need a single pass.
855 flatten = self._flatten_key_value_pair
856 for k, v in util.items(obj, exclude=exclude):
857 flatten(k, v, data)
859 # the collections.defaultdict protocol
860 if hasattr(obj, "default_factory") and callable(obj.default_factory):
861 factory = obj.default_factory
862 # i know that this string could be moved above the hasattr to reduce
863 # string duplication but mypy 1.18.2 complains and i don't want to use
864 # even more type: ignores
865 store_key = "default_factory"
866 if store_key in data:
867 store_key = tags.DEFAULT_FACTORY
868 value: Any
869 if util._is_type(factory):
870 # Reference the class/type
871 # in this case it's dict[str, str]
872 value = _mktyperef(factory)
873 else:
874 # The factory is not a type and could reference e.g. functions
875 # or even the object instance itself, which creates a cycle.
876 if self._mkref(factory):
877 # We've never seen this object before so pickle it in-place.
878 # Create an instance from the factory and assume that the
879 # resulting instance is a suitable exemplar.
880 value = self._flatten_obj_instance(handlers.CloneFactory(factory()))
881 else:
882 # We've seen this object before.
883 # Break the cycle by emitting a reference.
884 # in this case it's dict[str, int]
885 value = self._getref(factory)
886 data[store_key] = value
888 # Sub-classes of dict
889 if hasattr(obj, "__dict__") and self.unpicklable and obj != obj.__dict__:
890 if self._mkref(obj.__dict__):
891 dict_data = {}
892 self._flatten_dict_obj(obj.__dict__, dict_data, exclude=exclude)
893 data["__dict__"] = dict_data
894 else:
895 data["__dict__"] = self._getref(obj.__dict__)
897 return data
899 def _get_flattener(self, obj: Any) -> Callable[[Any], Any] | None:
900 if type(obj) in (list, dict):
901 if self._mkref(obj):
902 return (
903 self._list_recurse if type(obj) is list else self._flatten_dict_obj
904 )
905 else:
906 return self._getref
908 # We handle tuples and sets by encoding them in a "(tuple|set)dict"
909 elif type(obj) in (tuple, set):
910 if not self.unpicklable:
911 return self._list_recurse
912 return lambda obj: {
913 tags.TUPLE if type(obj) is tuple else tags.SET: [
914 self._flatten(v) for v in obj
915 ]
916 }
918 elif util._is_module_function(obj):
919 return self._flatten_function
921 elif util._is_object(obj):
922 return self._ref_obj_instance
924 elif util._is_type(obj):
925 return _mktyperef
927 # instance methods, lambdas, old style classes...
928 self._pickle_warning(obj)
929 return None
931 def _flatten_sequence_obj(
932 self, obj: Iterable[Any], data: dict[str, Any]
933 ) -> dict[str, Any] | list[Any]:
934 """Return a json-friendly dict for a sequence subclass."""
935 if hasattr(obj, "__dict__"):
936 self._flatten_dict_obj(obj.__dict__, data)
937 value = [self._flatten(v) for v in obj]
938 if self.unpicklable:
939 data[tags.SEQ] = value
940 else:
941 return value
942 return data