Skip to content

Commit

Permalink
Support overrides for dataclases and other init-based types
Browse files Browse the repository at this point in the history
  • Loading branch information
Maxim Avanov committed Apr 10, 2020
1 parent ac67cb9 commit 576deeb
Show file tree
Hide file tree
Showing 6 changed files with 58 additions and 15 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.rst
Expand Up @@ -2,6 +2,12 @@
CHANGELOG
=========

0.22.0
===============

* Support for overrides of init-based types (and dataclasses).


0.21.0
===============

Expand Down
12 changes: 12 additions & 0 deletions README.rst
Expand Up @@ -24,6 +24,8 @@ Typeit

`typeit` brings typed data into your project.

Start using it by automatically generating types for your JSON payloads:

.. code-block:: bash
$ echo '{"first-name": "Hello", "initial": null, "last_name": "World"}' | typeit gen
Expand All @@ -50,6 +52,16 @@ The snipped above produces output similar to this:
mk_main, serialize_main = TypeConstructor & overrides ^ Main
Use these functions to construct and serialize your payloads:

.. code-block:: python
payload = {"first-name": "Hello", "initial": None, "last_name": "World"}
data = mk_main(payload)
assert isinstance(data, Main)
assert serialize_main(data) == payload
Documentation
-------------
Expand Down
4 changes: 2 additions & 2 deletions docs/conf.py
Expand Up @@ -51,9 +51,9 @@
# built documents.
#
# The short X.Y version.
version = '0.21'
version = '0.22'
# The full version, including alpha/beta/rc tags.
release = '0.21.0'
release = '0.22.0'

# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Expand Up @@ -38,7 +38,7 @@ def requirements(at_path: Path):
# ----------------------------

setup(name='typeit',
version='0.21.0',
version='0.22.0',
description='typeit brings typed data into your project',
long_description=README,
classifiers=[
Expand Down
8 changes: 6 additions & 2 deletions tests/parser/test_dataclasses.py
Expand Up @@ -13,12 +13,16 @@ class InventoryItem:
unit_price: float
quantity_on_hand: int = 0

mk_inv, serialize_inv = typeit.TypeConstructor(InventoryItem)
overrides = {
(InventoryItem, 'quantity_on_hand'): 'quantity'
}

mk_inv, serialize_inv = typeit.TypeConstructor.override(overrides).apply_on(InventoryItem)

serialized = {
'name': 'test',
'unit_price': 1.0,
'quantity_on_hand': 5,
'quantity': 5,
}
x = mk_inv(serialized)
assert isinstance(x, InventoryItem)
Expand Down
41 changes: 31 additions & 10 deletions typeit/parser.py
Expand Up @@ -33,7 +33,10 @@
flags._Flag,
# new type extension
TypeExtension,
Mapping[property, str],
Union[
Mapping[property, str], # overrides syntax for NamedTuples
Mapping[Tuple[Type, str], str] # overrides syntax for dataclasses and init-based hints
],
]


Expand Down Expand Up @@ -353,6 +356,12 @@ def _maybe_node_for_dict(
return None, memo


_type_hints_getter = lambda x: list(filter(
lambda x: x[1] is not NoneType,
get_type_hints(x).items()
))


def _maybe_node_for_user_type(
typ: Type[iface.IType],
overrides: OverridesT,
Expand All @@ -364,6 +373,10 @@ def _maybe_node_for_user_type(
global_name_overrider: Callable[[str], str] = overrides.get(flags.GlobalNameOverride, flags.Identity)

if is_named_tuple(typ):
hints_source = typ
attribute_hints = _type_hints_getter(hints_source)
get_override_identifier = lambda x: getattr(typ, x)

deserialize_overrides = pmap({
# try to get a specific override for a field, if it doesn't exist, use the global modifier
overrides.get(
Expand All @@ -372,23 +385,31 @@ def _maybe_node_for_user_type(
): python_field_name
for python_field_name in typ._fields
})
hints_source = typ

# apply a local optimisation that discards `deserialize_overrides`
# if there is no difference with the original field_names;
# it is done to occupy less memory with unnecessary mappings
if deserialize_overrides == pmap({x: x for x in typ._fields}):
deserialize_overrides = pmap({})
else:
# use init-based types;
# note that overrides are not going to work without named tuples
deserialize_overrides = pmap({})
# use init-based types
hints_source = typ.__init__
attribute_hints = _type_hints_getter(hints_source)
get_override_identifier = lambda x: (typ, x)

attribute_hints = list(filter(
lambda x: x[1] is not NoneType,
get_type_hints(hints_source).items()
))
deserialize_overrides = pmap({
# try to get a specific override for a field, if it doesn't exist, use the global modifier
overrides.get(
(typ, python_field_name),
global_name_overrider(python_field_name)
): python_field_name
for python_field_name, _ in attribute_hints
})
# apply a local optimisation that discards `deserialize_overrides`
# if there is no difference with the original field_names;
# it is done to occupy less memory with unnecessary mappings
if deserialize_overrides == pmap({x: x for x, _ in attribute_hints}):
deserialize_overrides = pmap({})

type_schema = schema.nodes.SchemaNode(
schema.types.Structure(
Expand All @@ -402,7 +423,7 @@ def _maybe_node_for_user_type(
globally_modified_field_name = global_name_overrider(field_name)
# apply field override, if available
if deserialize_overrides:
field = getattr(typ, field_name)
field = get_override_identifier(field_name)
serialized_field_name = overrides.get(field, globally_modified_field_name)
else:
serialized_field_name = globally_modified_field_name
Expand Down

0 comments on commit 576deeb

Please sign in to comment.