Skip to content

Commit

Permalink
Merge pull request #3 from hgromer/dev
Browse files Browse the repository at this point in the history
refactor how Marshal works
  • Loading branch information
hgromer authored Jan 23, 2021
2 parents a19db8f + feec553 commit b2213cc
Show file tree
Hide file tree
Showing 7 changed files with 235 additions and 223 deletions.
28 changes: 17 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,15 @@ class Test:
That's it! We can now marshal, and more importantly, unmarshal this object to and from JSON.

```python
from pymarshaler import marshal
from pymarshaler.marshal import Marshal
import json

test_instance = Test('foo')
blob = marshal.marshal(test_instance)
blob = Marshal.marshal(test_instance)
print(blob)
>>> '{name: foo}'

marshal = Marshal()
result = marshal.unmarshal(Test, json.loads(blob))
print(result.name)
>>> 'foo'
Expand Down Expand Up @@ -57,14 +58,15 @@ As you can see, adding a nested class is as simple as as adding a basic structur
Pymarshaler will fail when encountering an unknown field by default, however you can configure it to ignore unknown fields

```python
from pymarshaler import marshal
from pymarshaler.marshal import Marshal
from pymarshaler.arg_delegates import ArgBuilderFactory

marshal = Marshal()
blob = {'test': 'foo', 'unused_field': 'blah'}
result = marshal.unmarshal(Test, blob)
>>> 'Found unknown field (unused_field: blah). If you would like to skip unknown fields set ArgBuilderFactory.ignore_unknown_fields(True)'
>>> 'Found unknown field (unused_field: blah). If you would like to skip unknown fields create a Marshal object who can skip ignore_unknown_fields'

ArgBuilderFactory.ignore_unknown_fields(True)
marhsal = Marshal(ignore_unknown_fields=True)
result = marshal.unmarshal(Test, blob)
print(result.name)
>>> 'foo'
Expand All @@ -75,7 +77,7 @@ print(result.name)
We can use pymarshaler to handle containers as well. Again we take advantage of python's robust typing system

```python
from pymarshaler import marshal
from pymarshaler.marshal import Marshal
from typing import Set
import json

Expand All @@ -84,6 +86,7 @@ class TestContainer:
def __int__(self, container: Set[str]):
self.container = container

marshal = Marshal()
container_instance = TestContainer({'foo', 'bar'})
blob = marshal.marshal(container_instance)
print(blob)
Expand All @@ -99,13 +102,14 @@ Pymarshaler can also handle containers that store user defined types. The `Set[s
Pymarshaler also supports default values, and will use any default values supplied in the `__init__` if those values aren't present in the JSON data.

```python
from pymarshaler import marshal
from pymarshaler.marshal import Marshal

class TestWithDefault:

def __init__(self, name: str = 'foo'):
self.name = name

marshal = Marshal()
result = marshal.unmarshal(TestWithDefault, {})
print(result.name)
>>> 'foo'
Expand All @@ -115,7 +119,7 @@ Pymarshaler will raise an error if any non-default attributes aren't given
Pymarshaler also supports a validate method on creation of the python object. This method will be called before being returned to the user.

```python
from pymarshaler import marshal
from pymarshaler.marshal import Marshal

class TestWithValidate:

Expand All @@ -126,6 +130,7 @@ class TestWithValidate:
print(f'My name is {self.name}!')


marshal = Marshal()
result = marshal.unmarshal(TestWithValidate, {'name': 'foo'})
>>> 'My name is foo!'
```
Expand All @@ -135,8 +140,8 @@ This can be used to validate the python object right at construction, potentiall
It's also possible to register your own custom unmarshaler for specific user defined classes.

```python
from pymarshaler.arg_delegates import ArgBuilderDelegate, ArgBuilderFactory
from pymarshaler import marshal
from pymarshaler.arg_delegates import ArgBuilderDelegate
from pymarshaler.marshal import Marshal


class ClassWithMessage:
Expand All @@ -160,7 +165,8 @@ class CustomDelegate(ArgBuilderDelegate):
return {'message_obj': ClassWithMessage(data['message'])}


ArgBuilderFactory.register_delegate(ClassWithCustomDelegate, CustomDelegate)
marshal = Marshal()
marshal.register_delegate(ClassWithCustomDelegate, CustomDelegate)
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.1.1'
__all__ = ['marshal', 'utils', 'arg_delegates', 'errors']
__version__ = '0.2.0'
__all__ = ['Marshal', 'utils', 'arg_delegates', 'errors']

from pymarshaler import marshal
from pymarshaler.marshal import Marshal
from pymarshaler import utils
from pymarshaler import arg_delegates
from pymarshaler import errors
140 changes: 32 additions & 108 deletions pymarshaler/arg_delegates.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,7 @@

import dateutil.parser as parser

from pymarshaler.errors import UnknownFieldError, InvalidDelegateError
from pymarshaler.utils import is_user_defined, is_builtin


def _apply_typing(param_type, value: typing.Any) -> typing.Any:
delegate = ArgBuilderFactory.get_delegate(param_type)
result = delegate.resolve(value)
if is_user_defined(param_type):
return param_type(**result)
return result
from pymarshaler.errors import UnknownFieldError


class ArgBuilderDelegate:
Expand All @@ -25,46 +16,56 @@ def resolve(self, data):
raise NotImplementedError(f'{ArgBuilderDelegate.__name__} has no implementation of resolve')


class ListArgBuilderDelegate(ArgBuilderDelegate):
class FunctionalArgBuilderDelegate(ArgBuilderDelegate):

def __init__(self, cls):
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 ListArgBuilderDelegate(FunctionalArgBuilderDelegate):

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

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


class SetArgBuilderDelegate(ArgBuilderDelegate):
class SetArgBuilderDelegate(FunctionalArgBuilderDelegate):

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

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


class TupleArgBuilderDelegate(ArgBuilderDelegate):
class TupleArgBuilderDelegate(FunctionalArgBuilderDelegate):

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

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


class DictArgBuilderDelegate(ArgBuilderDelegate):
class DictArgBuilderDelegate(FunctionalArgBuilderDelegate):

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

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


Expand All @@ -89,10 +90,10 @@ def resolve(self, data):
return parser.parse(data)


class UserDefinedArgBuilderDelegate(ArgBuilderDelegate):
class UserDefinedArgBuilderDelegate(FunctionalArgBuilderDelegate):

def __init__(self, cls, ignore_unknown_fields: bool, walk_unknown_fields: bool):
super().__init__(cls)
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

Expand All @@ -105,11 +106,11 @@ def _resolve(self, cls, data: dict):
for key, value in data.items():
if key in unsatisfied:
param_type = unsatisfied[key].annotation
args[key] = _apply_typing(param_type, value)
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 set '
'ArgBuilderFactory.ignore_unknown_fields(True)')
'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))
Expand All @@ -118,80 +119,3 @@ def _resolve(self, cls, data: dict):
if isinstance(x, dict):
args.update(self._resolve(cls, x))
return args


