Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/jsonpickle/pickler.py: 12%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

422 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 inspect 

8import itertools 

9import sys 

10import types 

11import warnings 

12from collections.abc import Callable, Iterable, Sequence 

13from itertools import chain 

14from typing import Any 

15 

16from . import handlers, tags, util 

17from .backend import json 

18 

19 

20def encode( 

21 value: Any, 

22 unpicklable: bool = True, 

23 make_refs: bool = True, 

24 keys: bool = True, 

25 max_depth: int | None = None, 

26 reset: bool = True, 

27 warn: bool = False, 

28 context: "Pickler | None" = None, 

29 use_base85: bool = False, 

30 fail_safe: Callable[[Exception], Any] | None = None, 

31 indent: int | None = None, 

32 separators: Any | None = None, 

33 include_properties: bool = False, 

34 handle_readonly: bool = False, 

35 handler_context: Any = None, 

36) -> str: 

37 """Return a JSON formatted representation of value, a Python object. 

38 

39 :param unpicklable: If set to ``False`` then the output will not contain the 

40 information necessary to turn the JSON data back into Python objects, 

41 but a simpler JSON stream is produced. It's recommended to set this 

42 parameter to ``False`` when your code does not rely on two objects 

43 having the same ``id()`` value, and when it is sufficient for those two 

44 objects to be equal by ``==``, such as when serializing sklearn 

45 instances. If you experience (de)serialization being incorrect when you 

46 use numpy, pandas, or sklearn handlers, this should be set to ``False``. 

47 If you want the output to not include the dtype for numpy arrays, add:: 

48 

49 jsonpickle.register( 

50 numpy.generic, UnpicklableNumpyGenericHandler, base=True 

51 ) 

52 

53 before your pickling code. 

54 :param make_refs: If set to False jsonpickle's referencing support is 

55 disabled. Objects that are id()-identical won't be preserved across 

56 encode()/decode(), but the resulting JSON stream will be conceptually 

57 simpler. jsonpickle detects cyclical objects and will break the cycle 

58 by calling repr() instead of recursing when make_refs is set False. 

59 :param keys: If set to True, the default, then jsonpickle will encode 

60 non-string dictionary keys instead of coercing them into strings via 

61 `repr()`. 

62 :param max_depth: If set to a non-negative integer then jsonpickle will 

63 not recurse deeper than 'max_depth' steps into the object. Anything 

64 deeper than 'max_depth' is represented using a Python repr() of the 

65 object. 

66 :param reset: Custom pickle handlers that use the `Pickler.flatten` method or 

67 `jsonpickle.encode` function must call `encode` with `reset=False` 

68 in order to retain object references during pickling. 

69 This flag is not typically used outside of a custom handler or 

70 `__getstate__` implementation. 

71 :param warn: If set to True then jsonpickle will warn when it 

72 returns None for an object which it cannot pickle 

73 (e.g. file descriptors). 

74 :param context: Supply a pre-built Pickler or Unpickler object to the 

75 `jsonpickle.encode` and `jsonpickle.decode` machinery instead 

76 of creating a new instance. The `context` represents the currently 

77 active Pickler and Unpickler objects when custom handlers are 

78 invoked by jsonpickle. 

79 :param use_base85: 

80 If possible, use base85 to encode binary data. Base85 bloats binary data 

81 by 1/4 as opposed to base64, which expands it by 1/3. This argument is 

82 ignored on Python 2 because it doesn't support it. 

83 :param fail_safe: If set to a function exceptions are ignored when pickling 

84 and if a exception happens the function is called and the return value 

85 is used as the value for the object that caused the error 

86 :param indent: When `indent` is a non-negative integer, then JSON array 

87 elements and object members will be pretty-printed with that indent 

88 level. An indent level of 0 will only insert newlines. ``None`` is 

89 the most compact representation. Since the default item separator is 

90 ``(', ', ': ')``, the output might include trailing whitespace when 

91 ``indent`` is specified. You can use ``separators=(',', ': ')`` to 

92 avoid this. This value is passed directly to the active JSON backend 

93 library and not used by jsonpickle directly. 

94 :param separators: 

95 If ``separators`` is an ``(item_separator, dict_separator)`` tuple 

96 then it will be used instead of the default ``(', ', ': ')`` 

97 separators. ``(',', ':')`` is the most compact JSON representation. 

98 This value is passed directly to the active JSON backend library and 

99 not used by jsonpickle directly. 

100 :param include_properties: 

101 Include the names and values of class properties in the generated json. 

102 Properties are unpickled properly regardless of this setting, this is 

103 meant to be used if processing the json outside of Python. Certain types 

104 such as sets will not pickle due to not having a native-json equivalent. 

105 Defaults to ``False``. 

106 :param handle_readonly: 

107 Handle objects with readonly methods, such as Django's SafeString. This 

108 basically prevents jsonpickle from raising an exception for such objects. 

109 You MUST set ``handle_readonly=True`` for the decoding if you encode with 

110 this flag set to ``True``. 

111 :param handler_context: 

112 Pass custom context to a custom handler. This can be used to customize 

113 behavior at runtime based off data. Defaults to ``None``. An example can 

114 be found in the examples/ directory on GitHub. 

115 

116 >>> encode('my string') == '"my string"' 

117 True 

118 >>> encode(36) == '36' 

119 True 

120 >>> encode({'foo': True}) == '{"foo": true}' 

121 True 

122 >>> encode({'foo': [1, 2, [3, 4]]}, max_depth=1) 

123 '{"foo": "[1, 2, [3, 4]]"}' 

124 

125 """ 

