Last active
July 24, 2019 22:44
-
-
Save malcolmgreaves/d71ae1f09075812e54d8ec54a5613616 to your computer and use it in GitHub Desktop.
There and back again: a namedtuple's tale. This is a module for serializing and deserializing generic namedtuple instances.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
There and back again: a namedtuple's tale. This is a module for serializing and deserializing generic | |
namedtuple instances. | |
The `serialize` and `deserialize` functions allow one to turn any `namedtuple` into a dictionary and to go | |
from such a `dict` to an instance of that `namedtuple`. These functions also work over `Sequence` & `Dict` | |
types: recursively exploring their structures to `serialize` or `deserialize` as-appropriate. | |
An example showing how to use this module is below: | |
``` | |
import json | |
from typing import Sequence | |
from namedtuple_fmt import serialize, deserialize | |
X = NamedTuple('X', [('msg',str)]) | |
json_str="""{"msg": "This is the first message"}""" | |
first_msg = deserialize(json.loads(json_str), X) | |
print(first_msg.msg) | |
print(deserialize(serialize(first_msg)) == X("This is the first message")) | |
print(deserialize(json.loads(json.dumps(serialize(first_msg)))) == X("This is the first message")) | |
json_str="""[{"msg": "This is the first message"},{"msg": "This is the 2nd message"}]""" | |
messages = deserialize(json.loads(json_str), Sequence[X]) | |
print(f"{len(messages)} messages") | |
print('\n'.join(map(lambda x: x.msg, messages)) | |
``` | |
""" | |
from typing import (Any, | |
Iterable, | |
Type, | |
Optional, | |
Tuple, | |
Sequence, | |
Dict) | |
def is_namedtuple(x: Any) -> bool: | |
"""Check to see if a value is either an instance of or type for a namedtuple. | |
This function evaluates to `True` in two cases. The first case is if the value is a `type` | |
and that type is a subtype of either `collections.namedtuple` or `typing.NamedTuple`. The | |
second case is when the input is not a `type`, but the input is an instance of a namedtuple | |
type. I.e. the input is an instantiated value of some `collections.namedtuple` or | |
`typing.NamedTuple`. | |
If neither of the above cases holds, then this function evaluates to `False`. | |
NOTE: This function was pulled from the following StackOverflow post: | |
https://stackoverflow.com/a/2166841 | |
""" | |
if x is None: | |
return False | |
if isinstance(x, type): | |
t = x | |
else: | |
t = type(x) | |
b = t.__bases__ | |
if len(b) != 1 or b[0] != tuple: | |
return False | |
if getattr(t, '_make', None) is None: | |
return False | |
f = getattr(t, '_fields', None) | |
if not isinstance(f, tuple): | |
return False | |
return all(type(n) == str for n in f) | |
def is_typed_namedtuple(x: Any) -> bool: | |
"""Check to see if a value is a `typing.NamedTuple` instance or `type`. | |
This function evaluates to `True` if the input is either a `type` that derives from | |
`typing.NamedTuple` or if the input is an instantiated `NamedTuple`. In all other cases | |
this function evaluates to `False`. | |
""" | |
return is_namedtuple(x) and getattr(x, '_field_types', None) is not None | |
def serialize(value: Any) -> Any: | |
"""Attempts to convert the `value` into an equivalent `dict` structure. | |
NOTE: If the value is not a namedtuple, dict, enum, or iterable, then the value is returned as-is. | |
""" | |
if is_namedtuple(value): | |
return {k: serialize(raw_val) for k, raw_val in value._asdict().items()} | |
elif isinstance(value, Dict): | |
return {serialize(k): serialize(v) for k, v in value.items()} | |
elif isinstance(value, Iterable) and not isinstance(value, str): | |
return list(map(serialize, value)) | |
elif isinstance(value, Enum): | |
# serialize the enum value's name as it's a better identifier than the | |
# actual enum value, which is usually inconsequential and arbitrary | |
# additionally, the name will _always_ be a str, so we can easily | |
# serialize & deserialize it | |
return value.name | |
else: | |
return value | |
def deserialize(type_value: Type, value: Any) -> Any: | |
"""Does final conversion of the `dict`-like `value` into an instance of `type_value`. | |
NOTE: If the input type `type_value` is a sequence, then deserialization is attempted on each | |
element. If it is a `dict`, then deserialization is attempted on each key and value. If this | |
specified type is a namedtuple or enum, then it will be appropriately handled. | |
Values without these explicit types are returned as-is. | |
""" | |
if is_namedtuple(type_value): | |
return _namedtuple_from_dict(type_value, value) | |
elif isinstance(type_value, type) and issubclass(type_value, Dict): | |
k_type, v_type = type_value.__args__ # type: ignore | |
return { | |
deserialize(k_type, k): deserialize(v_type, v) for k, v in value.items() | |
} | |
elif ( | |
isinstance(type_value, type) | |
and issubclass(type_value, Tuple) # type: ignore | |
and not issubclass(type_value, str) | |
): | |
tuple_type_args = type_value.__args__ | |
converted = map( | |
lambda type_val_pair: deserialize(type_val_pair[0], type_val_pair[1]), | |
zip(tuple_type_args, value), | |
) | |
return tuple(converted) | |
elif ( | |
isinstance(type_value, type) | |
and issubclass(type_value, Iterable) | |
and not issubclass(type_value, str) | |
): | |
i_type, = type_value.__args__ # type: ignore | |
converted = map(lambda x: deserialize(i_type, x), value) | |
if issubclass(type_value, Set): | |
return set(converted) | |
else: | |
return list(converted) | |
elif isinstance(type_value, type) and issubclass(type_value, Enum): | |
# instead of serializing the enum's _value_, we serialize it's _name_ | |
# so we can obtain the actual _value_ by looking in the enum type's __dict__ | |
# attribute with our supplied name | |
return type_value[value] # type: ignore | |
else: | |
return value | |
def _namedtuple_from_dict(namedtuple_type: Type, data: dict) -> Any: | |
"""Initializes an instance of the given namedtuple type from a dict of its names and values. | |
The `namedtuple_type` must be a valid namedtuple type object. The dictionary, `data`, must | |
contain keys that match 1-to-1 with the field names of the supplied namedtuple type. Moreover, | |
the values for these keys must be appropriate. | |
Raises | |
------ | |
TypeError: If the type `namedtuple_type` is not a namedtuple type. Notability, this means | |
it must have field names accessible via `._fields` and a constructor, which | |
accepts a sequence of valid field types (in the field names order), accessible | |
via `._make`. | |
""" | |
if is_namedtuple(namedtuple_type): | |
try: | |
field_values = tuple(_values_for_namedtuple(namedtuple_type, data)) | |
return namedtuple_type._make(field_values) # type: ignore | |
except AttributeError as ae: | |
raise TypeError("Did you pass in a valid NamedTuple type? It needs ._field_types " | |
"to return the list of valid field names & expected types! " | |
"And ._make to accept the initialization values.", ae) | |
else: | |
raise TypeError(f"Expecting a type derived from typing.NamedTuple or " | |
f"collections.namedtuple not a: {namedtuple_type}") | |
def _values_for_namedtuple(type_data: Type, data: dict) -> Iterable: | |
"""Instantiates a namedtuple instance from a dictionary of field names & their respective values. | |
""" | |
field_name_maybe_type = _field_name_optional_types(type_data) | |
for field_name, field_type in field_name_maybe_type: | |
value = data[field_name] | |
yield deserialize(type_value=field_type, value=value) | |
def _field_name_optional_types(namedtuple_type: Type) -> Sequence[Tuple[str, Optional[type]]]: | |
"""Obtains a list of (field_name, option(field_type)) pairs. The field_type is non-None iff | |
the input namedtuple type is a `typing.NamedTuple`. Otherwise this typing information isn't | |
present at runtime: this is indicated by a `None` value.""" | |
if is_typed_namedtuple(namedtuple_type): | |
return list(namedtuple_type._field_types.items()) # type: ignore | |
else: | |
return list(map(lambda f: (f, None), namedtuple_type._fields)) # type: ignore |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment