Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1#!/usr/bin/env python 

2# cardinal_pythonlib/json/serialize.py 

3 

4""" 

5=============================================================================== 

6 

7 Original code copyright (C) 2009-2021 Rudolf Cardinal (rudolf@pobox.com). 

8 

9 This file is part of cardinal_pythonlib. 

10 

11 Licensed under the Apache License, Version 2.0 (the "License"); 

12 you may not use this file except in compliance with the License. 

13 You may obtain a copy of the License at 

14 

15 https://www.apache.org/licenses/LICENSE-2.0 

16 

17 Unless required by applicable law or agreed to in writing, software 

18 distributed under the License is distributed on an "AS IS" BASIS, 

19 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 

20 See the License for the specific language governing permissions and 

21 limitations under the License. 

22 

23=============================================================================== 

24 

25**Functions to make it easy to serialize Python objects to/from JSON.** 

26 

27See ``notes_on_pickle_json.txt``. 

28 

29The standard Python representation used internally is a dictionary like this: 

30 

31.. code-block:: python 

32 

33 { 

34 __type__: 'MyClass', 

35 args: [some, positional, args], 

36 kwargs: { 

37 'some': 1, 

38 'named': 'hello', 

39 'args': [2, 3, 4], 

40 } 

41 } 

42 

43We will call this an ``InitDict``. 

44 

45Sometimes positional arguments aren't necessary and it's convenient to work 

46also with the simpler dictionary: 

47 

48.. code-block:: python 

49 

50 { 

51 'some': 1, 

52 'named': 'hello', 

53 'args': [2, 3, 4], 

54 } 

55 

56... which we'll call a ``KwargsDict``. 

57 

58""" 

59 

60import datetime 

61from enum import Enum 

62import json 

63import pprint 

64import sys 

65from typing import Any, Callable, Dict, List, TextIO, Tuple, Type 

66 

67import pendulum 

68from pendulum import Date, DateTime 

69# from pendulum.tz.timezone import Timezone 

70# from pendulum.tz.timezone_info import TimezoneInfo 

71# from pendulum.tz.transition import Transition 

72 

73from cardinal_pythonlib.logs import get_brace_style_log_with_null_handler 

74from cardinal_pythonlib.reprfunc import auto_repr 

75 

76log = get_brace_style_log_with_null_handler(__name__) 

77 

78Instance = Any 

79ClassType = Type[object] 

80 

81InitDict = Dict[str, Any] 

82KwargsDict = Dict[str, Any] 

83ArgsList = List[Any] 

84 

85ArgsKwargsTuple = Tuple[ArgsList, KwargsDict] 

86 

87InstanceToDictFnType = Callable[[Instance], Dict] 

88DictToInstanceFnType = Callable[[Dict, ClassType], Instance] 

89DefaultFactoryFnType = Callable[[], Instance] 

90InitArgsKwargsFnType = Callable[[Instance], ArgsKwargsTuple] 

91InitKwargsFnType = Callable[[Instance], KwargsDict] 

92InstanceToInitDictFnType = Callable[[Instance], InitDict] 

93 

94# ============================================================================= 

95# Constants for external use 

96# ============================================================================= 

97 

98METHOD_NO_ARGS = 'no_args' 

99METHOD_SIMPLE = 'simple' 

100METHOD_STRIP_UNDERSCORE = 'strip_underscore' 

101METHOD_PROVIDES_INIT_ARGS_KWARGS = 'provides_init_args_kwargs' 

102METHOD_PROVIDES_INIT_KWARGS = 'provides_init_kwargs' 

103 

104# ============================================================================= 

105# Constants for internal use 

106# ============================================================================= 

107 

108DEBUG = False 

109 

110ARGS_LABEL = 'args' 

111KWARGS_LABEL = 'kwargs' 

112TYPE_LABEL = '__type__' 

113 

114INIT_ARGS_KWARGS_FN_NAME = 'init_args_kwargs' 

115INIT_KWARGS_FN_NAME = 'init_kwargs' 

116 

117 

118# ============================================================================= 

119# Simple dictionary manipulation 

120# ============================================================================= 

121 

122def args_kwargs_to_initdict(args: ArgsList, kwargs: KwargsDict) -> InitDict: 

123 """ 

124 Converts a set of ``args`` and ``kwargs`` to an ``InitDict``. 

125 """ 