class _RegisteredDelegates:

def __init__(self):
self.registered_delegates = {}

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

def get(self, cls):
return self.registered_delegates[cls.__name__]

def contains(self, cls):
try:
return cls.__name__ in self.registered_delegates
except AttributeError:
return False


class ArgBuilderFactory:
_walk_unknown_fields = False

_ignore_unknown_fields = False

_registered_delegates = _RegisteredDelegates()

_default_arg_builder_delegates = {
typing.List._name: lambda x: ListArgBuilderDelegate(x),
typing.Set._name: lambda x: SetArgBuilderDelegate(x),
typing.Tuple._name: lambda x: TupleArgBuilderDelegate(x),
typing.Dict._name: lambda x: DictArgBuilderDelegate(x),
"PythonBuiltin": lambda x: BuiltinArgBuilderDelegate(x),
"UserDefined": lambda x: UserDefinedArgBuilderDelegate(
x,
ArgBuilderFactory._ignore_unknown_fields,
ArgBuilderFactory._walk_unknown_fields
),
"DateTime": lambda: DateTimeArgBuilderDelegate(),
}

@staticmethod
def walk_unknown_fields(walk: bool):
ArgBuilderFactory._walk_unknown_fields = walk
if walk:
ArgBuilderFactory._ignore_unknown_fields = walk

@staticmethod
def ignore_unknown_fields(ignore: bool):
ArgBuilderFactory._ignore_unknown_fields = ignore
if not ignore:
ArgBuilderFactory._walk_unknown_fields = ignore

@staticmethod
def register_delegate(cls, delegate_cls):
ArgBuilderFactory._registered_delegates.register(cls, delegate_cls(cls))

@staticmethod
def get_delegate(cls) -> ArgBuilderDelegate:
if ArgBuilderFactory._registered_delegates.contains(cls):
return ArgBuilderFactory._registered_delegates.get(cls)
elif is_user_defined(cls):
return ArgBuilderFactory._default_arg_builder_delegates['UserDefined'](cls)
elif '_name' in cls.__dict__:
return ArgBuilderFactory._safe_get(cls._name)(cls)
elif issubclass(cls, datetime.datetime):
return ArgBuilderFactory._default_arg_builder_delegates['DateTime']()
elif is_builtin(cls):
return ArgBuilderFactory._default_arg_builder_delegates['PythonBuiltin'](cls)
else:
raise InvalidDelegateError(f'No delegate for class {cls}')

@staticmethod
def _safe_get(name):
if name not in ArgBuilderFactory._default_arg_builder_delegates:
raise InvalidDelegateError(f'Unsupported class type {name}')
return ArgBuilderFactory._default_arg_builder_delegates[name]
10 changes: 5 additions & 5 deletions pymarshaler/errors.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
class PymarshallError(RuntimeError):
class PymarshalError(RuntimeError):
pass


class UnknownFieldError(PymarshallError):
class UnknownFieldError(PymarshalError):
pass


class UnsupportedClassError(PymarshallError):
class UnsupportedClassError(PymarshalError):
pass


class InvalidDelegateError(PymarshallError):
class InvalidDelegateError(PymarshalError):
pass


class MissingFieldsError(PymarshallError):
class MissingFieldsError(PymarshalError):
pass
Loading

0 comments on commit b2213cc

Please sign in to comment.