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