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

420 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 

370 self._push() 

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

372 

373 def _max_reached(self) -> bool: 

374 return self._depth == self._max_depth 

375 

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

377 if self.warn: 

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

379 

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

381 self._seen.append(obj) 

382 

383 max_reached = self._max_reached() 

384 

385 try: 

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

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

388 if in_cycle: 

389 # break the cycle 

390 flatten_func = repr 

391 else: 

392 flatten_func = self._get_flattener(obj) 

393 

394 if flatten_func is None: 

395 self._pickle_warning(obj) 

396 return None 

397 

398 return flatten_func(obj) 

399 

400 except (KeyboardInterrupt, SystemExit): 

401 raise 

402 except Exception as e: 

403 if self.fail_safe is None: 

404 raise 

405 else: 

406 return self.fail_safe(e) 

407 

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

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

410 

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

412 if self.unpicklable: 

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

414 else: 

415 data = None 

416 

417 return data 

418 

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

420 state = self._flatten(obj) 

421 if self.unpicklable: 

422 data[tags.STATE] = state 

423 else: 

424 data = state 

425 return data 

426 

427 def _flatten_key_value_pair( 

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

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

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

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

432 return data 

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

434 # attribute lookups 

435 if ( 

436 self.handle_readonly 

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

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

439 ): 

440 return data 

441 

442 if k is None: 

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

444 

445 if not isinstance(k, str): 

446 try: 

447 k = repr(k) 

448 except Exception: # ruff: ignore[BLE001] 

449 k = str(k) 

450 

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

452 return data 

453 

454 def _call_handler_flatten( 

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

456 ) -> Any: 

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

458 if ( 

459 self.handler_context is not None 

460 and handlers.handler_accepts_handler_context(handler.flatten) 

461 ): 

462 kwargs["handler_context"] = self.handler_context 

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

464 

465 def _flatten_obj_attrs( 

466 self, 

467 obj: Any, 

468 attrs: Iterable[str], 

469 data: dict[str, Any], 

470 exclude: Iterable[str] = (), 

471 ) -> bool: 

472 flatten = self._flatten_key_value_pair 

473 ok = False 

474 exclude = set(exclude) 

475 for k in attrs: 

476 if k in exclude: 

477 continue 

478 try: 

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

480 value = getattr(obj, k) 

481 else: 

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

483 flatten(k, value, data) 

484 except AttributeError: 

485 # The attribute may have been deleted 

486 continue 

487 ok = True 

488 return ok 

489 

490 def _flatten_properties( 

491 self, 

492 obj: Any, 

493 data: dict[str, Any], 

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

495 ) -> dict[str, Any]: 

496 if allslots is None: 

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

498 allslots = [] 

499 

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

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

502 

503 # i don't like lambdas 

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

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

506 

507 properties = [ 

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

509 ] 

510 

511 properties_dict = {} 

512 for p_name in properties: 

513 p_val = getattr(obj, p_name) 

514 if util._is_not_class(p_val): 

515 properties_dict[p_name] = p_val 

516 else: 

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

518 

519 data[tags.PROPERTY] = properties_dict 

520 

521 return data 

522 

523 def _flatten_newstyle_with_slots( 

524 self, 

525 obj: Any, 

526 data: dict[str, Any], 

527 exclude: Iterable[str] = (), 

528 ) -> dict[str, Any]: 

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

530 allslots = [ 

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

532 for cls in obj.__class__.mro() 

533 ] 

534 

535 # add properties to the attribute list 

536 if self.include_properties: 

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

538 

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

540 attrs = [ 

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

542 ] 

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

544 

545 return data 

546 

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

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

549 

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

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

552 """ 

553 try: 

554 if has_reduce and not has_reduce_ex: 

555 return obj.__reduce__() 

556 if has_reduce_ex: 

557 return obj.__reduce_ex__(util.PICKLE_PROTOCOL) 

558 except TypeError: 

559 pass 

560 return None 

561 

562 def _flatten_obj_instance( 

563 self, obj: Any 

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

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

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

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

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

569 has_class = hasattr(obj, "__class__") 

570 has_dict = hasattr(obj, "__dict__") 

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

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

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

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

575 has_reduce, has_reduce_ex = util.has_reduce(obj) 

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

577 

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

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

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

581 obj 

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

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

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

585 

586 if has_class: 

587 cls = obj.__class__ 

588 else: 

589 cls = type(obj) 

590 

591 # Check for a custom handler 

592 class_name = util.importable_name(cls) 

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

594 if handler is not None: 

595 if self.unpicklable: 

596 data[tags.OBJECT] = class_name 

597 handler_instance = handler(self) 

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

599 if result is None: 

600 self._pickle_warning(obj) 

601 return result 

602 

603 if self.include_properties: 

604 data = self._flatten_properties(obj, data) 

605 

606 if self.unpicklable: 

607 # test for a reduce implementation, and redirect before 

608 # doing anything else if that is what reduce requests 

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

610 

611 if reduce_val and isinstance(reduce_val, str): 

612 try: 

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

614 # curmod will be transformed by the 

615 # loop into the value to pickle 

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

617 for modname in varpath: 

618 curmod = getattr(curmod, modname) 

619 # replace obj with value retrieved 

620 return self._flatten(curmod) 

621 except KeyError: 

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

623 pass 

624 

625 elif reduce_val: 

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

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

628 rv_as_list = list(reduce_val) 

629 insufficiency = 6 - len(rv_as_list) 

630 if insufficiency: 

631 rv_as_list += [None] * insufficiency 

632 

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

634 rv_as_list[0] = tags.NEWOBJ 

635 

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

637 

638 # check that getstate/setstate is sane 

639 if not ( 

640 state 

641 and has_own_getstate 

642 and not hasattr(obj, "__setstate__") 

643 and not isinstance(obj, dict) 

644 ): 

645 # turn iterators to iterables for convenient serialization 

646 if rv_as_list[3]: 

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

648 

649 if rv_as_list[4]: 

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

651 

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

653 last_index = len(reduce_args) - 1 

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

655 last_index -= 1 

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

657 

658 return data 

659 

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

661 if self.unpicklable: 

662 data[tags.OBJECT] = class_name 

663 

664 if has_getnewargs_ex: 

665 data[tags.NEWARGSEX] = [ 

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

667 ] 

668 

669 if has_getnewargs and not has_getnewargs_ex: 

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

671 

672 if has_getinitargs: 

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

674 

675 if has_own_getstate: 

676 try: 

677 state = obj.__getstate__() 

678 except TypeError: 

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

680 # in Python3 

681 self._pickle_warning(obj) 

682 return None 

683 else: 

684 if exclude and isinstance(state, dict): 

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

686 if state: 

687 return self._getstate(state, data) 

688 

689 if isinstance(obj, types.ModuleType): 

690 if self.unpicklable: 

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

692 else: 

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

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

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

696 return data 

697 

698 if util._is_dictionary_subclass(obj): 

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

700 return data 

701 

702 if util._is_sequence_subclass(obj): 

703 return self._flatten_sequence_obj(obj, data) 

704 

705 if util._is_iterator(obj): 

706 # force list in python 3 

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

708 return data 

709 

710 if has_dict: 

711 # Support objects that subclasses list and set 

712 if util._is_sequence_subclass(obj): 

713 return self._flatten_sequence_obj(obj, data) 

714 

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

716 getattr(obj, "_", None) 

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

718 

719 if has_slots: 

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

721 

722 # catchall return for data created above without a return 

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

724 if data: 

725 return data 

726 

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

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

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

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

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

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

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

734 if not self.unpicklable: 

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

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

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

738 rv_as_list = list(reduce_val) 

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

740 if state: 

741 return self._flatten(state) 

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

743 if args: 

744 return self._flatten(args) 

745 

746 self._pickle_warning(obj) 

747 return None 

748 

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

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

751 if self.unpicklable: 

752 if self._mkref(obj): 

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

754 # json representation. 

755 return self._flatten_obj_instance(obj) 

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

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

758 # when processing cyclical objects. 

759 return self._getref(obj) 

760 else: 

761 max_reached = self._max_reached() 

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

763 if in_cycle: 

764 # A circular becomes None. 

765 return None 

766 

767 self._mkref(obj) 

768 return self._flatten_obj_instance(obj) 

769 

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

771 return tags.JSON_KEY + encode( 

772 k, 

773 reset=False, 

774 keys=True, 

775 context=self, 

776 make_refs=self.make_refs, 

777 ) 

778 

779 def _flatten_non_string_key_value_pair( 

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

781 ) -> dict[str, Any]: 

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

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

784 return data 

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

786 k = self._escape_key(k) 

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

788 return data 

789 

790 def _flatten_string_key_value_pair( 

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

792 ) -> dict[str, Any]: 

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

794 if ( 

795 isinstance(k, str) 

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

797 and self.keys 

798 ): 

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

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

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

802 return data 

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

804 return data 

805 if self.keys: 

806 if not isinstance(k, str): 

807 return data 

808 else: 

809 if k is None: 

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

811 

812 if not isinstance(k, str): 

813 try: 

814 k = repr(k) 

815 except Exception: # ruff: ignore[BLE001] 

816 k = str(k) 

817 

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

819 return data 

820 

821 def _flatten_dict_obj( 

822 self, 

823 obj: dict[Any, Any], 

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

825 exclude: Iterable[Any] = (), 

826 ) -> dict[str, Any]: 

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

828 if data is None: 

829 data = obj.__class__() 

830 

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

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

833 if self.keys: 

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

835 flatten = self._flatten_string_key_value_pair 

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

837 flatten(k, v, data) 

838 

839 # Phase 2: serialize non-string keys. 

840 flatten = self._flatten_non_string_key_value_pair 

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

842 flatten(k, v, data) 

843 else: 

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

845 flatten = self._flatten_key_value_pair 

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

847 flatten(k, v, data) 

848 

849 # the collections.defaultdict protocol 

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

851 factory = obj.default_factory 

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

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

854 # even more type: ignores 

855 store_key = "default_factory" 

856 if store_key in data: 

857 store_key = tags.DEFAULT_FACTORY 

858 value: Any 

859 if util._is_type(factory): 

860 # Reference the class/type 

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

862 value = _mktyperef(factory) 

863 else: 

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

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

866 if self._mkref(factory): 

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

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

869 # resulting instance is a suitable exemplar. 

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

871 else: 

872 # We've seen this object before. 

873 # Break the cycle by emitting a reference. 

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

875 value = self._getref(factory) 

876 data[store_key] = value 

877 

878 # Sub-classes of dict 

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

880 if self._mkref(obj.__dict__): 

881 dict_data = {} 

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

883 data["__dict__"] = dict_data 

884 else: 

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

886 

887 return data 

888 

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

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

891 if self._mkref(obj): 

892 return ( 

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

894 ) 

895 else: 

896 return self._getref 

897 

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

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

900 if not self.unpicklable: 

901 return self._list_recurse 

902 return lambda obj: { 

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

904 self._flatten(v) for v in obj 

905 ] 

906 } 

907 

908 elif util._is_module_function(obj): 

909 return self._flatten_function 

910 

911 elif util._is_object(obj): 

912 return self._ref_obj_instance 

913 

914 elif util._is_type(obj): 

915 return _mktyperef 

916 

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

918 self._pickle_warning(obj) 

919 return None 

920 

921 def _flatten_sequence_obj( 

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

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

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

925 if hasattr(obj, "__dict__"): 

926 self._flatten_dict_obj(obj.__dict__, data) 

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

928 if self.unpicklable: 

929 data[tags.SEQ] = value 

930 else: 

931 return value 

932 return data