|
1 |
| -from abc import ABC |
| 1 | +from abc import ABC, ABCMeta |
2 | 2 | from logging import getLogger
|
3 | 3 |
|
4 | 4 | import json
|
5 | 5 | import inspect
|
6 | 6 | import functools
|
7 |
| -from typing import Union, Iterable, List, Mapping, Dict, Any |
| 7 | +from typing import TypeVar, Union, Iterable, List, Mapping, Dict, Set, Any |
8 | 8 |
|
9 | 9 | # There are some weird (probably resolvable) errors during object cloning if this is an
|
10 | 10 | # instance variable of DictSerializable.
|
11 | 11 | logger = getLogger(__name__)
|
12 | 12 |
|
| 13 | +DictSerializableType = TypeVar("DictSerializableType", bound="DictSerializable") |
13 | 14 |
|
14 |
| -class DictSerializable(ABC): |
15 |
| - """A base class for objects that can be represented as a dictionary and serialized.""" |
16 | 15 |
|
17 |
| - typ = NotImplemented |
18 |
| - skip = set() |
| 16 | +class DictSerializableMeta(ABCMeta): |
| 17 | + """Metaclass for tracking DictSerializable type string to class mappings.""" |
| 18 | + |
| 19 | + _class: Dict[str, type] = {} |
| 20 | + |
| 21 | + def __new__(mcs, name, bases, *args, |
| 22 | + typ: str = None, skip: Set[str] = frozenset(), |
| 23 | + **kwargs): # noqa: D102 |
| 24 | + return super().__new__(mcs, name, bases, *args, **kwargs) |
| 25 | + |
| 26 | + def __init__(cls, name, bases, *args, typ: str = None, skip: Set[str] = frozenset(), **kwargs): |
| 27 | + super().__init__(name, bases, *args, **kwargs) |
| 28 | + if typ is not None: |
| 29 | + cls.typ = typ |
| 30 | + cls._class[typ] = cls |
| 31 | + elif not hasattr(cls, "typ"): |
| 32 | + cls.typ = NotImplementedError |
| 33 | + cls.skip = {x for b in bases for x in getattr(b, 'skip', {})} | skip |
| 34 | + |
| 35 | + @property |
| 36 | + def class_mapping(cls) -> Dict[str, type]: |
| 37 | + """ |
| 38 | + Return class typ string -> class map for DictSerializable and its descendants. |
| 39 | +
|
| 40 | + Note that is actually returns a copy of the internal dict to avoid accidental breakage. |
| 41 | +
|
| 42 | + Returns |
| 43 | + ------- |
| 44 | + Dict[str, type] |
| 45 | + The mapping from typ string to class |
| 46 | +
|
| 47 | + """ |
| 48 | + return cls._class.copy() |
| 49 | + |
| 50 | + |
| 51 | +class DictSerializable(ABC, metaclass=DictSerializableMeta): |
| 52 | + """A base class for objects that can be represented as a dictionary and serialized.""" |
19 | 53 |
|
20 | 54 | @classmethod
|
21 |
| - def from_dict(cls, d: Mapping[str, Any]) -> "DictSerializable": |
| 55 | + def from_dict(cls, d: Mapping[str, Any]) -> DictSerializableType: |
22 | 56 | """
|
23 | 57 | Reconstitute the object from a dictionary.
|
24 | 58 |
|
@@ -111,13 +145,14 @@ def build(d: Mapping[str, Any]) -> "DictSerializable":
|
111 | 145 | def __repr__(self) -> str:
|
112 | 146 | object_dict = self.as_dict()
|
113 | 147 | # as_dict() skips over keys in `skip`, but they should be in the representation.
|
114 |
| - skipped_keys = {x.lstrip('_') for x in vars(self) if x in self.skip} |
| 148 | + skipped_keys = {x.lstrip('_') for x in self.skip} |
115 | 149 | for key in skipped_keys:
|
116 | 150 | skipped_field = getattr(self, key, None)
|
117 | 151 | object_dict[key] = self._name_repr(skipped_field)
|
118 | 152 | return str(object_dict)
|
119 | 153 |
|
120 |
| - def _name_repr(self, entity: Union[Iterable["DictSerializable"], "DictSerializable"]) -> str: |
| 154 | + def _name_repr(self, |
| 155 | + entity: Union[Iterable[DictSerializableType], DictSerializableType]) -> str: |
121 | 156 | """
|
122 | 157 | A representation of an object or a list of objects that uses the name and type.
|
123 | 158 |
|
|
0 commit comments