126 

127 context = context or Pickler( 

128 unpicklable=unpicklable, 

129 make_refs=make_refs, 

130 keys=keys, 

131 max_depth=max_depth, 

132 warn=warn, 

133 use_base85=use_base85, 

134 fail_safe=fail_safe, 

135 include_properties=include_properties, 

136 handle_readonly=handle_readonly, 

137 original_object=value, 

138 handler_context=handler_context, 

139 ) 

140 if handler_context is not None: 

141 context.handler_context = handler_context 

142 return json.encode( 

143 context.flatten(value, reset=reset), indent=indent, separators=separators 

144 ) 

145 

146 

147def _in_cycle( 

148 obj: Any, objs: dict[int, int], max_reached: bool, make_refs: bool 

149) -> bool: 

150 """Detect cyclic structures that would lead to infinite recursion""" 

151 return ( 

152 (max_reached or (not make_refs and id(obj) in objs)) 

153 and not util._is_primitive(obj) 

154 and not util._is_enum(obj) 

155 ) 

156 

157 

158def _mktyperef(obj: type) -> dict[str, str]: 

159 """Return a typeref dictionary 

160 

161 >>> _mktyperef(AssertionError) == {'py/type': 'builtins.AssertionError'} 

162 True 

163 

164 """ 

165 return {tags.TYPE: util.importable_name(obj)} 

166 

167 

168def _wrap_string_slot(string: str | Sequence[str]) -> Sequence[str]: 

169 """Converts __slots__ = 'a' into __slots__ = ('a',)""" 

170 if isinstance(string, str): 

171 return (string,) 

172 return string 

173 

174 

175class Pickler: 

176 def __init__( 

177 self, 

178 unpicklable: bool = True, 

179 make_refs: bool = True, 

180 max_depth: int | None = None, 

181 keys: bool = True, 

182 warn: bool = False, 

183 use_base85: bool = False, 

184 fail_safe: Callable[[Exception], Any] | None = None, 

185 include_properties: bool = False, 

186 handle_readonly: bool = False, 

187 original_object: Any | None = None, 

188 handler_context: Any = None, 

189 ) -> None: 

190 self.unpicklable = unpicklable 

191 self.make_refs = make_refs 

192 self.backend = json 

193 self.keys = keys 

194 self.warn = warn 

195 self.use_base85 = use_base85 

196 # The current recursion depth 

197 self._depth = -1 

198 # The maximal recursion depth 

199 self._max_depth = max_depth 

200 # Maps id(obj) to reference IDs 

201 self._objs = {} 

202 # Avoids garbage collection 

203 self._seen = [] 

204 # A cache of objects that have already been flattened. 

205 self._flattened = {} 

206 # Used for util._is_readonly, see +483 

207 self.handle_readonly = handle_readonly 

208 # Custom context passed through to custom handlers, see #452 

209 self.handler_context = handler_context 

210 

211 if self.use_base85: 

212 self._bytes_tag = tags.B85 

213 self._bytes_encoder = util.b85encode 

214 else: 

215 self._bytes_tag = tags.B64 

216 self._bytes_encoder = util.b64encode 

217 

218 # ignore exceptions 

219 self.fail_safe = fail_safe 

220 self.include_properties = include_properties 

221 

222 self._original_object = original_object 

223 

224 def _determine_sort_keys(self) -> bool: 

225 for _, options in getattr(self.backend, "_encoder_options", {}).values(): 

226 if options.get("sort_keys", False): 

