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 of objects to their index in the _objs list
341 self._obj_to_idx = {}
342 self._objs = []
343 self._proxies = []
345 # Extra local classes not accessible globally
346 self._classes = {}
348 def _swap_proxies(self) -> None:
349 """Replace proxies with their corresponding instances"""
350 for obj, attr, proxy, method in self._proxies:
351 method(obj, attr, proxy)
352 self._proxies = []
354 def _restore(
355 self, obj: Any, _passthrough: Callable[[Any], Any] = _passthrough
356 ) -> Any:
357 # if obj isn't in these types, neither it nor nothing in it can have a tag
358 # don't change the tuple of types to a set, it won't work with isinstance
359 if not isinstance(obj, (str, list, dict, set, tuple)):
360 restore = _passthrough
361 else:
362 restore = self._restore_tags(obj)
363 return restore(obj)
365 def restore(
366 self, obj: Any, reset: bool = True, classes: ClassesType | None = None
367 ) -> Any:
368 """Restores a flattened object to its original python state.
370 Simply returns any of the basic builtin types
372 >>> u = Unpickler()
373 >>> u.restore('hello world') == 'hello world'
374 True
375 >>> u.restore({'key': 'value'}) == {'key': 'value'}
376 True
378 """
379 if reset:
380 self.reset()
381 if classes:
382 self.register_classes(classes)
383 value = self._restore(obj)
384 if reset:
385 self._swap_proxies()
386 return value
388 def register_classes(self, classes: ClassesType) -> None:
389 """Register one or more classes
391 :param classes: sequence of classes or a single class to register
393 """
394 if isinstance(classes, (list, tuple, set)):
395 for cls in classes:
396 self.register_classes(cls)
397 elif isinstance(classes, dict):
398 self._classes.update(
399 (
400 cls if isinstance(cls, str) else util.importable_name(cls),
401 handler,
402 )
403 for cls, handler in classes.items()
404 )
405 else:
406 self._classes[util.importable_name(classes)] = classes # type: ignore[arg-type]
408 def _restore_base64(self, obj: dict[str, Any]) -> bytes:
409 try:
410 return util.b64decode(obj[tags.B64].encode("utf-8"))
411 except (AttributeError, UnicodeEncodeError) as error:
412 warnings.warn(f"jsonpickle could not decode base64 payload: {error}")
413 return b""
415 def _restore_base85(self, obj: dict[str, Any]) -> bytes:
416 try:
417 return util.b85decode(obj[tags.B85].encode("utf-8"))
418 except (AttributeError, UnicodeEncodeError) as error:
419 warnings.warn(f"jsonpickle could not decode base85 payload: {error}")
420 return b""
422 def _restore_bytearray(self, obj: dict[str, Any]) -> bytearray:
423 payload = obj[tags.BYTEARRAY]
424 if tags.B85 in payload:
425 data = self._restore_base85(payload)
426 else:
427 data = self._restore_base64(payload)
428 return bytearray(data)
430 def _restore_memoryview(self, obj: dict[str, Any]) -> memoryview:
431 payload = obj[tags.MEMORYVIEW]
432 if tags.B85 in payload:
433 data = self._restore_base85(payload)
434 else:
435 data = self._restore_base64(payload)
436 return memoryview(data)
438 def _mkref(self, obj: Any) -> Any:
439 obj_id = id(obj)
440 try:
441 _ = self._obj_to_idx[obj_id]
442 except KeyError:
443 self._obj_to_idx[obj_id] = len(self._objs)
444 self._objs.append(obj)
445 return obj
447 def _restore_list(self, obj: list[Any]) -> list[Any]:
448 parent = []
449 self._mkref(parent)
450 children = [self._restore(v) for v in obj]
451 parent.extend(children)
452 method = _obj_setvalue
453 proxies = [
454 (parent, idx, value, method)
455 for idx, value in enumerate(parent)
456 if isinstance(value, _Proxy)
457 ]
458 self._proxies.extend(proxies)
459 return parent
461 def _restore_iterator(self, obj: dict[str, Any]) -> Iterator[Any]:
462 try:
463 return iter(self._restore_list(obj[tags.ITERATOR]))
464 except TypeError:
465 return iter([])
467 def _swapref(self, proxy: _Proxy, instance: Any) -> None:
468 proxy_id = id(proxy)
469 instance_id = id(instance)
471 instance_index = self._obj_to_idx[proxy_id]
472 self._obj_to_idx[instance_id] = instance_index
473 del self._obj_to_idx[proxy_id]
475 self._objs[instance_index] = instance
477 def _restore_reduce(self, obj: dict[str, Any]) -> Any:
478 """
479 Supports restoring with all elements of __reduce__ as per pep 307.
480 Assumes that iterator items (the last two) are represented as lists
481 as per pickler implementation.
482 """
483 proxy = _Proxy()
484 self._mkref(proxy)
485 try:
486 reduce_val = list(map(self._restore, obj[tags.REDUCE]))
487 except TypeError:
488 result = []
489 proxy.reset(result)
490 self._swapref(proxy, result)
491 return result
492 if len(reduce_val) < 6:
493 reduce_val.extend([None] * (6 - len(reduce_val)))
494 f, args, state, listitems, dictitems, state_setter = reduce_val
496 if f == tags.NEWOBJ or getattr(f, "__name__", "") == "__newobj__":
497 # mandated special case
498 cls = args[0]
499 if not isinstance(cls, type):
500 cls = self._restore(cls)
501 stage1 = cls.__new__(cls, *args[1:])
502 else:
503 if not callable(f):
504 result = []
505 proxy.reset(result)
506 self._swapref(proxy, result)
507 return result
508 try:
509 stage1 = f(*args)
510 except TypeError:
511 # this happens when there are missing kwargs and args don't match so we bypass
512 # __init__ since the state dict will set all attributes immediately afterwards
513 stage1 = f.__new__(f, *args)
515 if state and state_setter is None:
516 try:
517 stage1.__setstate__(state)
518 except AttributeError:
519 # it's fine - we'll try the prescribed default methods
520 try:
521 # we can't do a straight update here because we
522 # need object identity of the state dict to be
523 # preserved so that _swap_proxies works out
524 for k, v in stage1.__dict__.items():
525 state.setdefault(k, v)
526 stage1.__dict__ = state
527 except AttributeError:
528 # next prescribed default
529 try:
530 for k, v in state.items():
531 setattr(stage1, k, v)
532 except Exception: # ruff: ignore[BLE001]
533 dict_state, slots_state = state
534 if dict_state:
535 stage1.__dict__.update(dict_state)
536 if slots_state:
537 for k, v in slots_state.items():
538 setattr(stage1, k, v)
539 elif state:
540 # pickle protocol 5's state_setter takes priority over __setstate__
541 state_setter(stage1, state)
543 if listitems:
544 # should be lists if not None
545 try:
546 stage1.extend(listitems)
547 except AttributeError:
548 for x in listitems:
549 stage1.append(x)
551 if dictitems:
552 for k, v in dictitems:
553 stage1.__setitem__(k, v)
555 proxy.reset(stage1)
556 self._swapref(proxy, stage1)
557 return stage1
559 def _restore_id(self, obj: dict[str, Any]) -> Any:
560 try:
561 idx = obj[tags.ID]
562 return self._objs[idx]
563 except IndexError:
564 return _IDProxy(self._objs, idx)
565 except TypeError:
566 return None
568 def _restore_type(self, obj: dict[str, Any]) -> Any:
569 typeref = util.loadclass(obj[tags.TYPE], classes=self._classes)
570 if typeref is None:
571 return obj
572 return typeref
574 def _restore_module(self, obj: dict[str, Any]) -> Any:
575 new_obj = _loadmodule(obj[tags.MODULE])
576 return self._mkref(new_obj)
578 def _restore_repr_safe(self, obj: dict[str, Any]) -> Any:
579 new_obj = _loadmodule(obj[tags.REPR])
580 return self._mkref(new_obj)
582 def _restore_repr(self, obj: dict[str, Any]) -> Any:
583 obj = loadrepr(obj[tags.REPR])
584 return self._mkref(obj)
586 def _loadfactory(self, obj: dict[str, Any]) -> Any | None:
587 default_factory = None
588 for key in (tags.DEFAULT_FACTORY, "default_factory"):
589 try:
590 default_factory = obj.pop(key)
591 break
592 except KeyError:
593 continue
594 if default_factory is None:
595 return None
596 return self._restore(default_factory)
598 def _process_missing(self, class_name: str) -> None:
599 # most common case comes first
600 if self.on_missing == "ignore":
601 pass
602 elif self.on_missing == "warn":
603 warnings.warn(f"Unpickler._restore_object could not find {class_name}!")
604 elif self.on_missing == "error":
605 raise errors.ClassNotFoundError(
606 f"Unpickler.restore_object could not find {class_name}!"
607 )
608 elif util._is_function(self.on_missing):
609 self.on_missing(class_name) # type: ignore[operator]
611 def _restore_pickled_key(self, key: str) -> Any:
612 """Restore a possibly pickled key"""
613 if _is_json_key(key):
614 key = decode(
615 key[len(tags.JSON_KEY) :],
616 context=self,
617 keys=True,
618 reset=False,
619 )
620 return key
622 def _restore_key_fn(
623 self, _passthrough: Callable[[Any], Any] = _passthrough
624 ) -> Callable[[Any], Any]:
625 """Return a callable that restores keys
627 This function is responsible for restoring non-string keys
628 when we are decoding with `keys=True`.
630 """
631 # This function is called before entering a tight loop
632 # where the returned function will be called.
633 # We return a specific function after checking self.keys
634 # instead of doing so in the body of the function to
635 # avoid conditional branching inside a tight loop.
636 if self.keys:
637 restore_key = self._restore_pickled_key
638 else:
639 restore_key = _passthrough # type: ignore[assignment]
640 return restore_key
642 def _restore_from_dict(
643 self,
644 obj: dict[str, Any],
645 instance: Any,
646 ignorereserved: bool = True,
647 restore_dict_items: bool = True,
648 ) -> Any:
649 restore_key = self._restore_key_fn()
650 method = _obj_setattr
651 deferred = {}
653 for k, v in util.items(obj):
654 # ignore the reserved attribute
655 if ignorereserved and k in tags.RESERVED:
656 continue
657 if restore_dict_items:
658 k = restore_key(k)
659 value = self._restore(v)
660 else:
661 value = v
662 if util._is_noncomplex(instance) or util._is_dictionary_subclass(instance):
663 try:
664 if k == "__dict__":
665 setattr(instance, k, value)
666 else:
667 instance[k] = value
668 except TypeError:
669 # Immutable object, must be constructed in one shot
670 if k != "__dict__":
671 deferred[k] = value
672 continue
673 else:
674 if not isinstance(k, str):
675 continue
676 if not k.startswith("__"):
677 try:
678 setattr(instance, k, value)
679 except KeyError:
680 # certain numpy objects require us to prepend a _ to the var
681 # this should go in the np handler but I think this could be
682 # useful for other code
683 setattr(instance, f"_{k}", value)
684 except dataclasses.FrozenInstanceError:
685 # issue #240
686 # i think this is the only way to set frozen dataclass attrs
687 object.__setattr__(instance, k, value)
688 except AttributeError:
689 # some objects raise this for read-only attributes (#422) (#478)
690 if (
691 hasattr(instance, "__slots__")
692 and not len(instance.__slots__)
693 # we have to handle this separately because of +483
694 and issubclass(instance.__class__, (int, str))
695 and self.handle_readonly
696 ):
697 continue
698 raise
699 else:
700 setattr(instance, f"_{instance.__class__.__name__}{k}", value)
702 # This instance has an instance variable named `k` that is
703 # currently a proxy and must be replaced
704 if isinstance(value, _Proxy):
705 self._proxies.append((instance, k, value, method))
707 if deferred:
708 # SQLAlchemy Immutable mappings must be constructed in one shot
709 instance = instance.__class__(deferred)
711 return instance
713 def _restore_state(self, obj: dict[str, Any], instance: Any) -> Any:
714 state = self._restore(obj[tags.STATE])
715 has_slots = (
716 isinstance(state, tuple) and len(state) == 2 and isinstance(state[1], dict)
717 )
718 has_slots_and_dict = has_slots and isinstance(state[0], dict)
719 if hasattr(instance, "__setstate__"):
720 instance.__setstate__(state)
721 elif isinstance(state, dict):
722 # implements described default handling
723 # of state for object with instance dict
724 # and no slots
725 instance = self._restore_from_dict(
726 state, instance, ignorereserved=False, restore_dict_items=False
727 )
728 elif has_slots:
729 instance = self._restore_from_dict(
730 state[1], instance, ignorereserved=False, restore_dict_items=False
731 )
732 if has_slots_and_dict:
733 instance = self._restore_from_dict(
734 state[0], instance, ignorereserved=False, restore_dict_items=False
735 )
736 elif not hasattr(instance, "__getnewargs__") and not hasattr(
737 instance, "__getnewargs_ex__"
738 ):
739 # __setstate__ is not implemented so that means that the best
740 # we can do is return the result of __getstate__() rather than
741 # return an empty shell of an object.
742 # However, if there were newargs, it's not an empty shell
743 instance = state
744 return instance
746 def _restore_object_instance_variables(
747 self, obj: dict[str, Any], instance: Any
748 ) -> Any:
749 instance = self._restore_from_dict(obj, instance)
751 # Handle list and set subclasses
752 if has_tag(obj, tags.SEQ):
753 if hasattr(instance, "append"):
754 for v in obj[tags.SEQ]:
755 instance.append(self._restore(v))
756 elif hasattr(instance, "add"):
757 for v in obj[tags.SEQ]:
758 instance.add(self._restore(v))
760 if has_tag(obj, tags.STATE):
761 instance = self._restore_state(obj, instance)
763 return instance
765 def _restore_object_instance(
766 self, obj: dict[str, Any], cls: type, class_name: str = ""
767 ) -> Any:
768 # This is a placeholder proxy object which allows child objects to
769 # reference the parent object before it has been instantiated.
770 proxy = _Proxy()
771 self._mkref(proxy)
773 # An object can install itself as its own factory, so load the factory
774 # after the instance is available for referencing.
775 factory = self._loadfactory(obj)
777 if has_tag(obj, tags.NEWARGSEX):
778 args, kwargs = obj[tags.NEWARGSEX]
779 else:
780 args = getargs(obj, classes=self._classes)
781 kwargs = {}
782 if args:
783 args = self._restore(args)
784 if kwargs:
785 kwargs = self._restore(kwargs)
787 is_oldstyle = not (isinstance(cls, type) or getattr(cls, "__meta__", None))
788 try:
789 if not is_oldstyle and hasattr(cls, "__new__"):
790 # new style classes
791 if factory:
792 instance = cls.__new__(cls, factory, *args, **kwargs)
793 instance.default_factory = factory
794 else:
795 instance = cls.__new__(cls, *args, **kwargs)
796 else:
797 instance = object.__new__(cls)
798 except TypeError: # old-style classes
799 is_oldstyle = True
801 if is_oldstyle:
802 try:
803 instance = cls(*args)
804 except TypeError: # fail gracefully
805 try:
806 instance = make_blank_classic(cls)
807 except Exception: # ruff: ignore[BLE001]
808 self._process_missing(class_name)
809 return self._mkref(obj)
811 proxy.reset(instance)
812 self._swapref(proxy, instance)
814 if isinstance(instance, tuple):
815 return instance
817 instance = self._restore_object_instance_variables(obj, instance)
819 if _safe_hasattr(instance, "default_factory") and isinstance(
820 instance.default_factory, _Proxy
821 ):
822 instance.default_factory = instance.default_factory.get()
824 return instance
826 def _restore_object(self, obj: dict[str, Any]) -> Any:
827 class_name = obj[tags.OBJECT]
828 cls = util.loadclass(class_name, classes=self._classes)
829 handler = handlers.get(cls, handlers.get(class_name)) # type: ignore[arg-type]
830 if handler is not None: # custom handler
831 proxy = _Proxy()
832 self._mkref(proxy)
833 handler_instance = handler(self)
834 instance = self._call_handler_restore(handler_instance, obj)
835 proxy.reset(instance)
836 self._swapref(proxy, instance)
837 return instance
839 if cls is None:
840 self._process_missing(class_name)
841 return self._mkref(obj)
843 return self._restore_object_instance(obj, cls, class_name)
845 def _restore_function(self, obj: dict[str, Any]) -> Any:
846 return util.loadclass(obj[tags.FUNCTION], classes=self._classes)
848 def _restore_set(self, obj: dict[str, Any]) -> set[Any]:
849 try:
850 return {self._restore(v) for v in obj[tags.SET]}
851 except TypeError:
852 return set()
854 def _restore_dict(self, obj: dict[str, Any]) -> dict[str, Any]:
855 data = {}
856 self._mkref(data)
858 # If we are decoding dicts that can have non-string keys then we
859 # need to do a two-phase decode where the non-string keys are
860 # processed last. This ensures a deterministic order when
861 # assigning object IDs for references.
862 if self.keys:
863 # Phase 1: regular non-special keys.
864 for k, v in util.items(obj):
865 if _is_json_key(k):
866 continue
867 data[k] = result = self._restore(v)
868 if isinstance(result, _Proxy):
869 self._proxies.append((data, k, result, _obj_setvalue))
871 # Phase 2: object keys only.
872 for k, v in util.items(obj):
873 if not _is_json_key(k):
874 continue
875 restored_key = self._restore_pickled_key(k)
876 result = self._restore(v)
877 try:
878 data[restored_key] = result
879 except TypeError: # fail gracefully
880 # The encoder can never emit an unhashable key, so if this
881 # is triggered then we're dealing with hand-crafted input.
882 # Keep the raw json:// key rather than failing the whole decode
883 data[k] = result
884 else:
885 k = restored_key
886 # k is currently a proxy and must be replaced
887 if isinstance(result, _Proxy):
888 self._proxies.append((data, k, result, _obj_setvalue))
889 else:
890 # No special keys, thus we don't need to restore the keys either.
891 for k, v in util.items(obj):
892 data[k] = result = self._restore(v)
893 if isinstance(result, _Proxy):
894 self._proxies.append((data, k, result, _obj_setvalue))
895 return data
897 def _restore_tuple(self, obj: dict[str, Any]) -> tuple[Any, ...]:
898 try:
899 return tuple(self._restore(v) for v in obj[tags.TUPLE])
900 except TypeError:
901 return ()
903 def _restore_tags(
904 self, obj: Any, _passthrough: Callable[[Any], Any] = _passthrough
905 ) -> Callable[[Any], Any]:
906 """Return the restoration function for the specified object"""
907 try:
908 if not tags.RESERVED <= set(obj) and type(obj) not in (list, dict):
909 return _passthrough
910 except TypeError:
911 pass
912 if type(obj) is dict:
913 if tags.TUPLE in obj:
914 restore = self._restore_tuple
915 elif tags.SET in obj:
916 restore = self._restore_set # type: ignore[assignment]
917 elif tags.B64 in obj:
918 restore = self._restore_base64 # type: ignore[assignment]
919 elif tags.B85 in obj:
920 restore = self._restore_base85 # type: ignore[assignment]
921 elif tags.BYTEARRAY in obj:
922 restore = self._restore_bytearray # type: ignore[assignment]
923 elif tags.MEMORYVIEW in obj:
924 restore = self._restore_memoryview # type: ignore[assignment]
925 elif tags.ID in obj:
926 restore = self._restore_id
927 elif tags.ITERATOR in obj:
928 restore = self._restore_iterator # type: ignore[assignment]
929 elif tags.OBJECT in obj:
930 restore = self._restore_object
931 elif tags.TYPE in obj:
932 restore = self._restore_type
933 elif tags.REDUCE in obj:
934 restore = self._restore_reduce
935 elif tags.FUNCTION in obj:
936 restore = self._restore_function
937 elif tags.MODULE in obj:
938 restore = self._restore_module
939 elif tags.REPR in obj:
940 if self.safe:
941 restore = self._restore_repr_safe
942 else:
943 restore = self._restore_repr
944 else:
945 restore = self._restore_dict # type: ignore[assignment]
946 elif type(obj) is list:
947 restore = self._restore_list # type: ignore[assignment]
948 else:
949 restore = _passthrough # type: ignore[assignment]
950 return restore
952 def _call_handler_restore(
953 self, handler: handlers.BaseHandler, obj: dict[str, Any]
954 ) -> Any:
955 kwargs: dict[str, Any] = {}
956 if (
957 self.handler_context is not None
958 and handlers.handler_accepts_handler_context(handler.restore)
959 ):
960 kwargs["handler_context"] = self.handler_context
961 return handler.restore(obj, **kwargs)