126 return {ARGS_LABEL: args, 

127 KWARGS_LABEL: kwargs} 

128 

129 

130def kwargs_to_initdict(kwargs: KwargsDict) -> InitDict: 

131 """ 

132 Converts a set of ``kwargs`` to an ``InitDict``. 

133 """ 

134 return {ARGS_LABEL: [], 

135 KWARGS_LABEL: kwargs} 

136 

137 

138# noinspection PyUnusedLocal 

139def obj_with_no_args_to_init_dict(obj: Any) -> InitDict: 

140 """ 

141 Creates an empty ``InitDict``, for use with an object that takes no 

142 arguments at creation. 

143 """ 

144 

145 return {ARGS_LABEL: [], 

146 KWARGS_LABEL: {}} 

147 

148 

149def strip_leading_underscores_from_keys(d: Dict) -> Dict: 

150 """ 

151 Clones a dictionary, removing leading underscores from key names. 

152 Raises ``ValueError`` if this causes an attribute conflict. 

153 """ 

154 newdict = {} 

155 for k, v in d.items(): 

156 if k.startswith('_'): 

157 k = k[1:] 

158 if k in newdict: 

159 raise ValueError(f"Attribute conflict: _{k}, {k}") 

160 newdict[k] = v 

161 return newdict 

162 

163 

164def verify_initdict(initdict: InitDict) -> None: 

165 """ 

166 Ensures that its parameter is a proper ``InitDict``, or raises 

167 ``ValueError``. 

168 """ 

169 if (not isinstance(initdict, dict) or 

170 ARGS_LABEL not in initdict or 

171 KWARGS_LABEL not in initdict): 

172 raise ValueError("Not an InitDict dictionary") 

173 

174 

175# ============================================================================= 

176# InitDict -> class instance 

177# ============================================================================= 

178 

179def initdict_to_instance(d: InitDict, cls: ClassType) -> Any: 

180 """ 

181 Converse of simple_to_dict(). 

182 Given that JSON dictionary, we will end up re-instantiating the class with 

183 

184 .. code-block:: python 

185 

186 d = {'a': 1, 'b': 2, 'c': 3} 

187 new_x = SimpleClass(**d) 

188 

189 We'll also support arbitrary creation, by using both ``*args`` and 

190 ``**kwargs``. 

191 """ 

192 args = d.get(ARGS_LABEL, []) 

193 kwargs = d.get(KWARGS_LABEL, {}) 

194 # noinspection PyArgumentList 

195 return cls(*args, **kwargs) 

196 

197 

198# ============================================================================= 

199# Class instance -> InitDict, in various ways 

200# ============================================================================= 

201 

202def instance_to_initdict_simple(obj: Any) -> InitDict: 

203 """ 

204 For use when object attributes (found in ``obj.__dict__``) should be mapped 

205 directly to the serialized JSON dictionary. Typically used for classes 

206 like: 

207 

208 .. code-block:: python 

209 

210 class SimpleClass(object): 

211 def __init__(self, a, b, c): 

212 self.a = a 

213 self.b = b 

214 self.c = c 

215 

216 Here, after 

217 

218 x = SimpleClass(a=1, b=2, c=3) 

219 

220 we will find that 

221 

222 x.__dict__ == {'a': 1, 'b': 2, 'c': 3} 

223 

224 and that dictionary is a reasonable thing to serialize to JSON as keyword 

225 arguments. 

226 

227 We'll also support arbitrary creation, by using both ``*args`` and 

228 ``**kwargs``. We may not use this format much, but it has the advantage of 

229 being an arbitrarily correct format for Python class construction. 

230 """ 

231 return kwargs_to_initdict(obj.__dict__) 

232 

233 

234def instance_to_initdict_stripping_underscores(obj: Instance) -> InitDict: 

235 """ 

236 This is appropriate when a class uses a ``'_'`` prefix for all its 

237 ``__init__`` parameters, like this: 

238 

239 .. code-block:: python 

240 

241 class UnderscoreClass(object): 

242 def __init__(self, a, b, c): 

243 self._a = a 

244 self._b = b 

245 self._c = c 

246 

247 Here, after 

248 

249 .. code-block:: python 

250 

251 y = UnderscoreClass(a=1, b=2, c=3) 

252 

253 we will find that 

254 

255 .. code-block:: python 

256 

257 y.__dict__ == {'_a': 1, '_b': 2, '_c': 3} 

258 

259 but we would like to serialize the parameters we can pass back to 

260 ``__init__``, by removing the leading underscores, like this: 

261 

262 .. code-block:: python 

263 

264 {'a': 1, 'b': 2, 'c': 3} 

265 """ 