227 # the user has set one of the backends to sort keys 

228 return True 

229 return False 

230 

231 def _sort_attrs(self, obj: Any) -> Any: 

232 if hasattr(obj, "__slots__") and self.warn: 

233 # Slots are read-only by default, the only way 

234 # to sort keys is to do it in a subclass 

235 # and that would require calling the init function 

236 # of the parent again. That could cause issues 

237 # so we refuse to handle it. 

238 raise TypeError( 

239 "Objects with __slots__ cannot have their keys reliably sorted by " 

240 "jsonpickle! Please sort the keys in the __slots__ definition instead." 

241 ) 

242 # Somehow some classes don't have slots or dict 

243 elif hasattr(obj, "__dict__"): 

244 try: 

245 obj.__dict__ = dict(sorted(obj.__dict__.items())) 

246 except (TypeError, AttributeError): 

247 # Can't set attributes of builtin/extension type 

248 pass 

249 return obj 

250 

251 def reset(self) -> None: 

252 self._objs = {} 

253 self._depth = -1 

254 self._seen = [] 

255 self._flattened = {} 

256 

257 def _push(self) -> None: 

258 """Steps down one level in the namespace.""" 

259 self._depth += 1 

260 

261 def _pop(self, value: Any) -> Any: 

262 """Step up one level in the namespace and return the value. 

263 If we're at the root, reset the pickler's state. 

264 """ 

265 self._depth -= 1 

266 if self._depth == -1: 

267 self.reset() 

268 return value 

269 

270 def _log_ref(self, obj: Any) -> bool: 

271 """ 

272 Log a reference to an in-memory object. 

273 Return True if this object is new and was assigned 

274 a new ID. Otherwise return False. 

275 """ 

276 objid = id(obj) 

277 is_new = objid not in self._objs 

278 if is_new: 

279 new_id = len(self._objs) 

280 self._objs[objid] = new_id 

281 return is_new 

282 

283 def _mkref(self, obj: Any) -> bool: 

284 """ 

285 Log a reference to an in-memory object, and return 

286 if that object should be considered newly logged. 

287 """ 

288 is_new = self._log_ref(obj) 

289 # Pretend the object is new 

290 pretend_new = not self.unpicklable or not self.make_refs 

291 return pretend_new or is_new 

292 

293 def _unlog_ref(self, obj: Any) -> None: 

294 """ 

295 Undo the most recent _log_ref(), making obj unreferenceable. 

296 This was added to fix the bug described in 

297 test_decimal_passthrough_repeated_instance. Only safe to call for 

298 an object that was just logged, which basically limits it to handlers. 

299 """ 

300 self._objs.pop(id(obj), None) 

301 

302 def _getref(self, obj: Any) -> dict[str, int]: 

303 """Return a "py/id" entry for the specified object""" 

304 return {tags.ID: self._objs.get(id(obj))} # type: ignore[dict-item] 

305 

306 def _flatten(self, obj: Any) -> Any: 

307 """Flatten an object and its guts into a json-safe representation""" 

308 if self.unpicklable and self.make_refs: 

309 result = self._flatten_impl(obj) 

310 else: 

311 try: 

312 result = self._flattened[id(obj)] 

313 except KeyError: 

314 result = self._flattened[id(obj)] = self._flatten_impl(obj) 

315 return result 

316 

317 def flatten(self, obj: Any, reset: bool = True) -> Any: 

318 """Takes an object and returns a JSON-safe representation of it. 

319 

320 Simply returns any of the basic builtin datatypes 

321 

322 >>> p = Pickler() 

323 >>> p.flatten('hello world') == 'hello world' 

324 True 

325 >>> p.flatten(49) 

326 49 

327 >>> p.flatten(350.0) 

328 350.0 

329 >>> p.flatten(True) 

330 True 

331 >>> p.flatten(False) 

332 False 

333 >>> r = p.flatten(None) 

334 >>> r is None 

335 True 

336 >>> p.flatten(False) 

337 False 

338 >>> p.flatten([1, 2, 3, 4]) 

339 [1, 2, 3, 4] 

340 >>> p.flatten((1,2,))[tags.TUPLE] 

341 [1, 2] 

342 >>> p.flatten({'key': 'value'}) == {'key': 'value'} 

343 True 

344 """ 

345 if reset: 

346 self.reset() 

347 if self._determine_sort_keys(): 

348 obj = self._sort_attrs(obj) 

349 return self._flatten(obj) 

350 

351 def _flatten_bytestring(self, obj: bytes) -> dict[str, str]: 

