Skip to content

Commit

Permalink
Merge pull request #7 from hgromer/dev
Browse files Browse the repository at this point in the history
[0.4.0] delegates are now functions to avoid creating a ton of classes
  • Loading branch information
hgromer authored Feb 18, 2022
2 parents e95374b + bbb33a8 commit 5587003
Show file tree
Hide file tree
Showing 8 changed files with 97 additions and 175 deletions.
17 changes: 5 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,19 +153,17 @@ result = marshal.unmarshal(TestWithValidate, {'name': 'foo'})

This can be used to validate the python object right at construction, potentially raising an error if any of the fields have invalid values

It's also possible to register your own custom unmarshaler for specific user defined classes.
It's also possible to register your own custom unmarshaler for specific user defined classes by passing in a function pointer that will "resolve" the raw data

```python
from dataclasses import dataclass

from pymarshaler.arg_delegates import ArgBuilderDelegate
from pymarshaler.marshal import Marshal


@dataclass
class ClassWithMessage:

message: str
message: str


class ClassWithCustomDelegate:
Expand All @@ -174,17 +172,12 @@ class ClassWithCustomDelegate:
self.message_obj = message_obj


class CustomDelegate(ArgBuilderDelegate):

def __init__(self, cls):
super().__init__(cls)

def resolve(self, data):
return ClassWithCustomDelegate(ClassWithMessage(data['message']))
def custom_delegate(data):
return ClassWithCustomDelegate(ClassWithMessage(data['message']))


marshal = Marshal()
marshal.register_delegate(ClassWithCustomDelegate, CustomDelegate)
marshal.register_delegate(ClassWithCustomDelegate, custom_delegate)
result = marshal.unmarshal(ClassWithCustomDelegate, {'message': 'Hello from the custom delegate!'})
print(result.message_obj)
>>> 'Hello from the custom delegate!'
Expand Down
6 changes: 3 additions & 3 deletions pymarshaler/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
__version__ = '0.3.3'
__version__ = '0.4.0'
__all__ = ['Marshal', 'utils', 'arg_delegates', 'errors']

from pymarshaler.marshal import Marshal
from pymarshaler import utils
from pymarshaler import arg_delegates
from pymarshaler import errors
from pymarshaler import utils
from pymarshaler.marshal import Marshal
156 changes: 46 additions & 110 deletions pymarshaler/arg_delegates.py
Original file line number Diff line number Diff line change
@@ -1,130 +1,66 @@
import datetime
import typing

import dateutil.parser as parser

from pymarshaler.errors import UnknownFieldError
from pymarshaler.utils import get_init_params


class ArgBuilderDelegate:

def __init__(self, cls):
self.cls = cls

def resolve(self, data):
raise NotImplementedError(f'{ArgBuilderDelegate.__name__} has no implementation of resolve')


class FunctionalArgBuilderDelegate(ArgBuilderDelegate):

def __init__(self, cls, func):
super().__init__(cls)
self.func = func

def resolve(self, data):
raise NotImplementedError(f'{FunctionalArgBuilderDelegate.__name__} has no implementation of resolve')


class EnumArgBuilderDelegate(ArgBuilderDelegate):

def resolve(self, data):
for v in self.cls.__members__.values():
if v.value == data:
return v
raise UnknownFieldError(f'Invalid value {data} for enum {self.cls.__name__}')


class ListArgBuilderDelegate(FunctionalArgBuilderDelegate):

def __init__(self, cls, func):
super().__init__(cls, func)

def resolve(self, data: typing.List):
inner_type = self.cls.__args__[0]
return [self.func(inner_type, x) for x in data]


class SetArgBuilderDelegate(FunctionalArgBuilderDelegate):

def __init__(self, cls, func):
super().__init__(cls, func)

def resolve(self, data: typing.Set):
inner_type = self.cls.__args__[0]
return {self.func(inner_type, x) for x in data}


class TupleArgBuilderDelegate(FunctionalArgBuilderDelegate):

def __init__(self, cls, func):
super().__init__(cls, func)

def resolve(self, data: typing.Tuple):
inner_type = self.cls.__args__[0]
return (self.func(inner_type, x) for x in data)


class DictArgBuilderDelegate(FunctionalArgBuilderDelegate):

def __init__(self, cls, func):
super().__init__(cls, func)
def enum_delegate(cls, data, ignore_func):
for v in cls.__members__.values():
if v.value == data:
return v
raise UnknownFieldError(f'Invalid value {data} for enum {cls.__name__}')

def resolve(self, data: dict):
key_type = self.cls.__args__[0]
value_type = self.cls.__args__[1]
return {
self.func(key_type, key): self.func(value_type, value) for key, value in data.items()
}

def list_delegate(cls, data, func):
inner_type = cls.__args__[0]
return [func(inner_type, x) for x in data]

class BuiltinArgBuilderDelegate(ArgBuilderDelegate):

def __init__(self, cls):
super().__init__(cls)
def set_builder_delegate(cls, data, func):
inner_type = cls.__args__[0]
return {func(inner_type, x) for x in data}

def resolve(self, data):
if data is None:
return None
else:
return self.cls(data)

def tuple_delegate(cls, data, func):
inner_type = cls.__args__[0]
return (func(inner_type, x) for x in data)

class DateTimeArgBuilderDelegate(ArgBuilderDelegate):

def __init__(self):
super().__init__(datetime.datetime)
def dict_delegate(cls, data, func):
key_type = cls.__args__[0]
value_type = cls.__args__[1]
return {
func(key_type, key): func(value_type, value) for key, value in data.items()
}

def resolve(self, data):
return parser.parse(data)

def builtin_delegate(cls, data, ignore_func):
if data is None:
return None
else:
return cls(data)

class UserDefinedArgBuilderDelegate(FunctionalArgBuilderDelegate):

def __init__(self, cls, func, ignore_unknown_fields: bool, walk_unknown_fields: bool):
super().__init__(cls, func)
self.ignore_unknown_fields = ignore_unknown_fields
self.walk_unknown_fields = walk_unknown_fields
def datetime_delegate(cls_ignore, data, ignore_func=None):
return parser.parse(data)

def resolve(self, data: dict):
return self._resolve(self.cls, data)

def _resolve(self, cls, data: dict):
args = {}
unsatisfied = get_init_params(cls)
for key, value in data.items():
if key in unsatisfied:
param_type = unsatisfied[key]
args[key] = self.func(param_type, value)
elif not self.ignore_unknown_fields:
raise UnknownFieldError(f'Found unknown field ({key}: {value}). '
'If you would like to skip unknown fields '
'create a Marshal object who can skip ignore_unknown_fields')
elif self.walk_unknown_fields:
if isinstance(value, dict):
args.update(self._resolve(cls, value))
elif isinstance(value, (list, set, tuple)):
for x in value:
if isinstance(x, dict):
args.update(self._resolve(cls, x))
return args
def user_defined_delegate(cls, data, func, ignore_unknown_fields: bool, walk_unknown_fields: bool):
args = {}
unsatisfied = get_init_params(cls)
for key, value in data.items():
if key in unsatisfied:
param_type = unsatisfied[key]
args[key] = func(param_type, value)
elif not ignore_unknown_fields:
raise UnknownFieldError(f'Found unknown field ({key}: {value}). '
'If you would like to skip unknown fields '
'create a Marshal object who can skip ignore_unknown_fields')
elif walk_unknown_fields:
if isinstance(value, dict):
args.update(user_defined_delegate(cls, value, func, ignore_unknown_fields, walk_unknown_fields))
elif isinstance(value, (list, set, tuple)):
for x in value:
if isinstance(x, dict):
args.update(user_defined_delegate(cls, x, func, ignore_unknown_fields, walk_unknown_fields))
return args
66 changes: 33 additions & 33 deletions pymarshaler/marshal.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@

import orjson

from pymarshaler.arg_delegates import ArgBuilderDelegate, ListArgBuilderDelegate, \
SetArgBuilderDelegate, TupleArgBuilderDelegate, DictArgBuilderDelegate, BuiltinArgBuilderDelegate, \
UserDefinedArgBuilderDelegate, DateTimeArgBuilderDelegate, EnumArgBuilderDelegate
from pymarshaler.arg_delegates import enum_delegate, \
user_defined_delegate, datetime_delegate, builtin_delegate, list_delegate, tuple_delegate, dict_delegate
from pymarshaler.errors import MissingFieldsError, InvalidDelegateError, PymarshalError
from pymarshaler.utils import is_builtin, is_user_defined

Expand All @@ -17,7 +16,7 @@ class _RegisteredDelegates:
def __init__(self):
self.registered_delegates = {}

def register(self, cls, delegate: ArgBuilderDelegate):
def register(self, cls, delegate):
self.registered_delegates[cls] = delegate

def get_for(self, cls):
Expand All @@ -30,46 +29,48 @@ def get_for(self, cls):
return None


class _ArgBuilderFactory:
class _Resolver:

def __init__(self, func, ignore_unknown_fields: bool, walk_unknown_fields: bool):
def __init__(self, func, ignore_unknown_fields: bool, walk_unknown_fields: bool, set_delegate=None):
self._func = func
self.ignore_unknown_fields = ignore_unknown_fields
self.walk_unknown_fields = walk_unknown_fields
self._registered_delegates = _RegisteredDelegates()
self._default_arg_builder_delegates = {
typing.List._name: lambda x: ListArgBuilderDelegate(x, func),
typing.Set._name: lambda x: SetArgBuilderDelegate(x, func),
typing.Tuple._name: lambda x: TupleArgBuilderDelegate(x, func),
typing.Dict._name: lambda x: DictArgBuilderDelegate(x, func),
"PythonBuiltin": lambda x: BuiltinArgBuilderDelegate(x),
"UserDefined": lambda x: UserDefinedArgBuilderDelegate(
x,
func,
ignore_unknown_fields,
walk_unknown_fields
),
"DateTime": lambda: DateTimeArgBuilderDelegate()
typing.List._name: list_delegate,
typing.Set._name: set_delegate,
typing.Tuple._name: tuple_delegate,
typing.Dict._name: dict_delegate,
"PythonBuiltin": builtin_delegate,
"UserDefined": user_defined_delegate,
"DateTime": datetime_delegate
}

def register(self, cls, delegate_cls):
self._registered_delegates.register(cls, delegate_cls(cls))
def register(self, cls, func):
self._registered_delegates.register(cls, func)

def get_delegate(self, cls) -> ArgBuilderDelegate:
def resolve(self, cls, data) -> typing.Any:
is_class = inspect.isclass(cls)

if not is_class:
if '_name' in cls.__dict__:
return self._safe_get(cls._name)(cls)
return self._safe_get(cls._name)(cls, data, self._func)
else:
cls_maybe = self._registered_delegates.get_for(cls)
if cls_maybe:
return cls_maybe
delegate_maybe = self._registered_delegates.get_for(cls)
if delegate_maybe:
return delegate_maybe(data)
elif issubclass(cls, Enum):
return EnumArgBuilderDelegate(cls)
return enum_delegate(cls, data, None)
elif is_user_defined(cls):
return self._default_arg_builder_delegates['UserDefined'](cls)
return user_defined_delegate(cls,
data,
self._func,
self.ignore_unknown_fields,
self.walk_unknown_fields)
elif issubclass(cls, datetime.datetime):
return self._default_arg_builder_delegates['DateTime']()
return datetime_delegate(cls, data, None)
elif is_builtin(cls):
return self._default_arg_builder_delegates['PythonBuiltin'](cls)
return builtin_delegate(cls, data, None)

raise InvalidDelegateError(f'No delegate for class {cls}')

Expand All @@ -92,7 +93,7 @@ def __init__(self, ignore_unknown_fields: bool = False, walk_unknown_fields: boo
if walk_unknown_fields and ignore_unknown_fields is False:
raise PymarshalError('If walk_unknown_fields is True, ignore_unknown_fields must also be True')

self._arg_builder_factory = _ArgBuilderFactory(
self._arg_builder_factory = _Resolver(
self._apply_typing,
ignore_unknown_fields,
walk_unknown_fields
Expand Down Expand Up @@ -168,7 +169,7 @@ def register_delegate(self, cls, delegate_cls):

def _unmarshal(self, cls, data: dict):
init_params = inspect.signature(cls.__init__).parameters
args = self._arg_builder_factory.get_delegate(cls).resolve(data)
args = self._arg_builder_factory.resolve(cls, data)
if is_user_defined(type(args)):
result = args
else:
Expand All @@ -183,8 +184,7 @@ def _unmarshal(self, cls, data: dict):
return result

def _apply_typing(self, param_type, value: typing.Any) -> typing.Any:
delegate = self._arg_builder_factory.get_delegate(param_type)
result = delegate.resolve(value)
result = self._arg_builder_factory.resolve(param_type, value)
if is_user_defined(param_type):
return param_type(**result)
return result
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

setuptools.setup(
name="pymarshaler",
version='0.3.3',
version='0.4.0',
author="Hernan Romer",
author_email="[email protected]",
description="Package to marshal and unmarshal python objects",
Expand Down
12 changes: 0 additions & 12 deletions tests/test_classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@
from enum import Enum
from typing import List, Dict

from pymarshaler.arg_delegates import ArgBuilderDelegate


@dataclass
class Inner:
Expand Down Expand Up @@ -77,16 +75,6 @@ class ClassWithCustomDelegate:
pass


@dataclass
class CustomNoneDelegate(ArgBuilderDelegate):

def __init__(self, cls):
super().__init__(cls)

def resolve(self, data):
return ClassWithCustomDelegate()


@dataclass
class NestedList:

Expand Down
2 changes: 1 addition & 1 deletion tests/test_marshaling.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ def test_validate(self):

@timed
def test_custom_delegate(self):
marshal.register_delegate(ClassWithCustomDelegate, CustomNoneDelegate)
marshal.register_delegate(ClassWithCustomDelegate, lambda x: ClassWithCustomDelegate())
result = marshal.unmarshal(ClassWithCustomDelegate, {})
self.assertEqual(result, ClassWithCustomDelegate())

Expand Down
Loading

0 comments on commit 5587003

Please sign in to comment.