Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/jsonpickle/unpickler.py: 69%
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 dataclasses
8import warnings
9from collections.abc import Callable, Iterator, Sequence
10from typing import Any, TypeAlias
12from . import errors, handlers, tags, util
13from .backend import JSONBackend, json
15# class names to class objects (or sequence of classes)
16ClassesType: TypeAlias = type | dict[str, type] | Sequence[type] | None
17# handler for missing classes: either a policy name or a callback
18MissingHandler: TypeAlias = str | Callable[[str], Any]
21def decode(
22 string: str,
23 backend: JSONBackend | None = None,
24 # we get a lot of errors when typing with TypeVar
25 context: "Unpickler | None" = None,
26 keys: bool = False,
27 reset: bool = True,
28 safe: bool = True,
29 classes: ClassesType | None = None,
30 v1_decode: bool = False,
31 on_missing: MissingHandler = "ignore",
32 handle_readonly: bool = False,
33 handler_context: Any = None,
34) -> Any:
35 """Convert a JSON string into a Python object.
37 :param backend: If set to an instance of jsonpickle.backend.JSONBackend, jsonpickle
38 will use that backend for deserialization.
40 :param context: Supply a pre-built Pickler or Unpickler object to the
41 `jsonpickle.encode` and `jsonpickle.decode` machinery instead
42 of creating a new instance. The `context` represents the currently
43 active Pickler and Unpickler objects when custom handlers are
44 invoked by jsonpickle.
46 :param keys: If set to True then jsonpickle will decode non-string dictionary keys
47 into python objects via the jsonpickle protocol.
49 :param reset: Custom pickle handlers that use the `Pickler.flatten` method or
50 `jsonpickle.encode` function must call `encode` with `reset=False`
51 in order to retain object references during pickling.
52 This flag is not typically used outside of a custom handler or
53 `__getstate__` implementation.
55 :param safe: If set to ``False``, use of ``eval()`` for backwards-compatible (pre-0.7.0)
56 deserialization of repr-serialized objects is enabled. Defaults to ``True``.
57 The default value was ``False`` in jsonpickle v3 and changed to ``True`` in jsonpickle v4.
59 .. warning::
61 ``eval()`` is used when set to ``False`` and is not secure against
62 malicious inputs. You should avoid setting ``safe=False``.
64 :param classes: If set to a single class, or a sequence (list, set, tuple) of
65 classes, then the classes will be made available when constructing objects.
66 If set to a dictionary of class names to class objects, the class object
67 will be provided to jsonpickle to deserialize the class name into.
68 This can be used to give jsonpickle access to local classes that are not
69 available through the global module import scope, and the dict method can
70 be used to deserialize encoded objects into a new class. An example of using
71 this argument can be found in examples/changing_class_path.py on GitHub.
73 :param v1_decode: If set to True it enables you to decode objects serialized in
74 jsonpickle v1. Please do not attempt to re-encode the objects in the v1 format!
75 Version 2's format fixes issue #255, and allows dictionary identity to be
76 preserved through an encode/decode cycle.
78 :param on_missing: If set to 'error', it will raise an error if the class it's
79 decoding is not found. If set to 'warn', it will warn you in said case.
80 If set to a non-awaitable function, it will call said callback function
81 with the class name (a string) as the only parameter. Strings passed to
82 `on_missing` are lowercased automatically.
84 :param handle_readonly: If set to True, the Unpickler will handle objects encoded
85 with 'handle_readonly' properly. Do not set this flag for objects not encoded
86 with 'handle_readonly' set to True.
88 :param handler_context:
89 Pass custom context to a custom handler. This can be used to customize
90 behavior at runtime based off data. Defaults to ``None``. An example can
91 be found in the examples/ directory on GitHub.
93 >>> decode('"my string"') == 'my string'
94 True
95 >>> decode('36')
96 36
97 """
99 if isinstance(on_missing, str):
100 on_missing = on_missing.lower()
101 elif not util._is_function(on_missing):
102 warnings.warn(
103 "Unpickler.on_missing must be a string or a function! It will be ignored!"
104 )
106 backend = backend or json
107 is_ephemeral_context = context is None
108 context = context or Unpickler(
109 keys=keys,
110 backend=backend,
111 safe=safe,
112 v1_decode=v1_decode,
113 on_missing=on_missing,
114 handle_readonly=handle_readonly,
115 handler_context=handler_context,
116 )
117 if handler_context is not None:
118 context.handler_context = handler_context
119 data = backend.decode(string)
120 result = context.restore(data, reset=reset, classes=classes)
121 if is_ephemeral_context:
122 # Avoid holding onto references to external objects, which can
123 # prevent garbage collection from occuring.
124 context.reset()
125 return result
128def _safe_hasattr(obj: Any, attr: str) -> bool:
129 """Workaround unreliable hasattr() availability on sqlalchemy objects"""
130 try:
131 object.__getattribute__(obj, attr)
132 return True
133 except AttributeError:
134 return False
137def _is_json_key(key: Any) -> bool:
138 """Has this key a special object that has been encoded to JSON?"""
139 return isinstance(key, str) and key.startswith(tags.JSON_KEY)
142class _Proxy:
143 """Proxies are dummy objects that are later replaced by real instances
145 The `restore()` function has to solve a tricky problem when pickling
146 objects with cyclical references -- the parent instance does not yet
147 exist.
149 The problem is that `__getnewargs__()`, `__getstate__()`, custom handlers,
150 and cyclical objects graphs are allowed to reference the yet-to-be-created
151 object via the referencing machinery.
153 In other words, objects are allowed to depend on themselves for
154 construction!
156 We solve this problem by placing dummy Proxy objects into the referencing
157 machinery so that we can construct the child objects before constructing
158 the parent. Objects are initially created with Proxy attribute values
159 instead of real references.
161 We collect all objects that contain references to proxies and run
162 a final sweep over them to swap in the real instance. This is done
163 at the very end of the top-level `restore()`.
165 The `instance` attribute below is replaced with the real instance
166 after `__new__()` has been used to construct the object and is used
167 when swapping proxies with real instances.
169 """
171 def __init__(self) -> None:
172 self.instance = None
174 def get(self) -> Any:
175 return self.instance
177 def reset(self, instance: Any) -> None:
178 self.instance = instance
181class _IDProxy(_Proxy):
182 def __init__(self, objs: list[Any], index: int) -> None:
183 self._index = index
184 self._objs = objs
186 def get(self) -> Any:
187 try:
188 return self._objs[self._index]
189 except IndexError:
190 return None
193def _obj_setattr(obj: Any, attr: str, proxy: _Proxy) -> None:
194 """Use setattr to update a proxy entry"""
195 setattr(obj, attr, proxy.get())
198def _obj_setvalue(obj: Any, idx: Any, proxy: _Proxy) -> None:
199 """Use obj[key] assignments to update a proxy entry"""
200 obj[idx] = proxy.get()
203def has_tag(obj: Any, tag: str) -> bool:
204 """Helper class that tests to see if the obj is a dictionary
205 and contains a particular key/tag.
207 >>> obj = {'test': 1}
208 >>> has_tag(obj, 'test')
209 True
210 >>> has_tag(obj, 'fail')
211 False
213 >>> has_tag(42, 'fail')
214 False
216 """
217 return type(obj) is dict and tag in obj
220def getargs(obj: dict[str, Any], classes: dict[str, type] | None = None) -> Any:
221 """Return arguments suitable for __new__()"""
222 # Let saved newargs take precedence over everything
223 if has_tag(obj, tags.NEWARGSEX):
224 raise ValueError("__newargs_ex__ returns both args and kwargs")
226 if has_tag(obj, tags.NEWARGS):
227 return obj[tags.NEWARGS]
229 if has_tag(obj, tags.INITARGS):
230 return obj[tags.INITARGS]
232 try:
233 seq_list = obj[tags.SEQ]
234 obj_dict = obj[tags.OBJECT]
235 except KeyError:
236 return []
237 typeref = util.loadclass(obj_dict, classes=classes)
238 if not typeref:
239 return []
240 if hasattr(typeref, "_fields"):
241 if len(typeref._fields) == len(seq_list):
242 return seq_list
243 return []
246class _trivialclassic:
247 """
248 A trivial class that can be instantiated with no args
249 """
252def make_blank_classic(cls: type) -> Any:
253 """
254 Implement the mandated strategy for dealing with classic classes
255 which cannot be instantiated without __getinitargs__ because they
256 take parameters
257 """
258 instance = _trivialclassic()
259 instance.__class__ = cls
260 return instance
263def loadrepr(reprstr: str) -> Any:
264 """Returns an instance of the object from the object's repr() string.
265 It involves the dynamic specification of code.
267 .. warning::
269 This function is unsafe and uses `eval()`.
271 >>> obj = loadrepr('datetime/datetime.datetime.now()')
272 >>> obj.__class__.__name__
273 'datetime'
275 """
276 module, evalstr = reprstr.split("/")
277 mylocals = locals()
278 localname = module
279 if "." in localname:
280 localname = module.split(".", 1)[0]
281 mylocals[localname] = __import__(module)
282 return eval(evalstr, mylocals)
285def _loadmodule(module_str: str) -> Any | None:
286 """Returns a reference to a module.
288 >>> fn = _loadmodule('datetime/datetime.datetime.fromtimestamp')
289 >>> fn.__name__
290 'fromtimestamp'
292 """
293 module, identifier = module_str.split("/")
294 try:
295 result = __import__(module)
296 except ImportError:
297 return None
298 identifier_parts = identifier.split(".")
299 first_identifier = identifier_parts[0]
300 if first_identifier != module and not module.startswith(f"{first_identifier}."):
301 return None
302 for name in identifier_parts[1:]:
303 try:
304 result = getattr(result, name)
305 except AttributeError:
306 return None
307 return result
310def has_tag_dict(obj: Any, tag: str) -> bool:
311 """Helper class that tests to see if the obj is a dictionary
312 and contains a particular key/tag.
314 >>> obj = {'test': 1}
315 >>> has_tag(obj, 'test')
316 True
317 >>> has_tag(obj, 'fail')
318 False
320 >>> has_tag(42, 'fail')
321 False
323 """
324 return tag in obj
327def _passthrough(value: Any) -> Any:
328 """A function that returns its input as-is"""
329 return value
332class Unpickler:
333 def __init__(
334 self,
335 backend: JSONBackend | None = None,
336 keys: bool = False,
337 safe: bool = True,
338 v1_decode: bool = False,
339 on_missing: MissingHandler = "ignore",
340 handle_readonly: bool = False,
341 handler_context: Any = None,
342 ) -> None:
343 self.backend = backend or json
344 self.keys = keys
345 self.safe = safe
346 self.v1_decode = v1_decode
347 self.on_missing = on_missing
348 self.handle_readonly = handle_readonly
349 # Custom context passed through to custom handlers, see #452
350 self.handler_context = handler_context
352 self.reset()
354 def reset(self) -> None:
355 """Resets the object's internal state."""
356 # Map reference names to object instances
357 self._namedict = {}
359 # The stack of names traversed for child objects
360 self._namestack = []
362 # Map of objects to their index in the _objs list
363 self._obj_to_idx = {}
364 self._objs = []
365 self._proxies = []
367 # Extra local classes not accessible globally
368 self._classes = {}
370 def _swap_proxies(self) -> None:
371 """Replace proxies with their corresponding instances"""
372 for obj, attr, proxy, method in self._proxies:
373 method(obj, attr, proxy)
374 self._proxies = []
376 def _restore(
377 self, obj: Any, _passthrough: Callable[[Any], Any] = _passthrough
378 ) -> Any:
379 # if obj isn't in these types, neither it nor nothing in it can have a tag
380 # don't change the tuple of types to a set, it won't work with isinstance
381 if not isinstance(obj, (str, list, dict, set, tuple)):
382 restore = _passthrough
383 else:
384 restore = self._restore_tags(obj)
385 return restore(obj)
387 def restore(
388 self, obj: Any, reset: bool = True, classes: ClassesType | None = None
389 ) -> Any:
390 """Restores a flattened object to its original python state.
392 Simply returns any of the basic builtin types
394 >>> u = Unpickler()
395 >>> u.restore('hello world') == 'hello world'
396 True
397 >>> u.restore({'key': 'value'}) == {'key': 'value'}
398 True
400 """
401 if reset:
402 self.reset()
403 if classes:
404 self.register_classes(classes)
405 value = self._restore(obj)
406 if reset:
407 self._swap_proxies()
408 return value
410 def register_classes(self, classes: ClassesType) -> None:
411 """Register one or more classes
413 :param classes: sequence of classes or a single class to register
415 """
416 if isinstance(classes, (list, tuple, set)):
417 for cls in classes:
418 self.register_classes(cls)
419 elif isinstance(classes, dict):
420 self._classes.update(
421 (
422 cls if isinstance(cls, str) else util.importable_name(cls),
423 handler,
424 )
425 for cls, handler in classes.items()
426 )
427 else:
428 self._classes[util.importable_name(classes)] = classes # type: ignore[arg-type]
430 def _restore_base64(self, obj: dict[str, Any]) -> bytes:
431 try:
432 return util.b64decode(obj[tags.B64].encode("utf-8"))
433 except (AttributeError, UnicodeEncodeError):
434 return b""
436 def _restore_base85(self, obj: dict[str, Any]) -> bytes:
437 try:
438 return util.b85decode(obj[tags.B85].encode("utf-8"))
439 except (AttributeError, UnicodeEncodeError):
440 return b""
442 def _refname(self) -> str:
443 """Calculates the name of the current location in the JSON stack.
445 This is called as jsonpickle traverses the object structure to
446 create references to previously-traversed objects. This allows
447 cyclical data structures such as doubly-linked lists.
448 jsonpickle ensures that duplicate python references to the same
449 object results in only a single JSON object definition and
450 special reference tags to represent each reference.
452 >>> u = Unpickler()
453 >>> u._namestack = []
454 >>> u._refname() == '/'
455 True
456 >>> u._namestack = ['a']
457 >>> u._refname() == '/a'
458 True
459 >>> u._namestack = ['a', 'b']
460 >>> u._refname() == '/a/b'
461 True
463 """
464 return "/" + "/".join(self._namestack)
466 def _mkref(self, obj: Any) -> Any:
467 obj_id = id(obj)
468 try:
469 _ = self._obj_to_idx[obj_id]
470 except KeyError:
471 self._obj_to_idx[obj_id] = len(self._objs)
472 self._objs.append(obj)
473 # Backwards compatibility: old versions of jsonpickle
474 # produced "py/ref" references.
475 self._namedict[self._refname()] = obj
476 return obj
478 def _restore_list(self, obj: list[Any]) -> list[Any]:
479 parent = []
480 self._mkref(parent)
481 children = [self._restore(v) for v in obj]
482 parent.extend(children)
483 method = _obj_setvalue
484 proxies = [
485 (parent, idx, value, method)
486 for idx, value in enumerate(parent)
487 if isinstance(value, _Proxy)
488 ]
489 self._proxies.extend(proxies)
490 return parent
492 def _restore_iterator(self, obj: dict[str, Any]) -> Iterator[Any]:
493 try:
494 return iter(self._restore_list(obj[tags.ITERATOR]))
495 except TypeError:
496 return iter([])
498 def _swapref(self, proxy: _Proxy, instance: Any) -> None:
499 proxy_id = id(proxy)
500 instance_id = id(instance)
502 instance_index = self._obj_to_idx[proxy_id]
503 self._obj_to_idx[instance_id] = instance_index
504 del self._obj_to_idx[proxy_id]
506 self._objs[instance_index] = instance
507 self._namedict[self._refname()] = instance
509 def _restore_reduce(self, obj: dict[str, Any]) -> Any:
510 """
511 Supports restoring with all elements of __reduce__ as per pep 307.
512 Assumes that iterator items (the last two) are represented as lists
513 as per pickler implementation.
514 """
515 proxy = _Proxy()
516 self._mkref(proxy)
517 try:
518 reduce_val = list(map(self._restore, obj[tags.REDUCE]))
519 except TypeError:
520 result = []
521 proxy.reset(result)
522 self._swapref(proxy, result)
523 return result
524 if len(reduce_val) < 5:
525 reduce_val.extend([None] * (5 - len(reduce_val)))
526 f, args, state, listitems, dictitems = reduce_val
528 if f == tags.NEWOBJ or getattr(f, "__name__", "") == "__newobj__":
529 # mandated special case
530 cls = args[0]
531 if not isinstance(cls, type):
532 cls = self._restore(cls)
533 stage1 = cls.__new__(cls, *args[1:])
534 else:
535 if not callable(f):
536 result = []
537 proxy.reset(result)
538 self._swapref(proxy, result)
539 return result
540 try:
541 stage1 = f(*args)
542 except TypeError:
543 # this happens when there are missing kwargs and args don't match so we bypass
544 # __init__ since the state dict will set all attributes immediately afterwards
545 stage1 = f.__new__(f, *args)
547 if state:
548 try:
549 stage1.__setstate__(state)
550 except AttributeError:
551 # it's fine - we'll try the prescribed default methods
552 try:
553 # we can't do a straight update here because we
554 # need object identity of the state dict to be
555 # preserved so that _swap_proxies works out
556 for k, v in stage1.__dict__.items():
557 state.setdefault(k, v)
558 stage1.__dict__ = state
559 except AttributeError:
560 # next prescribed default
561 try:
562 for k, v in state.items():
563 setattr(stage1, k, v)
564 except Exception:
565 dict_state, slots_state = state
566 if dict_state:
567 stage1.__dict__.update(dict_state)
568 if slots_state:
569 for k, v in slots_state.items():
570 setattr(stage1, k, v)
572 if listitems:
573 # should be lists if not None
574 try:
575 stage1.extend(listitems)
576 except AttributeError:
577 for x in listitems:
578 stage1.append(x)
580 if dictitems:
581 for k, v in dictitems:
582 stage1.__setitem__(k, v)
584 proxy.reset(stage1)
585 self._swapref(proxy, stage1)
586 return stage1
588 def _restore_id(self, obj: dict[str, Any]) -> Any:
589 try:
590 idx = obj[tags.ID]
591 return self._objs[idx]
592 except IndexError:
593 return _IDProxy(self._objs, idx)
594 except TypeError:
595 return None
597 def _restore_type(self, obj: dict[str, Any]) -> Any:
598 typeref = util.loadclass(obj[tags.TYPE], classes=self._classes)
599 if typeref is None:
600 return obj
601 return typeref
603 def _restore_module(self, obj: dict[str, Any]) -> Any:
604 new_obj = _loadmodule(obj[tags.MODULE])
605 return self._mkref(new_obj)
607 def _restore_repr_safe(self, obj: dict[str, Any]) -> Any:
608 new_obj = _loadmodule(obj[tags.REPR])
609 return self._mkref(new_obj)
611 def _restore_repr(self, obj: dict[str, Any]) -> Any:
612 obj = loadrepr(obj[tags.REPR])
613 return self._mkref(obj)
615 def _loadfactory(self, obj: dict[str, Any]) -> Any | None:
616 default_factory = None
617 for key in (tags.DEFAULT_FACTORY, "default_factory"):
618 try:
619 default_factory = obj.pop(key)
620 break
621 except KeyError:
622 continue
623 if default_factory is None:
624 return None
625 return self._restore(default_factory)
627 def _process_missing(self, class_name: str) -> None:
628 # most common case comes first
629 if self.on_missing == "ignore":
630 pass
631 elif self.on_missing == "warn":
632 warnings.warn("Unpickler._restore_object could not find %s!" % class_name)
633 elif self.on_missing == "error":
634 raise errors.ClassNotFoundError(
635 "Unpickler.restore_object could not find %s!" % class_name
636 )
637 elif util._is_function(self.on_missing):
638 self.on_missing(class_name) # type: ignore[operator]
640 def _restore_pickled_key(self, key: str) -> Any:
641 """Restore a possibly pickled key"""
642 if _is_json_key(key):
643 key = decode(
644 key[len(tags.JSON_KEY) :],
645 backend=self.backend,
646 context=self,
647 keys=True,
648 reset=False,
649 )
650 return key
652 def _restore_key_fn(
653 self, _passthrough: Callable[[Any], Any] = _passthrough
654 ) -> Callable[[Any], Any]:
655 """Return a callable that restores keys
657 This function is responsible for restoring non-string keys
658 when we are decoding with `keys=True`.
660 """
661 # This function is called before entering a tight loop
662 # where the returned function will be called.
663 # We return a specific function after checking self.keys
664 # instead of doing so in the body of the function to
665 # avoid conditional branching inside a tight loop.
666 if self.keys:
667 restore_key = self._restore_pickled_key
668 else:
669 restore_key = _passthrough # type: ignore[assignment]
670 return restore_key
672 def _restore_from_dict(
673 self,
674 obj: dict[str, Any],
675 instance: Any,
676 ignorereserved: bool = True,
677 restore_dict_items: bool = True,
678 ) -> Any:
679 restore_key = self._restore_key_fn()
680 method = _obj_setattr
681 deferred = {}
683 for k, v in util.items(obj):
684 # ignore the reserved attribute
685 if ignorereserved and k in tags.RESERVED:
686 continue
687 if isinstance(k, (int, float)):
688 str_k = k.__str__()
689 else:
690 str_k = k
691 self._namestack.append(str_k)
692 if restore_dict_items:
693 k = restore_key(k)
694 # step into the namespace
695 value = self._restore(v)
696 else:
697 value = v
698 if util._is_noncomplex(instance) or util._is_dictionary_subclass(instance):
699 try:
700 if k == "__dict__":
701 setattr(instance, k, value)
702 else:
703 instance[k] = value
704 except TypeError:
705 # Immutable object, must be constructed in one shot
706 if k != "__dict__":
707 deferred[k] = value
708 self._namestack.pop()
709 continue
710 else:
711 if not k.startswith("__"):
712 try:
713 setattr(instance, k, value)
714 except KeyError:
715 # certain numpy objects require us to prepend a _ to the var
716 # this should go in the np handler but I think this could be
717 # useful for other code
718 setattr(instance, f"_{k}", value)
719 except dataclasses.FrozenInstanceError:
720 # issue #240
721 # i think this is the only way to set frozen dataclass attrs
722 object.__setattr__(instance, k, value)
723 except AttributeError as e:
724 # some objects raise this for read-only attributes (#422) (#478)
725 if (
726 hasattr(instance, "__slots__")
727 and not len(instance.__slots__)
728 # we have to handle this separately because of +483
729 and issubclass(instance.__class__, (int, str))
730 and self.handle_readonly
731 ):
732 continue
733 raise e
734 else:
735 setattr(instance, f"_{instance.__class__.__name__}{k}", value)
737 # This instance has an instance variable named `k` that is
738 # currently a proxy and must be replaced
739 if isinstance(value, _Proxy):
740 self._proxies.append((instance, k, value, method))
742 # step out
743 self._namestack.pop()
745 if deferred:
746 # SQLAlchemy Immutable mappings must be constructed in one shot
747 instance = instance.__class__(deferred)
749 return instance
751 def _restore_state(self, obj: dict[str, Any], instance: Any) -> Any:
752 state = self._restore(obj[tags.STATE])
753 has_slots = (
754 isinstance(state, tuple) and len(state) == 2 and isinstance(state[1], dict)
755 )
756 has_slots_and_dict = has_slots and isinstance(state[0], dict)
757 if hasattr(instance, "__setstate__"):
758 instance.__setstate__(state)
759 elif isinstance(state, dict):
760 # implements described default handling
761 # of state for object with instance dict
762 # and no slots
763 instance = self._restore_from_dict(
764 state, instance, ignorereserved=False, restore_dict_items=False
765 )
766 elif has_slots:
767 instance = self._restore_from_dict(
768 state[1], instance, ignorereserved=False, restore_dict_items=False
769 )
770 if has_slots_and_dict:
771 instance = self._restore_from_dict(
772 state[0], instance, ignorereserved=False, restore_dict_items=False
773 )
774 elif not hasattr(instance, "__getnewargs__") and not hasattr(
775 instance, "__getnewargs_ex__"
776 ):
777 # __setstate__ is not implemented so that means that the best
778 # we can do is return the result of __getstate__() rather than
779 # return an empty shell of an object.
780 # However, if there were newargs, it's not an empty shell
781 instance = state
782 return instance
784 def _restore_object_instance_variables(
785 self, obj: dict[str, Any], instance: Any
786 ) -> Any:
787 instance = self._restore_from_dict(obj, instance)
789 # Handle list and set subclasses
790 if has_tag(obj, tags.SEQ):
791 if hasattr(instance, "append"):
792 for v in obj[tags.SEQ]:
793 instance.append(self._restore(v))
794 elif hasattr(instance, "add"):
795 for v in obj[tags.SEQ]:
796 instance.add(self._restore(v))
798 if has_tag(obj, tags.STATE):
799 instance = self._restore_state(obj, instance)
801 return instance
803 def _restore_object_instance(
804 self, obj: dict[str, Any], cls: type, class_name: str = ""
805 ) -> Any:
806 # This is a placeholder proxy object which allows child objects to
807 # reference the parent object before it has been instantiated.
808 proxy = _Proxy()
809 self._mkref(proxy)
811 # An object can install itself as its own factory, so load the factory
812 # after the instance is available for referencing.
813 factory = self._loadfactory(obj)
815 if has_tag(obj, tags.NEWARGSEX):
816 args, kwargs = obj[tags.NEWARGSEX]
817 else:
818 args = getargs(obj, classes=self._classes)
819 kwargs = {}
820 if args:
821 args = self._restore(args)
822 if kwargs:
823 kwargs = self._restore(kwargs)
825 is_oldstyle = not (isinstance(cls, type) or getattr(cls, "__meta__", None))
826 try:
827 if not is_oldstyle and hasattr(cls, "__new__"):
828 # new style classes
829 if factory:
830 instance = cls.__new__(cls, factory, *args, **kwargs)
831 instance.default_factory = factory
832 else:
833 instance = cls.__new__(cls, *args, **kwargs)
834 else:
835 instance = object.__new__(cls)
836 except TypeError: # old-style classes
837 is_oldstyle = True
839 if is_oldstyle:
840 try:
841 instance = cls(*args)
842 except TypeError: # fail gracefully
843 try:
844 instance = make_blank_classic(cls)
845 except Exception: # fail gracefully
846 self._process_missing(class_name)
847 return self._mkref(obj)
849 proxy.reset(instance)
850 self._swapref(proxy, instance)
852 if isinstance(instance, tuple):
853 return instance
855 instance = self._restore_object_instance_variables(obj, instance)
857 if _safe_hasattr(instance, "default_factory") and isinstance(
858 instance.default_factory, _Proxy
859 ):
860 instance.default_factory = instance.default_factory.get()
862 return instance
864 def _restore_object(self, obj: dict[str, Any]) -> Any:
865 class_name = obj[tags.OBJECT]
866 cls = util.loadclass(class_name, classes=self._classes)
867 handler = handlers.get(cls, handlers.get(class_name)) # type: ignore[arg-type]
868 if handler is not None: # custom handler
869 proxy = _Proxy()
870 self._mkref(proxy)
871 handler_instance = handler(self)
872 instance = self._call_handler_restore(handler_instance, obj)
873 proxy.reset(instance)
874 self._swapref(proxy, instance)
875 return instance
877 if cls is None:
878 self._process_missing(class_name)
879 return self._mkref(obj)
881 return self._restore_object_instance(obj, cls, class_name)
883 def _restore_function(self, obj: dict[str, Any]) -> Any:
884 return util.loadclass(obj[tags.FUNCTION], classes=self._classes)
886 def _restore_set(self, obj: dict[str, Any]) -> set[Any]:
887 try:
888 return {self._restore(v) for v in obj[tags.SET]}
889 except TypeError:
890 return set()
892 def _restore_dict(self, obj: dict[str, Any]) -> dict[str, Any]:
893 data = {}
894 if not self.v1_decode:
895 self._mkref(data)
897 # If we are decoding dicts that can have non-string keys then we
898 # need to do a two-phase decode where the non-string keys are
899 # processed last. This ensures a deterministic order when
900 # assigning object IDs for references.
901 if self.keys:
902 # Phase 1: regular non-special keys.
903 for k, v in util.items(obj):
904 if _is_json_key(k):
905 continue
906 if isinstance(k, (int, float)):
907 str_k = k.__str__()
908 else:
909 str_k = k
910 self._namestack.append(str_k)
911 data[k] = self._restore(v)
913 self._namestack.pop()
915 # Phase 2: object keys only.
916 for k, v in util.items(obj):
917 if not _is_json_key(k):
918 continue
919 self._namestack.append(k)
921 k = self._restore_pickled_key(k)
922 data[k] = result = self._restore(v)
923 # k is currently a proxy and must be replaced
924 if isinstance(result, _Proxy):
925 self._proxies.append((data, k, result, _obj_setvalue))
927 self._namestack.pop()
928 else:
929 # No special keys, thus we don't need to restore the keys either.
930 for k, v in util.items(obj):
931 if isinstance(k, (int, float)):
932 str_k = k.__str__()
933 else:
934 str_k = k
935 self._namestack.append(str_k)
936 data[k] = result = self._restore(v)
937 if isinstance(result, _Proxy):
938 self._proxies.append((data, k, result, _obj_setvalue))
939 self._namestack.pop()
940 return data
942 def _restore_tuple(self, obj: dict[str, Any]) -> tuple[Any, ...]:
943 try:
944 return tuple(self._restore(v) for v in obj[tags.TUPLE])
945 except TypeError:
946 return ()
948 def _restore_tags(
949 self, obj: Any, _passthrough: Callable[[Any], Any] = _passthrough
950 ) -> Callable[[Any], Any]:
951 """Return the restoration function for the specified object"""
952 try:
953 if not tags.RESERVED <= set(obj) and type(obj) not in (list, dict):
954 return _passthrough
955 except TypeError:
956 pass
957 if type(obj) is dict:
958 if tags.TUPLE in obj:
959 restore = self._restore_tuple
960 elif tags.SET in obj:
961 restore = self._restore_set # type: ignore[assignment]
962 elif tags.B64 in obj:
963 restore = self._restore_base64 # type: ignore[assignment]
964 elif tags.B85 in obj:
965 restore = self._restore_base85 # type: ignore[assignment]
966 elif tags.ID in obj:
967 restore = self._restore_id
968 elif tags.ITERATOR in obj:
969 restore = self._restore_iterator # type: ignore[assignment]
970 elif tags.OBJECT in obj:
971 restore = self._restore_object
972 elif tags.TYPE in obj:
973 restore = self._restore_type
974 elif tags.REDUCE in obj:
975 restore = self._restore_reduce
976 elif tags.FUNCTION in obj:
977 restore = self._restore_function
978 elif tags.MODULE in obj:
979 restore = self._restore_module
980 elif tags.REPR in obj:
981 if self.safe:
982 restore = self._restore_repr_safe
983 else:
984 restore = self._restore_repr
985 else:
986 restore = self._restore_dict # type: ignore[assignment]
987 elif type(obj) is list:
988 restore = self._restore_list # type: ignore[assignment]
989 else:
990 restore = _passthrough # type: ignore[assignment]
991 return restore
993 def _call_handler_restore(
994 self, handler: handlers.BaseHandler, obj: dict[str, Any]
995 ) -> Any:
996 kwargs: dict[str, Any] = {}
997 if (
998 self.handler_context is not None
999 and handlers.handler_accepts_handler_context(handler.restore)
1000 ):
1001 kwargs["handler_context"] = self.handler_context
1002 return handler.restore(obj, **kwargs)