Coverage for /home/martinb/.local/share/virtualenvs/camcops/lib/python3.6/site-packages/cardinal_pythonlib/json/serialize.py : 35%

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
4"""
5===============================================================================
7 Original code copyright (C) 2009-2021 Rudolf Cardinal (rudolf@pobox.com).
9 This file is part of cardinal_pythonlib.
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
15 https://www.apache.org/licenses/LICENSE-2.0
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.
23===============================================================================
25**Functions to make it easy to serialize Python objects to/from JSON.**
27See ``notes_on_pickle_json.txt``.
29The standard Python representation used internally is a dictionary like this:
31.. code-block:: python
33 {
34 __type__: 'MyClass',
35 args: [some, positional, args],
36 kwargs: {
37 'some': 1,
38 'named': 'hello',
39 'args': [2, 3, 4],
40 }
41 }
43We will call this an ``InitDict``.
45Sometimes positional arguments aren't necessary and it's convenient to work
46also with the simpler dictionary:
48.. code-block:: python
50 {
51 'some': 1,
52 'named': 'hello',
53 'args': [2, 3, 4],
54 }
56... which we'll call a ``KwargsDict``.
58"""
60import datetime
61from enum import Enum
62import json
63import pprint
64import sys
65from typing import Any, Callable, Dict, List, TextIO, Tuple, Type
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
73from cardinal_pythonlib.logs import get_brace_style_log_with_null_handler
74from cardinal_pythonlib.reprfunc import auto_repr
76log = get_brace_style_log_with_null_handler(__name__)
78Instance = Any
79ClassType = Type[object]
81InitDict = Dict[str, Any]
82KwargsDict = Dict[str, Any]
83ArgsList = List[Any]
85ArgsKwargsTuple = Tuple[ArgsList, KwargsDict]
87InstanceToDictFnType = Callable[[Instance], Dict]
88DictToInstanceFnType = Callable[[Dict, ClassType], Instance]
89DefaultFactoryFnType = Callable[[], Instance]
90InitArgsKwargsFnType = Callable[[Instance], ArgsKwargsTuple]
91InitKwargsFnType = Callable[[Instance], KwargsDict]
92InstanceToInitDictFnType = Callable[[Instance], InitDict]
94# =============================================================================
95# Constants for external use
96# =============================================================================
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'
104# =============================================================================
105# Constants for internal use
106# =============================================================================
108DEBUG = False
110ARGS_LABEL = 'args'
111KWARGS_LABEL = 'kwargs'
112TYPE_LABEL = '__type__'
114INIT_ARGS_KWARGS_FN_NAME = 'init_args_kwargs'
115INIT_KWARGS_FN_NAME = 'init_kwargs'
118# =============================================================================
119# Simple dictionary manipulation
120# =============================================================================
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}
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}
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 """
145 return {ARGS_LABEL: [],
146 KWARGS_LABEL: {}}
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
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")
175# =============================================================================
176# InitDict -> class instance
177# =============================================================================
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
184 .. code-block:: python
186 d = {'a': 1, 'b': 2, 'c': 3}
187 new_x = SimpleClass(**d)
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)
198# =============================================================================
199# Class instance -> InitDict, in various ways
200# =============================================================================
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:
208 .. code-block:: python
210 class SimpleClass(object):
211 def __init__(self, a, b, c):
212 self.a = a
213 self.b = b
214 self.c = c
216 Here, after
218 x = SimpleClass(a=1, b=2, c=3)
220 we will find that
222 x.__dict__ == {'a': 1, 'b': 2, 'c': 3}
224 and that dictionary is a reasonable thing to serialize to JSON as keyword
225 arguments.
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__)
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:
239 .. code-block:: python
241 class UnderscoreClass(object):
242 def __init__(self, a, b, c):
243 self._a = a
244 self._b = b
245 self._c = c
247 Here, after
249 .. code-block:: python
251 y = UnderscoreClass(a=1, b=2, c=3)
253 we will find that
255 .. code-block:: python
257 y.__dict__ == {'_a': 1, '_b': 2, '_c': 3}
259 but we would like to serialize the parameters we can pass back to
260 ``__init__``, by removing the leading underscores, like this:
262 .. code-block:: python
264 {'a': 1, 'b': 2, 'c': 3}
265 """
266 return kwargs_to_initdict(
267 strip_leading_underscores_from_keys(obj.__dict__))
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))
286 return wrapper
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)
308 return wrapper
311# =============================================================================
312# Function to make custom instance -> InitDict functions
313# =============================================================================
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)
326 return custom_instance_to_initdict
329# =============================================================================
330# Describe how a Python class should be serialized to/from JSON
331# =============================================================================
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
349 def to_dict(self, obj: Instance) -> Dict:
350 return self._obj_to_dict_fn(obj)
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
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 )
378# =============================================================================
379# Maintain a record of how several classes should be serialized
380# =============================================================================
382TYPE_MAP = {} # type: Dict[str, JsonDescriptor]
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.
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.
398 - Otherwise:
400 .. code-block:: python
402 if method == 'simple':
403 # ... uses simple_to_dict and simple_from_dict (q.v.)
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
438def register_for_json(*args, **kwargs) -> Any:
439 """
440 Class decorator to register classes with our JSON system.
442 - If method is ``'provides_init_args_kwargs'``, the class provides a
443 function
445 .. code-block:: python
447 def init_args_kwargs(self) -> Tuple[List[Any], Dict[str, Any]]
449 that returns an ``(args, kwargs)`` tuple, suitable for passing to its
450 ``__init__()`` function as ``__init__(*args, **kwargs)``.
452 - If method is ``'provides_init_kwargs'``, the class provides a function
454 .. code-block:: python
456 def init_kwargs(self) -> Dict
458 that returns a dictionary ``kwargs`` suitable for passing to its
459 ``__init__()`` function as ``__init__(**kwargs)``.
461 - Otherwise, the method argument is as for ``register_class_for_json()``.
463 Usage looks like:
465 .. code-block:: python
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
475 """
476 if DEBUG:
477 print(f"register_for_json: args = {args!r}")
478 print(f"register_for_json: kwargs = {kwargs!r}")
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)
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
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
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_
557 return register_json_class
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)
569# =============================================================================
570# Hooks to implement the JSON encoding/decoding
571# =============================================================================
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)
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
612# =============================================================================
613# Functions for end users
614# =============================================================================
616def json_encode(obj: Instance, **kwargs) -> str:
617 """
618 Encodes an object to JSON using our custom encoder.
620 The ``**kwargs`` can be used to pass things like ``'indent'``, for
621 formatting.
622 """
623 return json.dumps(obj, cls=JsonClassEncoder, **kwargs)
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
637# =============================================================================
638# Implement JSON translation for common types
639# =============================================================================
641# -----------------------------------------------------------------------------
642# datetime.date
643# -----------------------------------------------------------------------------
645register_class_for_json(
646 cls=datetime.date,
647 obj_to_dict_fn=make_instance_to_initdict(['year', 'month', 'day'])
648)
650# -----------------------------------------------------------------------------
651# datetime.datetime
652# -----------------------------------------------------------------------------
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)
662# -----------------------------------------------------------------------------
663# datetime.timedelta
664# -----------------------------------------------------------------------------
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)
678# -----------------------------------------------------------------------------
679# enum.Enum
680# -----------------------------------------------------------------------------
681# Since this is a family of classes, we provide a decorator.
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 }
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']]
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")
719# -----------------------------------------------------------------------------
720# pendulum.DateTime (formerly pendulum.Pendulum)
721# -----------------------------------------------------------------------------
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 }
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'])
741register_class_for_json(
742 cls=DateTime,
743 obj_to_dict_fn=pendulum_to_dict,
744 dict_to_obj_fn=dict_to_pendulum
745)
748# -----------------------------------------------------------------------------
749# pendulum.Date
750# -----------------------------------------------------------------------------
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 }
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
773register_class_for_json(
774 cls=Date,
775 obj_to_dict_fn=pendulumdate_to_dict,
776 dict_to_obj_fn=dict_to_pendulumdate
777)
779# -----------------------------------------------------------------------------
780# unused
781# -----------------------------------------------------------------------------
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)
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)
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# )
820# =============================================================================
821# Testing
822# =============================================================================
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)
832def unit_tests():
834 class BaseTestClass(object):
835 def __repr__(self) -> str:
836 return auto_repr(self, with_addr=True)
838 def __str__(self) -> str:
839 return repr(self)
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()
849 def __eq__(self, other: 'SimpleThing') -> bool:
850 return simple_eq(self, other, ['a', 'b', 'c', 'd'])
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()
866 def __eq__(self, other: 'SimpleThing') -> bool:
867 return simple_eq(self, other, ['a', 'b', 'c', 'd', 'e'])
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
876 # noinspection PyProtectedMember
877 def __eq__(self, other: 'UnderscoreThing') -> bool:
878 return simple_eq(self, other, ['_a', '_b', '_c'])
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
887 def __eq__(self, other: 'InitDictThing') -> bool:
888 return simple_eq(self, other, ['p', 'q', 'r'])
890 def init_args_kwargs(self) -> ArgsKwargsTuple:
891 args = []
892 kwargs = {'a': self.p, 'b': self.q, 'c': self.r}
893 return args, kwargs
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
902 def __eq__(self, other):
903 return simple_eq(self, other, ['p', 'q', 'r'])
905 def init_kwargs(self) -> KwargsDict:
906 return {'a': self.p, 'b': self.q, 'c': self.r}
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()
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))
924 dump_map()
926 print("\nAll OK.\n")
929if __name__ == '__main__':
930 unit_tests()