266 return kwargs_to_initdict( 

267 strip_leading_underscores_from_keys(obj.__dict__)) 

268 

269 

270def wrap_kwargs_to_initdict(init_kwargs_fn: InitKwargsFnType, 

271 typename: str, 

272 check_result: bool = True) \ 

273 -> InstanceToInitDictFnType: 

274 """ 

275 Wraps a function producing a ``KwargsDict``, making it into a function 

276 producing an ``InitDict``. 

277 """ 

278 def wrapper(obj: Instance) -> InitDict: 

279 result = init_kwargs_fn(obj) 

280 if check_result and not isinstance(result, dict): 

281 raise ValueError( 

282 f"Class {typename} failed to provide a kwargs dict and " 

283 f"provided instead: {result!r}") 

284 return kwargs_to_initdict(init_kwargs_fn(obj)) 

285 

286 return wrapper 

287 

288 

289def wrap_args_kwargs_to_initdict(init_args_kwargs_fn: InitArgsKwargsFnType, 

290 typename: str, 

291 check_result: bool = True) \ 

292 -> InstanceToInitDictFnType: 

293 """ 

294 Wraps a function producing a ``KwargsDict``, making it into a function 

295 producing an ``InitDict``. 

296 """ 

297 def wrapper(obj: Instance) -> InitDict: 

298 result = init_args_kwargs_fn(obj) 

299 if check_result and (not isinstance(result, tuple) or 

300 not len(result) == 2 or 

301 not isinstance(result[0], list) or 

302 not isinstance(result[1], dict)): 

303 raise ValueError( 

304 f"Class {typename} failed to provide an (args, kwargs) tuple " 

305 f"and provided instead: {result!r}") 

306 return args_kwargs_to_initdict(*result) 

307 

308 return wrapper 

309 

310 

311# ============================================================================= 

312# Function to make custom instance -> InitDict functions 

313# ============================================================================= 

314 

315def make_instance_to_initdict(attributes: List[str]) -> InstanceToDictFnType: 

316 """ 

317 Returns a function that takes an object (instance) and produces an 

318 ``InitDict`` enabling its re-creation. 

319 """ 

320 def custom_instance_to_initdict(x: Instance) -> InitDict: 

321 kwargs = {} 

322 for a in attributes: 

323 kwargs[a] = getattr(x, a) 

324 return kwargs_to_initdict(kwargs) 

325 

326 return custom_instance_to_initdict 

327 

328 

329# ============================================================================= 

330# Describe how a Python class should be serialized to/from JSON 

331# ============================================================================= 

332 

333class JsonDescriptor(object): 

334 """ 

335 Describe how a Python class should be serialized to/from JSON. 

336 """ 

337 def __init__(self, 

338 typename: str, 

339 obj_to_dict_fn: InstanceToDictFnType, 

340 dict_to_obj_fn: DictToInstanceFnType, 

341 cls: ClassType, 

342 default_factory: DefaultFactoryFnType = None) -> None: 

343 self._typename = typename 

344 self._obj_to_dict_fn = obj_to_dict_fn 

345 self._dict_to_obj_fn = dict_to_obj_fn 

346 self._cls = cls 

347 self._default_factory = default_factory 

348 

349 def to_dict(self, obj: Instance) -> Dict: 

350 return self._obj_to_dict_fn(obj) 

351 

352 def to_obj(self, d: Dict) -> Instance: 

353 # noinspection PyBroadException 

354 try: 

355 return self._dict_to_obj_fn(d, self._cls) 

356 except Exception as err: 

357 log.warning( 

358 "Failed to deserialize object of type {t}; exception was {e}; " 

359 "dict was {d}; will use default factory instead", 

360 t=self._typename, e=repr(err), d=repr(d)) 

361 if self._default_factory: 

362 return self._default_factory() 

363 else: 

364 return None 

365 

366 def __repr__(self): 

