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

415 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 decimal 

8import inspect 

9import itertools 

10import sys 

11import types 

12import warnings 

13from collections.abc import Callable, Iterable, Sequence 

14from itertools import chain, islice 

15from typing import Any 

16 

17from . import handlers, tags, util 

18from .backend import JSONBackend, json 

19 

20 

21def encode( 

22 value: Any, 

23 unpicklable: bool = True, 

24 make_refs: bool = True, 

25 keys: bool = False, 

26 max_depth: int | None = None, 

27 reset: bool = True, 

28 backend: JSONBackend | None = None, 

29 warn: bool = False, 

30 context: "Pickler | None" = None, 

31 max_iter: int | None = None, 

32 use_decimal: bool = False, 

33 numeric_keys: bool = False, 

34 use_base85: bool = False, 

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

36 indent: int | None = None, 

37 separators: Any | None = None, 

38 include_properties: bool = False, 

39 handle_readonly: bool = False, 

40 handler_context: Any = None, 

41) -> str: 

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

43 

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

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

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

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

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

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

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

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

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

53 

54 jsonpickle.register( 

55 numpy.generic, UnpicklableNumpyGenericHandler, base=True 

56 ) 

57 

58 before your pickling code. 

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

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

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

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

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

64 :param keys: If set to True then jsonpickle will encode non-string 

65 dictionary keys instead of coercing them into strings via `repr()`. 

66 This is typically what you want if you need to support Integer or 

67 objects as dictionary keys. 

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

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

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

71 object. 

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

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

74 in order to retain object references during pickling. 

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

76 `__getstate__` implementation. 

77 :param backend: If set to an instance of jsonpickle.backend.JSONBackend, 

78 jsonpickle will use that backend for serialization. 

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

80 returns None for an object which it cannot pickle 

81 (e.g. file descriptors). 

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

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

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

85 active Pickler and Unpickler objects when custom handlers are 

86 invoked by jsonpickle. 

87 :param max_iter: If set to a non-negative integer then jsonpickle will 

88 consume at most `max_iter` items when pickling iterators. 

89 :param use_decimal: If set to True jsonpickle will allow Decimal 

90 instances to pass-through, with the assumption that the simplejson 

91 backend will be used in `use_decimal` mode. In order to use this mode 

92 you will need to configure simplejson:: 

93 

94 jsonpickle.set_encoder_options('simplejson', 

95 use_decimal=True, sort_keys=True) 

96 jsonpickle.set_decoder_options('simplejson', 

97 use_decimal=True) 

98 jsonpickle.set_preferred_backend('simplejson') 

99 

100 NOTE: A side-effect of the above settings is that float values will be 

101 converted to Decimal when converting to json. 

102 :param numeric_keys: Only use this option if the backend supports integer 

103 dict keys natively. This flag tells jsonpickle to leave numeric keys 

104 as-is rather than conforming them to json-friendly strings. 

105 Using ``keys=True`` is the typical solution for integer keys, so only 

106 use this if you have a specific use case where you want to allow the 

107 backend to handle serialization of numeric dict keys. 

108 :param use_base85: 

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

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

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

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

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

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

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

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

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

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

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

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

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

122 library and not used by jsonpickle directly. 

123 :param separators: 

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

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

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

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

128 not used by jsonpickle directly. 

129 :param include_properties: 

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

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

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

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

134 Defaults to ``False``. 

135 :param handle_readonly: 

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

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

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

139 this flag set to ``True``. 

140 :param handler_context: 

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

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

143 be found in the examples/ directory on GitHub. 

144 

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

146 True 

147 >>> encode(36) == '36' 

148 True 

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

150 True 

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

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

153 

154 """ 

155 

156 backend = backend or json 

157 context = context or Pickler( 

158 unpicklable=unpicklable, 

159 make_refs=make_refs, 

160 keys=keys, 

161 backend=backend, 

162 max_depth=max_depth, 

163 warn=warn, 

164 max_iter=max_iter, 

165 numeric_keys=numeric_keys, 

166 use_decimal=use_decimal, 

167 use_base85=use_base85, 

168 fail_safe=fail_safe, 

169 include_properties=include_properties, 

170 handle_readonly=handle_readonly, 

171 original_object=value, 

172 handler_context=handler_context, 

173 ) 

174 if handler_context is not None: 

175 context.handler_context = handler_context 

176 return backend.encode( 

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

178 ) 

179 

180 

181def _in_cycle( 

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

183) -> bool: 

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

185 return ( 

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

187 and not util._is_primitive(obj) 

188 and not util._is_enum(obj) 

189 ) 

190 

191 

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

193 """Return a typeref dictionary 