352 return {self._bytes_tag: self._bytes_encoder(obj)} 

353 

354 def _flatten_impl(self, obj: Any) -> Any: 

355 ######################################### 

356 # if obj is nonrecursive return immediately 

357 # for performance reasons we don't want to do recursive checks 

358 typeof_obj = type(obj) 

359 if typeof_obj is bytes: 

360 return self._flatten_bytestring(obj) 

361 

362 if typeof_obj in (str, bool, int, float, type(None)): 

363 return obj 

364 

365 # bytearray is list-like, so it is neither reducible nor atomic. 

366 if typeof_obj is bytearray: 

367 return {tags.BYTEARRAY: self._flatten_bytestring(bytes(obj))} 

368 

369 # memoryview has no useful __reduce__, so the generic object path 

370 # would otherwise serialize it as an empty, unrestorable stub, 

371 # silently losing the underlying buffer's contents. 

372 if typeof_obj is memoryview: 

373 return {tags.MEMORYVIEW: self._flatten_bytestring(bytes(obj))} 

374 ######################################### 

375 

376 self._push() 

377 return self._pop(self._flatten_obj(obj)) 

378 

379 def _max_reached(self) -> bool: 

380 return self._depth == self._max_depth 

381 

382 def _pickle_warning(self, obj: Any) -> None: 

383 if self.warn: 

384 warnings.warn(f"jsonpickle cannot pickle {obj}: replaced with None") 

385 

386 def _flatten_obj(self, obj: Any) -> Any: 

387 self._seen.append(obj) 

388 

389 max_reached = self._max_reached() 

390 

391 try: 

392 in_cycle = _in_cycle(obj, self._objs, max_reached, self.make_refs) 

393 flatten_func: Callable[[Any], str] | None 

394 if in_cycle: 

395 # break the cycle 

396 flatten_func = repr 

397 else: 

398 flatten_func = self._get_flattener(obj) 

399 

400 if flatten_func is None: 

401 self._pickle_warning(obj) 

402 return None 

403 

404 return flatten_func(obj) 

405 

406 except (KeyboardInterrupt, SystemExit): 

407 raise 

408 except Exception as e: 

409 if self.fail_safe is None: 

410 raise 

411 else: 

412 return self.fail_safe(e) 

413 

414 def _list_recurse(self, obj: Iterable[Any]) -> list[Any]: 

415 return [self._flatten(v) for v in obj] 

416 

417 def _flatten_function(self, obj: Callable[..., Any]) -> dict[str, str] | None: 

418 if self.unpicklable: 

419 data = {tags.FUNCTION: util.importable_name(obj)} 

420 else: 

421 data = None 

422 

423 return data 

424 

425 def _getstate(self, obj: Any, data: dict[str, Any]) -> dict[str, Any]: 

426 state = self._flatten(obj) 

427 if self.unpicklable: 

428 data[tags.STATE] = state 

429 else: 

430 data = state 

431 return data 

432 

433 def _flatten_key_value_pair( 

434 self, k: Any, v: Any, data: dict[str | Any, Any] 

435 ) -> dict[str | Any, Any]: 

436 """Flatten a key/value pair into the passed-in dictionary.""" 

437 if not util._is_picklable(k, v): 

438 return data 

439 # TODO: use inspect.getmembers_static on 3.11+ because it avoids dynamic 

440 # attribute lookups 

441 if ( 

442 self.handle_readonly 

443 and k in {attr for attr, val in inspect.getmembers(self._original_object)} 

444 and util._is_readonly(self._original_object, k, v) 

445 ): 

446 return data 

447 

448 if k is None: 

449 k = "null" # for compatibility with common json encoders 

450 

451 if not isinstance(k, str): 

452 try: 

453 k = repr(k) 

454 except Exception: # ruff: ignore[BLE001] 

455 k = str(k) 

456 

457 data[k] = self._flatten(v) 

458 return data 

459 

460 def _call_handler_flatten( 

461 self, handler: handlers.BaseHandler, obj: Any, data: dict[str, Any] 

462 ) -> Any: 

463 kwargs: dict[str, Any] = {} 

464 if ( 

465 self.handler_context is not None 

466 and handlers.handler_accepts_handler_context(handler.flatten) 

467 ): 

468 kwargs["handler_context"] = self.handler_context 

469 return handler.flatten(obj, data, **kwargs) 

470 

471 def _flatten_obj_attrs( 

472 self, 

473 obj: Any, 

474 attrs: Iterable[str], 

475 data: dict[str, Any], 

476 exclude: Iterable[str] = (), 

477 ) -> bool: 