367 return ( 

368 f"<{self.__class__.__qualname__}(" 

369 f"typename={self._typename!r}, " 

370 f"obj_to_dict_fn={self._obj_to_dict_fn!r}, " 

371 f"dict_to_obj_fn={self._dict_to_obj_fn!r}, " 

372 f"cls={self._cls!r}, " 

373 f"default_factory={self._default_factory!r}) " 

374 f"at {hex(id(self))}>" 

375 ) 

376 

377 

378# ============================================================================= 

379# Maintain a record of how several classes should be serialized 

380# ============================================================================= 

381 

382TYPE_MAP = {} # type: Dict[str, JsonDescriptor] 

383 

384 

385def register_class_for_json( 

386 cls: ClassType, 

387 method: str = METHOD_SIMPLE, 

388 obj_to_dict_fn: InstanceToDictFnType = None, 

389 dict_to_obj_fn: DictToInstanceFnType = initdict_to_instance, 

390 default_factory: DefaultFactoryFnType = None) -> None: 

391 """ 

392 Registers the class cls for JSON serialization. 

393 

394 - If both ``obj_to_dict_fn`` and dict_to_obj_fn are registered, the 

395 framework uses these to convert instances of the class to/from Python 

396 dictionaries, which are in turn serialized to JSON. 

397 

398 - Otherwise: 

399 

400 .. code-block:: python 

401 

402 if method == 'simple': 

403 # ... uses simple_to_dict and simple_from_dict (q.v.) 

404 

405 if method == 'strip_underscore': 

406 # ... uses strip_underscore_to_dict and simple_from_dict (q.v.) 

407 """ 

408 typename = cls.__qualname__ # preferable to __name__ 

409 # ... __name__ looks like "Thing" and is ambiguous 

410 # ... __qualname__ looks like "my.module.Thing" and is not 

411 if obj_to_dict_fn and dict_to_obj_fn: 

412 descriptor = JsonDescriptor( 

413 typename=typename, 

414 obj_to_dict_fn=obj_to_dict_fn, 

415 dict_to_obj_fn=dict_to_obj_fn, 

416 cls=cls, 

417 default_factory=default_factory) 

418 elif method == METHOD_SIMPLE: 

419 descriptor = JsonDescriptor( 

420 typename=typename, 

421 obj_to_dict_fn=instance_to_initdict_simple, 

422 dict_to_obj_fn=initdict_to_instance, 

423 cls=cls, 

424 default_factory=default_factory) 

425 elif method == METHOD_STRIP_UNDERSCORE: 

426 descriptor = JsonDescriptor( 

427 typename=typename, 

428 obj_to_dict_fn=instance_to_initdict_stripping_underscores, 

429 dict_to_obj_fn=initdict_to_instance, 

430 cls=cls, 

431 default_factory=default_factory) 

432 else: 

433 raise ValueError("Unknown method, and functions not fully specified") 

434 global TYPE_MAP 

435 TYPE_MAP[typename] = descriptor 

436 

437 

438def register_for_json(*args, **kwargs) -> Any: 

439 """ 

440 Class decorator to register classes with our JSON system. 

441 

442 - If method is ``'provides_init_args_kwargs'``, the class provides a 

443 function 

444 

445 .. code-block:: python 

446 

447 def init_args_kwargs(self) -> Tuple[List[Any], Dict[str, Any]] 

448 

449 that returns an ``(args, kwargs)`` tuple, suitable for passing to its 

450 ``__init__()`` function as ``__init__(*args, **kwargs)``. 

451 

452 - If method is ``'provides_init_kwargs'``, the class provides a function 

453 

454 .. code-block:: python 

455 

456 def init_kwargs(self) -> Dict 

457 

458 that returns a dictionary ``kwargs`` suitable for passing to its 

459 ``__init__()`` function as ``__init__(**kwargs)``. 

460 

461 - Otherwise, the method argument is as for ``register_class_for_json()``. 

462 

463 Usage looks like: 

464 

465 .. code-block:: python 

466 

467 @register_for_json(method=METHOD_STRIP_UNDERSCORE) 

468 class TableId(object): 

469 def __init__(self, db: str = '', schema: str = '', 

470 table: str = '') -> None: 

471 self._db = db 

472 self._schema = schema 

473 self._table = table 

474 

475 """ 

476 if DEBUG: 

477 print(f"register_for_json: args = {args!r}") 

478 print(f"register_for_json: kwargs = {kwargs!r}") 

