From bbb33a85cbc182002941d1673dd9fe61080d7213 Mon Sep 17 00:00:00 2001 From: hgromer Date: Fri, 18 Feb 2022 11:56:34 -0500 Subject: [PATCH] delegates are now functions to avoid creating a ton of classes --- README.md | 17 ++-- pymarshaler/__init__.py | 6 +- pymarshaler/arg_delegates.py | 156 +++++++++++------------------------ pymarshaler/marshal.py | 66 +++++++-------- setup.py | 2 +- tests/test_classes.py | 12 --- tests/test_marshaling.py | 2 +- tests/timed.py | 11 ++- 8 files changed, 97 insertions(+), 175 deletions(-) diff --git a/README.md b/README.md index 0175e4b..e49a020 100644 --- a/README.md +++ b/README.md @@ -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: @@ -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!' diff --git a/pymarshaler/__init__.py b/pymarshaler/__init__.py index e57f885..6da53aa 100644 --- a/pymarshaler/__init__.py +++ b/pymarshaler/__init__.py @@ -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 diff --git a/pymarshaler/arg_delegates.py b/pymarshaler/arg_delegates.py index 910ae66..47770d6 100644 --- a/pymarshaler/arg_delegates.py +++ b/pymarshaler/arg_delegates.py @@ -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 diff --git a/pymarshaler/marshal.py b/pymarshaler/marshal.py index e9bf506..4d1d037 100644 --- a/pymarshaler/marshal.py +++ b/pymarshaler/marshal.py @@ -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 @@ -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): @@ -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}') @@ -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 @@ -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: @@ -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 diff --git a/setup.py b/setup.py index cb6afe5..d98e108 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ setuptools.setup( name="pymarshaler", - version='0.3.3', + version='0.4.0', author="Hernan Romer", author_email="nanug33@gmail.com", description="Package to marshal and unmarshal python objects", diff --git a/tests/test_classes.py b/tests/test_classes.py index 55c7cdd..feecf22 100644 --- a/tests/test_classes.py +++ b/tests/test_classes.py @@ -5,8 +5,6 @@ from enum import Enum from typing import List, Dict -from pymarshaler.arg_delegates import ArgBuilderDelegate - @dataclass class Inner: @@ -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: diff --git a/tests/test_marshaling.py b/tests/test_marshaling.py index 6ca694e..8158584 100644 --- a/tests/test_marshaling.py +++ b/tests/test_marshaling.py @@ -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()) diff --git a/tests/timed.py b/tests/timed.py index 537a0c2..d5d59fc 100644 --- a/tests/timed.py +++ b/tests/timed.py @@ -11,8 +11,13 @@ def timed(function): """ @wraps(function) def wrapper(*args, **kwargs): - start = timer() - result = function(*args, **kwargs) - print("{} took {} ms".format(function.__name__, round(1000 * (timer() - start), 3))) + total_time = 0 + result = None + + for _ in range(5): + start = timer() + result = function(*args, **kwargs) + total_time += timer() - start + print("{} took {} ms".format(function.__name__, round(1000 * (total_time / 5), 3))) return result return wrapper