478 flatten = self._flatten_key_value_pair 

479 ok = False 

480 exclude = set(exclude) 

481 for k in attrs: 

482 if k in exclude: 

483 continue 

484 try: 

485 if not k.startswith("__"): 

486 value = getattr(obj, k) 

487 else: 

488 value = getattr(obj, f"_{obj.__class__.__name__}{k}") 

489 flatten(k, value, data) 

490 except AttributeError: 

491 # The attribute may have been deleted 

492 continue 

493 ok = True 

494 return ok 

495 

496 def _flatten_properties( 

497 self, 

498 obj: Any, 

499 data: dict[str, Any], 

500 allslots: Iterable[Sequence[str]] | None = None, 

501 ) -> dict[str, Any]: 

502 if allslots is None: 

503 # setting a list as a default argument can lead to some weird errors 

504 allslots = [] 

505 

506 # convert to set in case there are a lot of slots 

507 allslots_set = set(itertools.chain.from_iterable(allslots)) 

508 

509 # i don't like lambdas 

510 def valid_property(x: tuple[str, Any]) -> bool: 

511 return not x[0].startswith("__") and x[0] not in allslots_set 

512 

513 properties = [ 

514 x[0] for x in inspect.getmembers(obj.__class__) if valid_property(x) 

515 ] 

516 

517 properties_dict = {} 

518 for p_name in properties: 

519 p_val = getattr(obj, p_name) 

520 if util._is_not_class(p_val): 

521 properties_dict[p_name] = p_val 

522 else: 

523 properties_dict[p_name] = self._flatten(p_val) 

524 

525 data[tags.PROPERTY] = properties_dict 

526 

527 return data 

528 

529 def _flatten_newstyle_with_slots( 

530 self, 

531 obj: Any, 

532 data: dict[str, Any], 

533 exclude: Iterable[str] = (), 

534 ) -> dict[str, Any]: 

535 """Return a json-friendly dict for new-style objects with __slots__.""" 

536 allslots = [ 

537 _wrap_string_slot(getattr(cls, "__slots__", ())) 

538 for cls in obj.__class__.mro() 

539 ] 

540 

541 # add properties to the attribute list 

542 if self.include_properties: 

543 data = self._flatten_properties(obj, data, allslots) 

544 

545 if not self._flatten_obj_attrs(obj, chain(*allslots), data, exclude): 

546 attrs = [ 

547 x for x in dir(obj) if not x.startswith("__") and not x.endswith("__") 

548 ] 

549 self._flatten_obj_attrs(obj, attrs, data, exclude) 

550 

551 return data 

552 

553 def _reduce(self, obj: Any, has_reduce: bool, has_reduce_ex: bool) -> Any: 

554 """Return the object's __reduce__/__reduce_ex__ output, or None. 

555 

556 Many builtin types raise TypeError from these; treat that as 

557 "no reduce available" rather than letting it propagate. 

558 """ 

559 try: 

560 if has_reduce and not has_reduce_ex: 

561 return obj.__reduce__() 

562 if has_reduce_ex: 

563 return obj.__reduce_ex__(util.PICKLE_PROTOCOL) 

564 except TypeError: 

565 pass 

566 return None 

567 

568 def _flatten_obj_instance( 

569 self, obj: Any 

570 ) -> dict[str, Any] | list[Any] | Any | None: 

571 """Recursively flatten an instance and return a json-friendly dict""" 

572 # we're generally not bothering to annotate parts that aren't part of the public API 

573 # but this annotation alone saves us 3 mypy "errors" 

574 data: dict[str, Any] = {} 

575 has_class = hasattr(obj, "__class__") 

576 has_dict = hasattr(obj, "__dict__") 

577 has_slots = not has_dict and hasattr(obj, "__slots__") 

578 has_getnewargs = util.has_method(obj, "__getnewargs__") 

579 has_getnewargs_ex = util.has_method(obj, "__getnewargs_ex__") 

580 has_getinitargs = util.has_method(obj, "__getinitargs__") 

581 has_reduce, has_reduce_ex = util.has_reduce(obj) 

582 exclude = set(getattr(obj, "_jsonpickle_exclude", ())) 

583 

584 # Support objects with __getstate__(); this ensures that 

585 # both __setstate__() and __getstate__() are implemented 

586 has_own_getstate = hasattr(type(obj), "__getstate__") and type( 

587 obj 

588 ).__getstate__ is not getattr(object, "__getstate__", None) 