479 

480 # https://stackoverflow.com/questions/653368/how-to-create-a-python-decorator-that-can-be-used-either-with-or-without-paramet # noqa 

481 # In brief, 

482 # @decorator 

483 # x 

484 # 

485 # means 

486 # x = decorator(x) 

487 # 

488 # so 

489 # @decorator(args) 

490 # x 

491 # 

492 # means 

493 # x = decorator(args)(x) 

494 

495 if len(args) == 1 and len(kwargs) == 0 and callable(args[0]): 

496 if DEBUG: 

497 print("... called as @register_for_json") 

498 # called as @decorator 

499 # ... the single argument is the class itself, e.g. Thing in: 

500 # @decorator 

501 # class Thing(object): 

502 # # ... 

503 # ... e.g.: 

504 # args = (<class '__main__.unit_tests.<locals>.SimpleThing'>,) 

505 # kwargs = {} 

506 cls = args[0] # type: ClassType 

507 register_class_for_json(cls, method=METHOD_SIMPLE) 

508 return cls 

509 

510 # Otherwise: 

511 if DEBUG: 

512 print("... called as @register_for_json(*args, **kwargs)") 

513 # called as @decorator(*args, **kwargs) 

514 # ... e.g.: 

515 # args = () 

516 # kwargs = {'method': 'provides_to_init_args_kwargs_dict'} 

517 method = kwargs.pop('method', METHOD_SIMPLE) # type: str 

518 obj_to_dict_fn = kwargs.pop('obj_to_dict_fn', None) # type: InstanceToDictFnType # noqa 

519 dict_to_obj_fn = kwargs.pop('dict_to_obj_fn', initdict_to_instance) # type: DictToInstanceFnType # noqa 

520 default_factory = kwargs.pop('default_factory', None) # type: DefaultFactoryFnType # noqa 

521 check_result = kwargs.pop('check_results', True) # type: bool 

522 

523 def register_json_class(cls_: ClassType) -> ClassType: 

524 odf = obj_to_dict_fn 

525 dof = dict_to_obj_fn 

526 if method == METHOD_PROVIDES_INIT_ARGS_KWARGS: 

527 if hasattr(cls_, INIT_ARGS_KWARGS_FN_NAME): 

528 odf = wrap_args_kwargs_to_initdict( 

529 getattr(cls_, INIT_ARGS_KWARGS_FN_NAME), 

530 typename=cls_.__qualname__, 

531 check_result=check_result 

532 ) 

533 else: 

534 raise ValueError( 

535 f"Class type {cls_} does not provide function " 

536 f"{INIT_ARGS_KWARGS_FN_NAME}") 

537 elif method == METHOD_PROVIDES_INIT_KWARGS: 

538 if hasattr(cls_, INIT_KWARGS_FN_NAME): 

539 odf = wrap_kwargs_to_initdict( 

540 getattr(cls_, INIT_KWARGS_FN_NAME), 

541 typename=cls_.__qualname__, 

542 check_result=check_result 

543 ) 

544 else: 

545 raise ValueError( 

546 f"Class type {cls_} does not provide function " 

547 f"{INIT_KWARGS_FN_NAME}") 

548 elif method == METHOD_NO_ARGS: 

549 odf = obj_with_no_args_to_init_dict 

550 register_class_for_json(cls_, 

551 method=method, 

552 obj_to_dict_fn=odf, 

553 dict_to_obj_fn=dof, 

554 default_factory=default_factory) 

555 return cls_ 

556 

557 return register_json_class 

558 

559 

560def dump_map(file: TextIO = sys.stdout) -> None: 

561 """ 

562 Prints the JSON "registered types" map to the specified file. 

563 """ 

564 pp = pprint.PrettyPrinter(indent=4, stream=file) 

565 print("Type map: ", file=file) 

566 pp.pprint(TYPE_MAP) 

567 

568 

569# ============================================================================= 

570# Hooks to implement the JSON encoding/decoding 

571# ============================================================================= 

572 

573class JsonClassEncoder(json.JSONEncoder): 

574 """ 

575 Provides a JSON encoder whose ``default`` method encodes a Python object 

576 to JSON with reference to our ``TYPE_MAP``. 

577 """ 

578 def default(self, obj: Instance) -> Any: 

579 typename = type(obj).__qualname__ # preferable to __name__, as above 

