One of the best things (for me) that come up with python 3.7 release is a dataclasses module. Basically dataclasses module "provides a decorator and functions for automatically adding generated special methods such as __init__() and __repr__() to user-defined classes". In other words this module just provides some syntax sugar and frees you from adding __init__() or __repr__() or other methods described in docs by hand.
As dataclass has fields typing and look almost common to marshmallow Schemas I just supposed they can be useful for serializing of responses in my API. After reading of PEP 557 I created the following mixin:
@dataclass class DataClassMixin: def __post_init__(self): errors = {} self_fields = fields(self) for fld in self_fields: value = getattr(self, fld.name) value_type = type(value) has_origin_attr = hasattr(fld.type, '__origin__') field_type_original = fld.type.__origin__ if has_origin_attr else fld.type invalid_type_message = f"Invalid data type. " \ f"Expected '{field_type_original.__name__}' got '{value_type.__name__}'" # cheat the 'TypeError: Subscripted generics cannot be used with class and instance checks' # https://bugs.python.org/issue29262 if ( not fld.metadata.get('allow_empty') and ( (has_origin_attr and value_type is not field_type_original) or (not has_origin_attr and not issubclass(value_type, field_type_original))) and (fld.name not in errors or invalid_type_message not in errors[fld.name]) ): errors[fld.name] = [invalid_type_message] if value: validation_fn = fld.metadata.get('validate') if validation_fn and callable(validation_fn): is_valid, message = validation_fn(value) if not is_valid: errors[fld.name] = [message] if errors: raise ValidationError(errors) def as_dict(self): return asdict(self)This mixin has only 2 methods where as_dict is just a wrapper around the native asdict function and __post_init__ is a magic method which come up from a dataclasses package and can be useful for data validation purposes. In the example above it validates the attribute types and throws a custom ValidationError (see it below). Note that it also checks the __origin__ type of data structures like List from the typing module.
Defining a ValidationError
class BaseError(Exception): code = 400 def __init__(self, message=None): super().__init__() self.message = message class ValidationError(BaseError): code = 422 def __init__(self, errors: dict, message=None): message = message or 'Validation of incoming data failed' super().__init__(message=message) self.errors = errorsUsage example
Let's define a User class and a custom validation function called validate_age:
def validate_age(age): return age > 0, 'Age must be greater than 0' @dataclass class User(DataClassMixin): id: int = field(default=None) email: str = field(default=None) age: int = field(default=None, metadata={'allow_empty': False, 'validate': validate_age}) name: str = field(default=None)And to ensure the validation works just run the following code:
In: try: attributes = dict(id=10, email='john.doe@testmail.com', name='John Doe', age=0) user = User(**attributes) except Exception as e: print(e.message, e.errors) Validation of incoming data failed {'age': ['Age must be greater than 0']} In: try: attributes = dict(id=10, email='john.doe@testmail.com', name='John Doe', age=10) user = User(**attributes) except Exception as e: print(e.message, e.errors) In: user.as_dict() Out: {'email': 'john.doe@testmail.com', 'id': 10, 'age': 10, 'name': 'John Doe'}
As you can see it's very lightweight and flexible solution made with python standard library. Hope it can be useful for someone else.
No comments:
Post a Comment