589 # not using has_method since __getstate__() is handled separately below 

590 # Note: on Python 3.11+, all objects have __getstate__. 

591 

592 if has_class: 

593 cls = obj.__class__ 

594 else: 

595 cls = type(obj) 

596 

597 # Check for a custom handler 

598 class_name = util.importable_name(cls) 

599 handler = handlers.get(cls, handlers.get(class_name)) # type: ignore[arg-type] 

600 if handler is not None: 

601 if self.unpicklable: 

602 data[tags.OBJECT] = class_name 

603 handler_instance = handler(self) 

604 result = self._call_handler_flatten(handler_instance, obj, data) 

605 if result is None: 

606 self._pickle_warning(obj) 

607 return result 

608 

609 if self.include_properties: 

610 data = self._flatten_properties(obj, data) 

611 

612 if self.unpicklable: 

613 # test for a reduce implementation, and redirect before 

614 # doing anything else if that is what reduce requests 

615 reduce_val = self._reduce(obj, has_reduce, has_reduce_ex) 

616 

617 if reduce_val and isinstance(reduce_val, str): 

618 try: 

619 varpath = iter(reduce_val.split(".")) 

620 # curmod will be transformed by the 

621 # loop into the value to pickle 

622 curmod = sys.modules[next(varpath)] 

623 for modname in varpath: 

624 curmod = getattr(curmod, modname) 

625 # replace obj with value retrieved 

626 return self._flatten(curmod) 

627 except KeyError: 

628 # well, we can't do anything with that, so we ignore it 

629 pass 

630 

631 elif reduce_val: 

632 # at this point, reduce_val should be some kind of iterable 

633 # pad out to len 6, for pickle protocol 5 support 

634 rv_as_list = list(reduce_val) 

635 insufficiency = 6 - len(rv_as_list) 

636 if insufficiency: 

637 rv_as_list += [None] * insufficiency 

638 

639 if getattr(rv_as_list[0], "__name__", "") == "__newobj__": 

640 rv_as_list[0] = tags.NEWOBJ 

641 

642 _, args, state, _, _ = rv_as_list[:5] 

643 

644 # check that getstate/setstate is sane 

645 if not ( 

646 state 

647 and has_own_getstate 

648 and not hasattr(obj, "__setstate__") 

649 and not isinstance(obj, dict) 

650 ): 

651 # turn iterators to iterables for convenient serialization 

652 if rv_as_list[3]: 

653 rv_as_list[3] = tuple(rv_as_list[3]) 

654 

655 if rv_as_list[4]: 

656 rv_as_list[4] = tuple(rv_as_list[4]) 

657 

658 reduce_args = list(map(self._flatten, rv_as_list)) 

659 last_index = len(reduce_args) - 1 

660 while last_index >= 2 and reduce_args[last_index] is None: 

661 last_index -= 1 

662 data[tags.REDUCE] = reduce_args[: last_index + 1] 

663 

664 return data 

665 

666 if has_class and not isinstance(obj, types.ModuleType): 

667 if self.unpicklable: 

668 data[tags.OBJECT] = class_name 

669 

670 if has_getnewargs_ex: 

671 data[tags.NEWARGSEX] = [ 

672 self._flatten(arg) for arg in obj.__getnewargs_ex__() 

673 ] 

674 

675 if has_getnewargs and not has_getnewargs_ex: 

676 data[tags.NEWARGS] = self._flatten(obj.__getnewargs__()) 

677 

678 if has_getinitargs: 

679 data[tags.INITARGS] = self._flatten(obj.__getinitargs__()) 

680 

681 if has_own_getstate: 

682 try: 

683 state = obj.__getstate__() 

684 except TypeError: 

685 # Has getstate but it cannot be called, e.g. file descriptors 

686 # in Python3 

687 self._pickle_warning(obj) 

688 return None 

689 else: 

690 if exclude and isinstance(state, dict): 

691 state = {k: v for k, v in util.items(state, exclude=exclude)} 

692 if state: 

693 return self._getstate(state, data) 

694 

695 if isinstance(obj, types.ModuleType): 

696 if self.unpicklable: 

697 data[tags.MODULE] = f"{obj.__name__}/{obj.__name__}" 

698 else: 

699 # TODO: this causes a mypy assignment error, figure out 

700 # if it's actually an error or a false alarm 

701 data = str(obj) # type: ignore[assignment] 

702 return data 

703 

704 if util._is_dictionary_subclass(obj): 

705 self._flatten_dict_obj(obj, data, exclude=exclude) 

706 return data 