580 if typename in TYPE_MAP: 

581 descriptor = TYPE_MAP[typename] 

582 d = descriptor.to_dict(obj) 

583 if TYPE_LABEL in d: 

584 raise ValueError("Class already has attribute: " + TYPE_LABEL) 

585 d[TYPE_LABEL] = typename 

586 if DEBUG: 

587 log.debug("Serializing {!r} -> {!r}", obj, d) 

588 return d 

589 # Otherwise, nothing that we know about: 

590 return super().default(obj) 

591 

592 

593def json_class_decoder_hook(d: Dict) -> Any: 

594 """ 

595 Provides a JSON decoder that converts dictionaries to Python objects if 

596 suitable methods are found in our ``TYPE_MAP``. 

597 """ 

598 if TYPE_LABEL in d: 

599 typename = d.get(TYPE_LABEL) 

600 if typename in TYPE_MAP: 

601 if DEBUG: 

602 log.debug("Deserializing: {!r}", d) 

603 d.pop(TYPE_LABEL) 

604 descriptor = TYPE_MAP[typename] 

605 obj = descriptor.to_obj(d) 

606 if DEBUG: 

607 log.debug("... to: {!r}", obj) 

608 return obj 

609 return d 

610 

611 

612# ============================================================================= 

613# Functions for end users 

614# ============================================================================= 

615 

616def json_encode(obj: Instance, **kwargs) -> str: 

617 """ 

618 Encodes an object to JSON using our custom encoder. 

619 

620 The ``**kwargs`` can be used to pass things like ``'indent'``, for 

621 formatting. 

622 """ 

623 return json.dumps(obj, cls=JsonClassEncoder, **kwargs) 

624 

625 

626def json_decode(s: str) -> Any: 

627 """ 

628 Decodes an object from JSON using our custom decoder. 

629 """ 

630 try: 

631 return json.JSONDecoder(object_hook=json_class_decoder_hook).decode(s) 

632 except json.JSONDecodeError: 

633 log.warning("Failed to decode JSON (returning None): {!r}", s) 

634 return None 

635 

636 

637# ============================================================================= 

638# Implement JSON translation for common types 

639# ============================================================================= 

640 

641# ----------------------------------------------------------------------------- 

642# datetime.date 

643# ----------------------------------------------------------------------------- 

644 

645register_class_for_json( 

646 cls=datetime.date, 

647 obj_to_dict_fn=make_instance_to_initdict(['year', 'month', 'day']) 

648) 

649 

650# ----------------------------------------------------------------------------- 

651# datetime.datetime 

652# ----------------------------------------------------------------------------- 

653 

654register_class_for_json( 

655 cls=datetime.datetime, 

656 obj_to_dict_fn=make_instance_to_initdict([ 

657 'year', 'month', 'day', 'hour', 'minute', 'second', 'microsecond', 

658 'tzinfo' 

659 ]) 

660) 

661 

662# ----------------------------------------------------------------------------- 

663# datetime.timedelta 

664# ----------------------------------------------------------------------------- 

665 

666# Note in passing: the repr() of datetime.date and datetime.datetime look like 

667# 'datetime.date(...)' only because their repr() function explicitly does 

668# 'datetime.' + self.__class__.__name__; there's no way, it seems, to get 

669# that or __qualname__ to add the prefix automatically. 

670register_class_for_json( 

671 cls=datetime.timedelta, 

672 obj_to_dict_fn=make_instance_to_initdict([ 

673 'days', 'seconds', 'microseconds' 

674 ]) 

675) 

676 

677 

678# ----------------------------------------------------------------------------- 

679# enum.Enum 

680# ----------------------------------------------------------------------------- 

681# Since this is a family of classes, we provide a decorator. 

682 

683def enum_to_dict_fn(e: Enum) -> Dict[str, Any]: 

684 """ 

685 Converts an ``Enum`` to a ``dict``. 

686 """ 

687 return { 

688 'name': e.name 

689 } 

690 

691 

692def dict_to_enum_fn(d: Dict[str, Any], enum_class: Type[Enum]) -> Enum: 

693 """ 

694 Converts an ``dict`` to a ``Enum``. 

695 """ 

696 return enum_class[d['name']] 

697 

698 

699def register_enum_for_json(*args, **kwargs) -> Any: 