194 

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

196 True 

197 

198 """ 

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

200 

201 

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

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

204 if isinstance(string, str): 

205 return (string,) 

206 return string 

207 

208 

209class Pickler: 

210 def __init__( 

211 self, 

212 unpicklable: bool = True, 

213 make_refs: bool = True, 

214 max_depth: int | None = None, 

215 backend: JSONBackend | None = None, 

216 keys: bool = False, 

217 warn: bool = False, 

218 max_iter: int | None = None, 

219 numeric_keys: bool = False, 

220 use_decimal: bool = False, 

221 use_base85: bool = False, 

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

223 include_properties: bool = False, 

224 handle_readonly: bool = False, 

225 original_object: Any | None = None, 

226 handler_context: Any = None, 

227 ) -> None: 

228 self.unpicklable = unpicklable 

229 self.make_refs = make_refs 

230 self.backend = backend or json 

231 self.keys = keys 

232 self.warn = warn 

233 self.numeric_keys = numeric_keys 

234 self.use_base85 = use_base85 

235 # The current recursion depth 

236 self._depth = -1 

237 # The maximal recursion depth 

238 self._max_depth = max_depth 

239 # Maps id(obj) to reference IDs 

240 self._objs = {} 

241 # Avoids garbage collection 

242 self._seen = [] 

243 # maximum amount of items to take from a pickled iterator 

244 self._max_iter = max_iter 

245 # Whether to allow decimals to pass-through 

246 self._use_decimal = use_decimal 

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

248 self._flattened = {} 

249 # Used for util._is_readonly, see +483 

250 self.handle_readonly = handle_readonly 

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

252 self.handler_context = handler_context 

253 

254 if self.use_base85: 

255 self._bytes_tag = tags.B85 

256 self._bytes_encoder = util.b85encode 

257 else: 

258 self._bytes_tag = tags.B64 

259 self._bytes_encoder = util.b64encode 

260 

261 # ignore exceptions 

262 self.fail_safe = fail_safe 

263 self.include_properties = include_properties 

264 

265 self._original_object = original_object 

266 

267 def _determine_sort_keys(self) -> bool: 

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

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

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

271 return True 

272 return False 

273 

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

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

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

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

278 # and that would require calling the init function 

279 # of the parent again. That could cause issues 

280 # so we refuse to handle it. 

281 raise TypeError( 

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

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

284 ) 

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

286 elif hasattr(obj, "__dict__"): 

287 try: 

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

289 except (TypeError, AttributeError): 

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

291 pass 

292 return obj 

293 

294 def reset(self) -> None: 

295 self._objs = {} 

296 self._depth = -1 

297 self._seen = [] 

298 self._flattened = {} 

299 

300 def _push(self) -> None: 

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

302 self._depth += 1 

303 

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

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

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

307 """ 

308 self._depth -= 1 

309 if self._depth == -1: 

310 self.reset() 

311 return value 

312 

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

314 """ 

315 Log a reference to an in-memory object. 

316 Return True if this object is new and was assigned 

317 a new ID. Otherwise return False. 

318 """ 

319 objid = id(obj) 

320 is_new = objid not in self._objs 

321 if is_new: 

322 new_id = len(self._objs) 

323 self._objs[objid] = new_id 

324 return is_new 

325 

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

327 """ 

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

329 if that object should be considered newly logged. 

330 """ 

331 is_new = self._log_ref(obj) 

332 # Pretend the object is new 

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

334 return pretend_new or is_new 

335 

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

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

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

339 

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

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

342 if self.unpicklable and self.make_refs: 

343 result = self._flatten_impl(obj) 

344 else: 

345 try: 

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

347 except KeyError: 

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

349 return result 

350 

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

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

353 

354 Simply returns any of the basic builtin datatypes 

355 

356 >>> p = Pickler() 

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

358 True 

359 >>> p.flatten(49) 

360 49 

361 >>> p.flatten(350.0) 

362 350.0 

363 >>> p.flatten(True) 

364 True 

365 >>> p.flatten(False) 

366 False 

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

368 >>> r is None 

369 True 

370 >>> p.flatten(False) 