707 

708 if util._is_sequence_subclass(obj): 

709 return self._flatten_sequence_obj(obj, data) 

710 

711 if util._is_iterator(obj): 

712 # force list in python 3 

713 data[tags.ITERATOR] = list(map(self._flatten, obj)) 

714 return data 

715 

716 if has_dict: 

717 # Support objects that subclasses list and set 

718 if util._is_sequence_subclass(obj): 

719 return self._flatten_sequence_obj(obj, data) 

720 

721 # hack for zope persistent objects; this unghostifies the object 

722 getattr(obj, "_", None) 

723 return self._flatten_dict_obj(obj.__dict__, data, exclude=exclude) 

724 

725 if has_slots: 

726 return self._flatten_newstyle_with_slots(obj, data, exclude=exclude) 

727 

728 # catchall return for data created above without a return 

729 # (e.g. __getnewargs__ is not supposed to be the end of the story) 

730 if data: 

731 return data 

732 

733 # Objects whose state is only reachable through __reduce__/__reduce_ex__ 

734 # (e.g. datetime.timedelta) have no __dict__, __slots__ or __getstate__ 

735 # for the branches above to read, so nothing has been produced and they 

736 # would otherwise become null. Emit a lossy view built from the reduce 

737 # output instead: its state if present, else the constructor args. The 

738 # string form and the listitems/dictitems slots (append/update-based 

739 # reconstruction) are not represented and keep the previous behaviour. 

740 if not self.unpicklable: 

741 reduce_val = self._reduce(obj, has_reduce, has_reduce_ex) 

742 if reduce_val is not None and not isinstance(reduce_val, str): 

743 # reduce tuple: (callable, args, state, listitems, dictitems) 

744 rv_as_list = list(reduce_val) 

745 state = rv_as_list[2] if len(rv_as_list) > 2 else None 

746 if state: 

747 return self._flatten(state) 

748 args = rv_as_list[1] if len(rv_as_list) > 1 else None 

749 if args: 

750 return self._flatten(args) 

751 

752 self._pickle_warning(obj) 

753 return None 

754 

755 def _ref_obj_instance(self, obj: Any) -> dict[str, Any] | list[Any] | None: 

756 """Reference an existing object or flatten if new""" 

757 if self.unpicklable: 

758 if self._mkref(obj): 

759 # We've never seen this object so return its 

760 # json representation. 

761 return self._flatten_obj_instance(obj) 

762 # We've seen this object before so place an object 

763 # reference tag in the data. This avoids infinite recursion 

764 # when processing cyclical objects. 

765 return self._getref(obj) 

766 else: 

767 max_reached = self._max_reached() 

768 in_cycle = _in_cycle(obj, self._objs, max_reached, False) 

769 if in_cycle: 

770 # A circular becomes None. 

771 return None 

772 

773 self._mkref(obj) 

774 return self._flatten_obj_instance(obj) 

775 

776 def _escape_key(self, k: Any) -> str: 

777 return tags.JSON_KEY + encode( 

778 k, 

779 reset=False, 

780 keys=True, 

781 context=self, 

782 make_refs=self.make_refs, 

783 ) 

784 

785 def _flatten_non_string_key_value_pair( 

786 self, k: Any, v: Any, data: dict[str, Any] 

787 ) -> dict[str, Any]: 

788 """Flatten only non-string key/value pairs""" 

789 if not util._is_picklable(k, v): 

790 return data 

791 if self.keys and not isinstance(k, str): 

792 k = self._escape_key(k) 

793 data[k] = self._flatten(v) 

794 return data 

795 

796 def _flatten_string_key_value_pair( 

797 self, k: str, v: Any, data: dict[str, Any] 

798 ) -> dict[str, Any]: 

799 """Flatten string key/value pairs only.""" 

800 if ( 

801 isinstance(k, str) 

802 and (k.startswith(tags.JSON_KEY) or k in tags.RESERVED) 

803 and self.keys 

804 ): 

805 # Escape data keys colliding with the json:// prefix or a reserved 

806 # wire tag; must run before _is_picklable, which drops RESERVED keys. 

807 data[self._escape_key(k)] = self._flatten(v) 

808 return data 

809 if not util._is_picklable(k, v): 

810 return data 

811 if self.keys: 

812 if not isinstance(k, str): 

813 return data 

814 else: 

815 if k is None: 

816 k = "null" # for compatibility with common json encoders 

817 

818 if not isinstance(k, str): 

819 try: 

820 k = repr(k) 

821 except Exception: # ruff: ignore[BLE001] 

