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

521 statements  

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 

11 

12from . import errors, handlers, tags, util 

13from .backend import json 

14 

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] 

19 

20 

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. 

34 

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. 

40 

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. 

44 

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. 

50 

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. 

54 

55 .. warning:: 

56 

57 ``eval()`` is used when set to ``False`` and is not secure against 

58 malicious inputs. You should avoid setting ``safe=False``. 

59 

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. 

68 

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. 

74 

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. 

78 

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. 

83 

84 >>> decode('"my string"') == 'my string' 

85 True 

86 >>> decode('36') 

87 36 

88 """ 

89 

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 ) 

96 

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 

114 

115 

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 

123 

124 

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) 

128 

129 

130class _Proxy: 

131 """Proxies are dummy objects that are later replaced by real instances 

132 

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. 

136 

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. 

140 

141 In other words, objects are allowed to depend on themselves for 

142 construction! 

143 

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. 

148 

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()`. 

152 

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. 

156 

157 """ 

158 

159 def __init__(self) -> None: 

160 self.instance = None 

161 

162 def get(self) -> Any: 

163 return self.instance 

164 

165 def reset(self, instance: Any) -> None: 

166 self.instance = instance 

167 

168 

169class _IDProxy(_Proxy): 

170 def __init__(self, objs: list[Any], index: int) -> None: 

171 self._index = index 

172 self._objs = objs 

173 

174 def get(self) -> Any: 

175 try: 

176 return self._objs[self._index] 

177 except IndexError: 

178 return None 

179 

180 

181def _obj_setattr(obj: Any, attr: str, proxy: _Proxy) -> None: 

182 """Use setattr to update a proxy entry""" 

183 setattr(obj, attr, proxy.get()) 

184 

185 

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() 

189 

190 

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. 

194 

195 >>> obj = {'test': 1} 

196 >>> has_tag(obj, 'test') 

197 True 

198 >>> has_tag(obj, 'fail') 

199 False 

200 

201 >>> has_tag(42, 'fail') 

202 False 

203 

204 """ 

205 return type(obj) is dict and tag in obj 

206 

207 

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") 

213 

214 if has_tag(obj, tags.NEWARGS): 

215 return obj[tags.NEWARGS] 

216 

217 if has_tag(obj, tags.INITARGS): 

218 return obj[tags.INITARGS] 

219 

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 [] 

231 

232 

233class _trivialclassic: 

234 """ 

235 A trivial class that can be instantiated with no args 

236 """ 

237 

238 

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 

248 

249 

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. 

253 

254 .. warning:: 

255 

256 This function is unsafe and uses `eval()`. 

257 

258 >>> obj = loadrepr('datetime/datetime.datetime.now()') 

259 >>> obj.__class__.__name__ 

260 'datetime' 

261 

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) 

270 

271 

272def _loadmodule(module_str: str) -> Any | None: 

273 """Returns a reference to a module. 

274 

275 >>> fn = _loadmodule('datetime/datetime.datetime.fromtimestamp') 

276 >>> fn.__name__ 

277 'fromtimestamp' 

278 

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 

295 

296 

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. 

300 

301 >>> obj = {'test': 1} 

302 >>> has_tag(obj, 'test') 

303 True 

304 >>> has_tag(obj, 'fail') 

305 False 

306 

307 >>> has_tag(42, 'fail') 

308 False 

309 

310 """ 

311 return tag in obj 

312 

313 

314def _passthrough(value: Any) -> Any: 

315 """A function that returns its input as-is""" 

316 return value 

317 

318 

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 

335 

336 self.reset() 

337 

338 def reset(self) -> None: 

339 """Resets the object's internal state.""" 

340 # Map reference names to object instances 

341 self._namedict = {} 

342 

343 # The stack of names traversed for child objects 

344 self._namestack = [] 

345 

346 # Map of objects to their index in the _objs list 

347 self._obj_to_idx = {} 

348 self._objs = [] 

349 self._proxies = [] 

350 

351 # Extra local classes not accessible globally 

352 self._classes = {} 

353 

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 = [] 

359 

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) 

370 

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. 

375 

376 Simply returns any of the basic builtin types 

377 

378 >>> u = Unpickler() 

379 >>> u.restore('hello world') == 'hello world' 

380 True 

381 >>> u.restore({'key': 'value'}) == {'key': 'value'} 

382 True 

383 

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 

393 

394 def register_classes(self, classes: ClassesType) -> None: 

395 """Register one or more classes 

396 

397 :param classes: sequence of classes or a single class to register 

398 

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] 

413 

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"" 

419 

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"" 

425 

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) 

433 

434 def _refname(self) -> str: 

435 """Calculates the name of the current location in the JSON stack. 

436 

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. 

443 

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 

454 

455 """ 

456 return "/" + "/".join(self._namestack) 

457 

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 

469 

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 

483 

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([]) 

489 

490 def _swapref(self, proxy: _Proxy, instance: Any) -> None: 

491 proxy_id = id(proxy) 

492 instance_id = id(instance) 

493 

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] 

497 

498 self._objs[instance_index] = instance 

499 self._namedict[self._refname()] = instance 

500 

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 

519 

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) 

538 

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) 

566 

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) 

574 

575 if dictitems: 

576 for k, v in dictitems: 

577 stage1.__setitem__(k, v) 

578 

579 proxy.reset(stage1) 

580 self._swapref(proxy, stage1) 

581 return stage1 

582 

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 

591 

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 

597 

598 def _restore_module(self, obj: dict[str, Any]) -> Any: 

599 new_obj = _loadmodule(obj[tags.MODULE]) 

600 return self._mkref(new_obj) 

601 

602 def _restore_repr_safe(self, obj: dict[str, Any]) -> Any: 

603 new_obj = _loadmodule(obj[tags.REPR]) 

604 return self._mkref(new_obj) 

605 

606 def _restore_repr(self, obj: dict[str, Any]) -> Any: 

607 obj = loadrepr(obj[tags.REPR]) 

608 return self._mkref(obj) 

609 

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) 

621 

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] 

634 

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 

645 

646 def _restore_key_fn( 

647 self, _passthrough: Callable[[Any], Any] = _passthrough 

648 ) -> Callable[[Any], Any]: 

649 """Return a callable that restores keys 

650 

651 This function is responsible for restoring non-string keys 

652 when we are decoding with `keys=True`. 

653 

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 

665 

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 = {} 

676 

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) 

730 

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)) 

735 

736 # step out 

737 self._namestack.pop() 

738 

739 if deferred: 

740 # SQLAlchemy Immutable mappings must be constructed in one shot 

741 instance = instance.__class__(deferred) 

742 

743 return instance 

744 

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 

777 

778 def _restore_object_instance_variables( 

779 self, obj: dict[str, Any], instance: Any 

780 ) -> Any: 

781 instance = self._restore_from_dict(obj, instance) 

782 

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)) 

791 

792 if has_tag(obj, tags.STATE): 

793 instance = self._restore_state(obj, instance) 

794 

795 return instance 

796 

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) 

804 

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) 

808 

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) 

818 

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 

832 

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) 

842 

843 proxy.reset(instance) 

844 self._swapref(proxy, instance) 

845 

846 if isinstance(instance, tuple): 

847 return instance 

848 

849 instance = self._restore_object_instance_variables(obj, instance) 

850 

851 if _safe_hasattr(instance, "default_factory") and isinstance( 

852 instance.default_factory, _Proxy 

853 ): 

854 instance.default_factory = instance.default_factory.get() 

855 

856 return instance 

857 

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 

870 

871 if cls is None: 

872 self._process_missing(class_name) 

873 return self._mkref(obj) 

874 

875 return self._restore_object_instance(obj, cls, class_name) 

876 

877 def _restore_function(self, obj: dict[str, Any]) -> Any: 

878 return util.loadclass(obj[tags.FUNCTION], classes=self._classes) 

879 

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() 

885 

886 def _restore_dict(self, obj: dict[str, Any]) -> dict[str, Any]: 

887 data = {} 

888 self._mkref(data) 

889 

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)) 

907 

908 self._namestack.pop() 

909 

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) 

915 

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)) 

921 

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 

936 

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 () 

942 

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 

989 

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)