700 """ 

701 Class decorator to register ``Enum``-derived classes with our JSON system. 

702 See comments/help for ``@register_for_json``, above. 

703 """ 

704 if len(args) == 1 and len(kwargs) == 0 and callable(args[0]): 

705 # called as @register_enum_for_json 

706 cls = args[0] # type: ClassType 

707 register_class_for_json( 

708 cls, 

709 obj_to_dict_fn=enum_to_dict_fn, 

710 dict_to_obj_fn=dict_to_enum_fn 

711 ) 

712 return cls 

713 else: 

714 # called as @register_enum_for_json(*args, **kwargs) 

715 raise AssertionError("Use as plain @register_enum_for_json, " 

716 "without arguments") 

717 

718 

719# ----------------------------------------------------------------------------- 

720# pendulum.DateTime (formerly pendulum.Pendulum) 

721# ----------------------------------------------------------------------------- 

722 

723def pendulum_to_dict(p: DateTime) -> Dict[str, Any]: 

724 """ 

725 Converts a ``Pendulum`` or ``datetime`` object to a ``dict``. 

726 """ 

727 return { 

728 'iso': str(p) 

729 } 

730 

731 

732# noinspection PyUnusedLocal,PyTypeChecker 

733def dict_to_pendulum(d: Dict[str, Any], 

734 pendulum_class: ClassType) -> DateTime: 

735 """ 

736 Converts a ``dict`` object back to a ``Pendulum``. 

737 """ 

738 return pendulum.parse(d['iso']) 

739 

740 

741register_class_for_json( 

742 cls=DateTime, 

743 obj_to_dict_fn=pendulum_to_dict, 

744 dict_to_obj_fn=dict_to_pendulum 

745) 

746 

747 

748# ----------------------------------------------------------------------------- 

749# pendulum.Date 

750# ----------------------------------------------------------------------------- 

751 

752def pendulumdate_to_dict(p: Date) -> Dict[str, Any]: 

753 """ 

754 Converts a ``pendulum.Date`` object to a ``dict``. 

755 """ 

756 return { 

757 'iso': str(p) 

758 } 

759 

760 

761# noinspection PyUnusedLocal 

762def dict_to_pendulumdate(d: Dict[str, Any], 

763 pendulumdate_class: ClassType) -> Date: 

764 """ 

765 Converts a ``dict`` object back to a ``pendulum.Date``. 

766 """ 

767 # noinspection PyTypeChecker 

768 dt = pendulum.parse(d['iso']) # type: pendulum.DateTime 

769 # noinspection PyTypeChecker 

770 return dt.date() # type: pendulum.Date 

771 

772 

773register_class_for_json( 

774 cls=Date, 

775 obj_to_dict_fn=pendulumdate_to_dict, 

776 dict_to_obj_fn=dict_to_pendulumdate 

777) 

778 

779# ----------------------------------------------------------------------------- 

780# unused 

781# ----------------------------------------------------------------------------- 

782 

783# def timezone_to_initdict(x: Timezone) -> Dict[str, Any]: 

784# kwargs = { 

785# 'name': x.name, 

786# 'transitions': x.transitions, 

787# 'tzinfos': x.tzinfos, 

788# 'default_tzinfo_index': x._default_tzinfo_index, # NB different name 

789# 'utc_transition_times': x._utc_transition_times, # NB different name 

790# } 

791# return kwargs_to_initdict(kwargs) 

792 

793 

794# def timezoneinfo_to_initdict(x: TimezoneInfo) -> Dict[str, Any]: 

795# kwargs = { 

796# 'tz': x.tz, 

797# 'utc_offset': x.offset, # NB different name 

798# 'is_dst': x.is_dst, 

799# 'dst': x.dst_, # NB different name 

800# 'abbrev': x.abbrev, 

801# } 

802# return kwargs_to_initdict(kwargs) 

803 

804# register_class_for_json( 

805# cls=Transition, 

806# obj_to_dict_fn=make_instance_to_initdict([ 

807# 'unix_time', 'tzinfo_index', 'pre_time', 'time', 'pre_tzinfo_index' 

808# ]) 

809# ) 

810# register_class_for_json( 

811# cls=Timezone, 

812# obj_to_dict_fn=timezone_to_initdict 

813# ) 

814# register_class_for_json( 

815# cls=TimezoneInfo, 

