import typing
from copy import copy
from pyjackson import utils
from pyjackson.core import (FIELD_MAPPING_NAME_FIELD, TYPE_AS_LIST, TYPE_FIELD_NAME_FIELD_NAME,
TYPE_FIELD_NAME_FIELD_POSITION, TYPE_FIELD_NAME_FIELD_ROOT, Position)
from pyjackson.generics import _register_serializer
from pyjackson.utils import get_class_field_names
[docs]class cached_property:
def __init__(self, method):
self.method = method
self.field_name = '__{}_value'.format(method.__name__)
def __get__(self, instance, owner):
if instance is None:
return self
if not hasattr(instance, self.field_name):
value = self.method(instance)
setattr(instance, self.field_name, value)
else:
value = getattr(instance, self.field_name)
return value
[docs]def make_string(*fields: str, include_name=True):
"""
Decorator to create a `__str__` method for class based on `__init__` arguments
Usage: directly :func:`@make_string` on class declaration to include all fields
or :func:`@make_string(*fields, include_name)` to alter params
:param fields: list of strings with field names
:param include_name: whether to include class name
"""
if len(fields) == 1 and not isinstance(fields[0], str):
cls = fields[0]
fields = []
else:
cls = None
def make_str(cls):
def __str__(self):
flds = fields or [f.name for f in utils.get_function_fields(cls.__init__, False)]
args = ','.join('{}={}'.format(key, getattr(self, key)) for key in flds)
args_str = str(args)
if include_name:
args_str = '({})'.format(args_str)
return cls.__name__ + args_str if include_name else args_str
cls.__str__ = __str__
cls.__repr__ = __str__
return cls
if cls is not None:
return make_str(cls)
return make_str
[docs]def as_list(cls: typing.Type):
"""
Mark class to serialize it to list instead of dict
:param cls: class to mark
"""
setattr(cls, TYPE_AS_LIST, True)
return cls
[docs]def type_field(field_name, position: Position = Position.INSIDE, allow_reregistration=False):
"""Class decorator for polymorphic hierarchies to define class field name, where subclass's type alias will be stored
Use it on hierarchy root class, add class field with defined name to any subclasses
The same field name will be used during deserialization
:param field_name: class field name to put alias for type
:param position: where to put type alias
:param allow_reregistration: whether to allow reregistration of same alias or throw error
"""
class SubtypeRegisterMixin:
_subtypes = dict()
locals()[TYPE_FIELD_NAME_FIELD_NAME] = field_name
locals()[TYPE_FIELD_NAME_FIELD_POSITION] = position
def __init_subclass__(cls, **kwargs):
super(SubtypeRegisterMixin, cls).__init_subclass__(**kwargs)
subtype_name = cls.__dict__.get(field_name, f'{cls.__module__}.{cls.__name__}')
setattr(cls, field_name, subtype_name) # set default type name
if subtype_name is None:
return
existing = SubtypeRegisterMixin._subtypes.get(subtype_name, None)
if existing is not None:
msg = 'Cant register {} as {}. Subtype {} is already registered'.format(cls, subtype_name, existing)
from pyjackson.generics import Serializer
if issubclass(cls, Serializer):
# allow reregistration if cls is generic type and it's base was registered during declaration
if cls._is_dynamic:
# do not register initialized generics
return
if issubclass(existing, Serializer) and issubclass(cls, existing):
# raise if cls is child of existing and does not declare type alias
raise ValueError(msg)
elif existing != cls and not allow_reregistration: # it's not serializer and different class
raise ValueError(msg)
SubtypeRegisterMixin._subtypes[subtype_name] = cls
def class_wrap(root_cls):
# to register itself with correct module name
subtype_name = root_cls.__dict__.get(field_name, f'{root_cls.__module__}.{root_cls.__name__}')
wrapped = type(root_cls.__name__, (root_cls, SubtypeRegisterMixin), {field_name: subtype_name})
wrapped.__module__ = root_cls.__module__
wrapped.__doc__ = root_cls.__doc__
wrapped.__qualname__ = root_cls.__qualname__
setattr(wrapped, TYPE_FIELD_NAME_FIELD_ROOT, wrapped)
return wrapped
return class_wrap
[docs]def real_types(*types):
"""Register multiple real types for one serializer"""
def dec(cls):
for t in types:
_register_serializer(cls, t)
return cls
return dec
[docs]def rename_fields(**field_mapping):
"""
Change name of fields in payload. This behavior is inheritable and overridable for child classes
:param field_mapping: str-str mapping of field name (from constructor) to field name in payload
"""
def decorator(cls):
if hasattr(cls, FIELD_MAPPING_NAME_FIELD):
mapping, new_mapping = copy(getattr(cls, FIELD_MAPPING_NAME_FIELD)), field_mapping
mapping.update(new_mapping)
else:
mapping = field_mapping
setattr(cls, FIELD_MAPPING_NAME_FIELD, mapping)
return cls
return decorator
[docs]def camel_case(cls):
"""
Change snake_case field names to camelCase names
"""
fields = get_class_field_names(cls)
rename = {}
for f in fields:
tokens = f.split('_')
rename[f] = tokens[0] + ''.join(t.capitalize() for t in tokens[1:])
return rename_fields(**rename)(cls)