822 k = str(k) 

823 

824 data[k] = self._flatten(v) 

825 return data 

826 

827 def _flatten_dict_obj( 

828 self, 

829 obj: dict[Any, Any], 

830 data: dict[Any, Any] | None = None, 

831 exclude: Iterable[Any] = (), 

832 ) -> dict[str, Any]: 

833 """Recursively call flatten() and return json-friendly dict""" 

834 if data is None: 

835 data = obj.__class__() 

836 

837 # If we allow non-string keys then we have to do a two-phase 

838 # encoding to ensure that the reference IDs are deterministic. 

839 if self.keys: 

840 # Phase 1: serialize regular objects, ignore fancy keys. 

841 flatten = self._flatten_string_key_value_pair 

842 for k, v in util.items(obj, exclude=exclude): 

843 flatten(k, v, data) 

844 

845 # Phase 2: serialize non-string keys. 

846 flatten = self._flatten_non_string_key_value_pair 

847 for k, v in util.items(obj, exclude=exclude): 

848 flatten(k, v, data) 

849 else: 

850 # If we have string keys only then we only need a single pass. 

851 flatten = self._flatten_key_value_pair 

852 for k, v in util.items(obj, exclude=exclude): 

853 flatten(k, v, data) 

854 

855 # the collections.defaultdict protocol 

856 if hasattr(obj, "default_factory") and callable(obj.default_factory): 

857 factory = obj.default_factory 

858 # i know that this string could be moved above the hasattr to reduce 

859 # string duplication but mypy 1.18.2 complains and i don't want to use 

860 # even more type: ignores 

861 store_key = "default_factory" 

862 if store_key in data: 

863 store_key = tags.DEFAULT_FACTORY 

864 value: Any 

865 if util._is_type(factory): 

866 # Reference the class/type 

867 # in this case it's dict[str, str] 

868 value = _mktyperef(factory) 

869 else: 

870 # The factory is not a type and could reference e.g. functions 

871 # or even the object instance itself, which creates a cycle. 

872 if self._mkref(factory): 

873 # We've never seen this object before so pickle it in-place. 

874 # Create an instance from the factory and assume that the 

875 # resulting instance is a suitable exemplar. 

876 value = self._flatten_obj_instance(handlers.CloneFactory(factory())) 

877 else: 

878 # We've seen this object before. 

879 # Break the cycle by emitting a reference. 

880 # in this case it's dict[str, int] 

881 value = self._getref(factory) 

882 data[store_key] = value 

883 

884 # Sub-classes of dict 

885 if hasattr(obj, "__dict__") and self.unpicklable and obj != obj.__dict__: 

886 if self._mkref(obj.__dict__): 

887 dict_data = {} 

888 self._flatten_dict_obj(obj.__dict__, dict_data, exclude=exclude) 

889 data["__dict__"] = dict_data 

890 else: 

891 data["__dict__"] = self._getref(obj.__dict__) 

892 

893 return data 

894 

895 def _get_flattener(self, obj: Any) -> Callable[[Any], Any] | None: 

896 if type(obj) in (list, dict): 

897 if self._mkref(obj): 

898 return ( 

899 self._list_recurse if type(obj) is list else self._flatten_dict_obj 

900 ) 

901 else: 

902 return self._getref 

903 

904 # We handle tuples and sets by encoding them in a "(tuple|set)dict" 

905 elif type(obj) in (tuple, set): 

906 if not self.unpicklable: 

907 return self._list_recurse 

908 return lambda obj: { 

909 tags.TUPLE if type(obj) is tuple else tags.SET: [ 

910 self._flatten(v) for v in obj 

911 ] 

912 } 

913 

914 elif util._is_module_function(obj): 

915 return self._flatten_function 

916 

917 elif util._is_object(obj): 

918 return self._ref_obj_instance 

919 

920 elif util._is_type(obj): 

921 return _mktyperef 

922 

923 # instance methods, lambdas, old style classes... 

924 self._pickle_warning(obj) 

925 return None 

926 

927 def _flatten_sequence_obj( 

928 self, obj: Iterable[Any], data: dict[str, Any] 

929 ) -> dict[str, Any] | list[Any]: 

930 """Return a json-friendly dict for a sequence subclass.""" 

931 if hasattr(obj, "__dict__"): 

932 self._flatten_dict_obj(obj.__dict__, data) 

933 value = [self._flatten(v) for v in obj] 

934 if self.unpicklable: 

935 data[tags.SEQ] = value 

936 else: 

937 return value 

938 return data