816# obj_to_dict_fn=timezoneinfo_to_initdict 

817# ) 

818 

819 

820# ============================================================================= 

821# Testing 

822# ============================================================================= 

823 

824def simple_eq(one: Instance, two: Instance, attrs: List[str]) -> bool: 

825 """ 

826 Test if two objects are equal, based on a comparison of the specified 

827 attributes ``attrs``. 

828 """ 

829 return all(getattr(one, a) == getattr(two, a) for a in attrs) 

830 

831 

832def unit_tests(): 

833 

834 class BaseTestClass(object): 

835 def __repr__(self) -> str: 

836 return auto_repr(self, with_addr=True) 

837 

838 def __str__(self) -> str: 

839 return repr(self) 

840 

841 @register_for_json 

842 class SimpleThing(BaseTestClass): 

843 def __init__(self, a, b, c, d: datetime.datetime = None): 

844 self.a = a 

845 self.b = b 

846 self.c = c 

847 self.d = d or datetime.datetime.now() 

848 

849 def __eq__(self, other: 'SimpleThing') -> bool: 

850 return simple_eq(self, other, ['a', 'b', 'c', 'd']) 

851 

852 # If you comment out the decorator for this derived class, serialization 

853 # will fail, and that is a good thing (derived classes shouldn't be 

854 # serialized on a "have a try" basis). 

855 @register_for_json 

856 class DerivedThing(BaseTestClass): 

857 def __init__(self, a, b, c, d: datetime.datetime = None, e: int = 5, 

858 f: datetime.date = None): 

859 self.a = a 

860 self.b = b 

861 self.c = c 

862 self.d = d or datetime.datetime.now() 

863 self.e = e 

864 self.f = f or datetime.date.today() 

865 

866 def __eq__(self, other: 'SimpleThing') -> bool: 

867 return simple_eq(self, other, ['a', 'b', 'c', 'd', 'e']) 

868 

869 @register_for_json(method=METHOD_STRIP_UNDERSCORE) 

870 class UnderscoreThing(BaseTestClass): 

871 def __init__(self, a, b, c): 

872 self._a = a 

873 self._b = b 

874 self._c = c 

875 

876 # noinspection PyProtectedMember 

877 def __eq__(self, other: 'UnderscoreThing') -> bool: 

878 return simple_eq(self, other, ['_a', '_b', '_c']) 

879 

880 @register_for_json(method=METHOD_PROVIDES_INIT_ARGS_KWARGS) 

881 class InitDictThing(BaseTestClass): 

882 def __init__(self, a, b, c): 

883 self.p = a 

884 self.q = b 

885 self.r = c 

886 

887 def __eq__(self, other: 'InitDictThing') -> bool: 

888 return simple_eq(self, other, ['p', 'q', 'r']) 

889 

890 def init_args_kwargs(self) -> ArgsKwargsTuple: 

891 args = [] 

892 kwargs = {'a': self.p, 'b': self.q, 'c': self.r} 

893 return args, kwargs 

894 

895 @register_for_json(method=METHOD_PROVIDES_INIT_KWARGS) 

896 class KwargsDictThing(BaseTestClass): 

897 def __init__(self, a, b, c): 

898 self.p = a 

899 self.q = b 

900 self.r = c 

901 

902 def __eq__(self, other): 

903 return simple_eq(self, other, ['p', 'q', 'r']) 

904 

905 def init_kwargs(self) -> KwargsDict: 

906 return {'a': self.p, 'b': self.q, 'c': self.r} 

907 

908 def check_json(start: Any) -> None: 

909 print(repr(start)) 

910 encoded = json_encode(start) 

911 print("-> JSON: " + repr(encoded)) 

912 resurrected = json_decode(encoded) 

913 print("-> resurrected: " + repr(resurrected)) 

914 assert resurrected == start 

915 print("... OK") 

916 print() 

917 

918 check_json(SimpleThing(1, 2, 3)) 

919 check_json(DerivedThing(1, 2, 3, e=6)) 

920 check_json(UnderscoreThing(1, 2, 3)) 

921 check_json(InitDictThing(1, 2, 3)) 

922 check_json(KwargsDictThing(1, 2, 3)) 

923 

924 dump_map() 

925 

926 print("\nAll OK.\n") 

927 

928 

929if __name__ == '__main__': 

930 unit_tests()