371 False 

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

373 [1, 2, 3, 4] 

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

375 [1, 2] 

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

377 True 

378 """ 

379 if reset: 

380 self.reset() 

381 if self._determine_sort_keys(): 

382 obj = self._sort_attrs(obj) 

383 return self._flatten(obj) 

384 

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

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

387 

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

389 ######################################### 

390 # if obj is nonrecursive return immediately 

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

392 if type(obj) is bytes: 

393 return self._flatten_bytestring(obj) 

394 

395 # Decimal is a primitive when use_decimal is True 

396 if type(obj) in (str, bool, int, float, type(None)) or ( 

397 self._use_decimal and isinstance(obj, decimal.Decimal) 

398 ): 

399 return obj 

400 ######################################### 

401 

402 self._push() 

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

404 

405 def _max_reached(self) -> bool: 

406 return self._depth == self._max_depth 

407 

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

409 if self.warn: 

410 msg = "jsonpickle cannot pickle %r: replaced with None" % obj 

411 warnings.warn(msg) 

412 

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

414 self._seen.append(obj) 

415 

416 max_reached = self._max_reached() 

417 

418 try: 

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

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

421 if in_cycle: 

422 # break the cycle 

423 flatten_func = repr 

424 else: 

425 flatten_func = self._get_flattener(obj) 

426 

427 if flatten_func is None: 

428 self._pickle_warning(obj) 

429 return None 

430 

431 return flatten_func(obj) 

432 

433 except (KeyboardInterrupt, SystemExit) as e: 

434 raise e 

435 except Exception as e: 

436 if self.fail_safe is None: 

437 raise e 

438 else: 

439 return self.fail_safe(e) 

440 

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

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

443 

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

445 if self.unpicklable: 

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

447 else: 

448 data = None 

449 

450 return data 

451 

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

453 state = self._flatten(obj) 

454 if self.unpicklable: 

455 data[tags.STATE] = state 

456 else: 

457 data = state 

458 return data 

459 

460 def _flatten_key_value_pair( 

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

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

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

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

465 return data 

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

467 # attribute lookups 

468 if ( 

469 self.handle_readonly 

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

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

472 ): 

473 return data 

474 

475 if k is None: 

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

477 

478 if self.numeric_keys and isinstance(k, (int, float)): 

479 pass 

480 elif not isinstance(k, str): 

481 try: 

482 k = repr(k) 

483 except Exception: 

484 k = str(k) 

485 

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

487 return data 

488 

489 def _call_handler_flatten( 

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

491 ) -> Any: 

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

493 if ( 

494 self.handler_context is not None 

495 and handlers.handler_accepts_handler_context(handler.flatten) 

496 ): 

497 kwargs["handler_context"] = self.handler_context 

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

499 

500 def _flatten_obj_attrs( 

501 self, 

502 obj: Any, 

503 attrs: Iterable[str], 

504 data: dict[str, Any], 

505 exclude: Iterable[str] = (), 

506 ) -> bool: 

507 flatten = self._flatten_key_value_pair 

508 ok = False 

509 exclude = set(exclude) 

510 for k in attrs: 

511 if k in exclude: 

512 continue 

513 try: 

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

515 value = getattr(obj, k) 

516 else: 

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

518 flatten(k, value, data) 

519 except AttributeError: 

520 # The attribute may have been deleted 

521 continue 

522 ok = True 

523 return ok 

524 

525 def _flatten_properties( 

526 self, 

527 obj: Any, 

528 data: dict[str, Any], 

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

530 ) -> dict[str, Any]: 

531 if allslots is None: 

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

533 allslots = [] 

534 

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

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

537 

538 # i don't like lambdas 

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

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

541 

542 properties = [ 

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

544 ] 

545 

546 properties_dict = {} 

547 for p_name in properties: 

548 p_val = getattr(obj, p_name) 

549 if util._is_not_class(p_val): 

550 properties_dict[p_name] = p_val 

551 else: 

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

553 

554 data[tags.PROPERTY] = properties_dict 

555 

556 return data 

557 

558 def _flatten_newstyle_with_slots( 

559 self, 

560 obj: Any, 

561 data: dict[str, Any], 

562 exclude: Iterable[str] = (), 

563 ) -> dict[str, Any]: 

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

565 allslots = [ 

566 _wrap_string_slot(getattr(cls, "__slots__", tuple())) 

567 for cls in obj.__class__.mro() 

568 ] 

569 

570 # add properties to the attribute list 

571 if self.include_properties: 

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

573 

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

575 attrs = [ 

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

577 ] 

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

579 

580 return data 

581 

582 def _flatten_obj_instance( 

583 self, obj: Any 

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

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

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

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

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

589 has_class = hasattr(obj, "__class__") 

590 has_dict = hasattr(obj, "__dict__") 

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

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

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

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

595 has_reduce, has_reduce_ex = util.has_reduce(obj) 

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

597 

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

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

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

601 obj 

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

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

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

605 

606 if has_class: 

607 cls = obj.__class__ 

608 else: 

609 cls = type(obj) 

610 

611 # Check for a custom handler 

612 class_name = util.importable_name(cls) 

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

614 if handler is not None: 

615 if self.unpicklable: 

616 data[tags.OBJECT] = class_name 

617 handler_instance = handler(self) 

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

619 if result is None: 

620 self._pickle_warning(obj) 

621 return result 

622 

623 reduce_val = None 

624 

625 if self.include_properties: 

626 data = self._flatten_properties(obj, data) 

627 

628 if self.unpicklable: 

629 if has_reduce and not has_reduce_ex: 

630 try: 

631 reduce_val = obj.__reduce__() 

632 except TypeError: 

633 # A lot of builtin types have a reduce which 

634 # just raises a TypeError 

635 # we ignore those 

636 pass 

637 

638 # test for a reduce implementation, and redirect before 

639 # doing anything else if that is what reduce requests 

640 elif has_reduce_ex: 

641 try: 

642 # we're implementing protocol 2 

643 reduce_val = obj.__reduce_ex__(2) 

644 except TypeError: 

645 # A lot of builtin types have a reduce which 

646 # just raises a TypeError 

647 # we ignore those 

648 pass 

649 

650 if reduce_val and isinstance(reduce_val, str): 

651 try: 

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

653 # curmod will be transformed by the 

654 # loop into the value to pickle 

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

656 for modname in varpath: 

657 curmod = getattr(curmod, modname) 

658 # replace obj with value retrieved 

659 return self._flatten(curmod) 

660 except KeyError: 

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

662 pass 

663 

664 elif reduce_val: 

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

666 # pad out to len 5 

667 rv_as_list = list(reduce_val) 

668 insufficiency = 5 - len(rv_as_list) 

669 if insufficiency: 

670 rv_as_list += [None] * insufficiency 

671 

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

673 rv_as_list[0] = tags.NEWOBJ 

674 

675 f, args, state, listitems, dictitems = rv_as_list 

676 

677 # check that getstate/setstate is sane 

678 if not ( 

679 state 

680 and has_own_getstate 

681 and not hasattr(obj, "__setstate__") 

682 and not isinstance(obj, dict) 

683 ): 

684 # turn iterators to iterables for convenient serialization 

685 if rv_as_list[3]: 

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

687 

688 if rv_as_list[4]: 

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

690 

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

692 last_index = len(reduce_args) - 1 

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

694 last_index -= 1 

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

696 

697 return data 

698 

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

700 if self.unpicklable: 

701 data[tags.OBJECT] = class_name 

702 

703 if has_getnewargs_ex: 

704 data[tags.NEWARGSEX] = [ 

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

706 ] 

707 

708 if has_getnewargs and not has_getnewargs_ex: 

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

710 

711 if has_getinitargs: 

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

713 

714 if has_own_getstate: 

715 try: 

716 state = obj.__getstate__() 

717 except TypeError: 

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

719 # in Python3 

720 self._pickle_warning(obj) 

721 return None 

722 else: 

723 if exclude and isinstance(state, dict): 

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

725 if state: 

726 return self._getstate(state, data) 

727 

728 if isinstance(obj, types.ModuleType): 

729 if self.unpicklable: 

730 data[tags.MODULE] = "{name}/{name}".format(name=obj.__name__) 

731 else: 

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

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

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

735 return data 

736 

737 if util._is_dictionary_subclass(obj): 

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

739 return data 

740 

741 if util._is_sequence_subclass(obj): 

742 return self._flatten_sequence_obj(obj, data) 

743 

744 if util._is_iterator(obj): 

745 # force list in python 3 

746 data[tags.ITERATOR] = list(map(self._flatten, islice(obj, self._max_iter))) 

747 return data 

748 

749 if has_dict: 

750 # Support objects that subclasses list and set 

751 if util._is_sequence_subclass(obj): 

752 return self._flatten_sequence_obj(obj, data) 

753 

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

755 getattr(obj, "_", None) 

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

757 

758 if has_slots: 

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

760 

761 # catchall return for data created above without a return 

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

763 if data: 

764 return data 

765 

766 self._pickle_warning(obj) 

767 return None 

768 

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

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

771 if self.unpicklable: 

772 if self._mkref(obj): 

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

774 # json representation. 

775 return self._flatten_obj_instance(obj) 

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

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

778 # when processing cyclical objects. 

779 return self._getref(obj) 

780 else: 

781 max_reached = self._max_reached() 

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

783 if in_cycle: 

784 # A circular becomes None. 

785 return None 

786 

787 self._mkref(obj) 

788 return self._flatten_obj_instance(obj) 

789 

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

791 return tags.JSON_KEY + encode( 

792 k, 

793 reset=False, 

794 keys=True, 

795 context=self, 

796 backend=self.backend, 

797 make_refs=self.make_refs, 

798 ) 

799 

800 def _flatten_non_string_key_value_pair( 

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

802 ) -> dict[str, Any]: 

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

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

805 return data 

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

807 k = self._escape_key(k) 

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

809 return data 

810 

811 def _flatten_string_key_value_pair( 

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

813 ) -> dict[str, Any]: 

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

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

816 return data 

817 if self.keys: 

818 if not isinstance(k, str): 

819 return data 

820 elif k.startswith(tags.JSON_KEY): 

821 k = self._escape_key(k) 

822 else: 

823 if k is None: 

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

825 

826 if self.numeric_keys and isinstance(k, (int, float)): 

827 pass 

828 elif not isinstance(k, str): 

829 try: 

830 k = repr(k) 

831 except Exception: 

832 k = str(k) 

833 

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

835 return data 

836 

837 def _flatten_dict_obj( 

838 self, 

839 obj: dict[Any, Any], 

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

841 exclude: Iterable[Any] = (), 

842 ) -> dict[str, Any]: 

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

844 if data is None: 

845 data = obj.__class__() 

846 

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

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

849 if self.keys: 

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

851 flatten = self._flatten_string_key_value_pair 

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

853 flatten(k, v, data) 

854 

855 # Phase 2: serialize non-string keys. 

856 flatten = self._flatten_non_string_key_value_pair 

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

858 flatten(k, v, data) 

859 else: 

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

861 flatten = self._flatten_key_value_pair 

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

863 flatten(k, v, data) 

864 

865 # the collections.defaultdict protocol 

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

867 factory = obj.default_factory 

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

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

870 # even more type: ignores 

871 store_key = "default_factory" 

872 if store_key in data: 

873 store_key = tags.DEFAULT_FACTORY 

874 value: Any 

875 if util._is_type(factory): 

876 # Reference the class/type 

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

878 value = _mktyperef(factory) 

879 else: 

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

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

882 if self._mkref(factory): 

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

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

885 # resulting instance is a suitable exemplar. 

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

887 else: 

888 # We've seen this object before. 

889 # Break the cycle by emitting a reference. 

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

891 value = self._getref(factory) 

892 data[store_key] = value 

893 

894 # Sub-classes of dict 

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

896 if self._mkref(obj.__dict__): 

897 dict_data = {} 

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

899 data["__dict__"] = dict_data 

900 else: 

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

902 

903 return data 

904 

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

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

907 if self._mkref(obj): 

908 return ( 

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

910 ) 

911 else: 

912 return self._getref 

913 

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

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

916 if not self.unpicklable: 

917 return self._list_recurse 

918 return lambda obj: { 

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

920 self._flatten(v) for v in obj 

921 ] 

922 } 

923 

924 elif util._is_module_function(obj): 

925 return self._flatten_function 

926 

927 elif util._is_object(obj): 

928 return self._ref_obj_instance 

929 

930 elif util._is_type(obj): 

931 return _mktyperef 

932 

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

934 self._pickle_warning(obj) 

935 return None 

936 

937 def _flatten_sequence_obj( 

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

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

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

941 if hasattr(obj, "__dict__"): 

942 self._flatten_dict_obj(obj.__dict__, data) 

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

944 if self.unpicklable: 

945 data[tags.SEQ] = value 

946 else: 

947 return value 

948 return data