While working with dataclasses, type hints are good but what I’m looking also for is a Validation of passed values (such as string of max length 50, int with upper limit of 100 etc)
Is there anyway to validate passed value ? For example, Pydantic has these Validators. I’m looking for something native without adding external libraries. My only solution is:
from dataclasses import dataclass def validate_str(max_length): def _validate(f): def wrapper(self, value): if type(value) is not str: raise TypeError(f"Expected str, got: {type(value)}") elif len(value) > max_length: raise ValueError( f"Expected string of max length {max_length}, got string of length {len(value)} : {value}" # noqa ) else: return f(self, value) return wrapper return _validate @dataclass class Example: """Class for keeping track of an item in inventory.""" @property def name(self): return self._name @name.setter @validate_str(max_length=50) def name(self, value): self._name = value
where validate_str
is just a custom decorator method to check length of provided value, but then I repeat myself.
I would like to pass validator somehow in same row of dataclass attribute as:
@dataclass class InventoryItem: """Class for keeping track of an item in inventory.""" name: str = validate_somehow() unit_price: float = validate_somehow() quantity_on_hand: int = 0
Advertisement
Answer
The ideal approach would be to use a modified version of the Validator
example from the Python how-to guide on descriptors.
For example:
from abc import ABC, abstractmethod from dataclasses import dataclass, MISSING class Validator(ABC): def __set_name__(self, owner, name): self.private_name = '_' + name def __get__(self, obj, obj_type=None): return getattr(obj, self.private_name) def __set__(self, obj, value): self.validate(value) setattr(obj, self.private_name, value) @abstractmethod def validate(self, value): """Note: subclasses must implement this method""" class String(Validator): # You may or may not want a default value def __init__(self, default: str = MISSING, minsize=None, maxsize=None, predicate=None): self.default = default self.minsize = minsize self.maxsize = maxsize self.predicate = predicate # override __get__() to return a default value if one is not passed in to __init__() def __get__(self, obj, obj_type=None): return getattr(obj, self.private_name, self.default) def validate(self, value): if not isinstance(value, str): raise TypeError(f'Expected {value!r} to be an str') if self.minsize is not None and len(value) < self.minsize: raise ValueError( f'Expected {value!r} to be no smaller than {self.minsize!r}' ) if self.maxsize is not None and len(value) > self.maxsize: raise ValueError( f'Expected {value!r} to be no bigger than {self.maxsize!r}' ) if self.predicate is not None and not self.predicate(value): raise ValueError( f'Expected {self.predicate} to be true for {value!r}' ) @dataclass class A: y: str = String(default='DEFAULT', minsize=5, maxsize=10, predicate=str.isupper) # Descriptor instance x: int = 5 a = A() print(a) a = A('TESTING!!') print(a) try: a.y = 'testing!!' except Exception as e: print('Error:', e) try: a = A('HEY') except Exception as e: print('Error:', e) try: a = A('HELLO WORLD!') except Exception as e: print('Error:', e) try: a.y = 7 except Exception as e: print('Error:', e)
Output:
A(y='DEFAULT', x=5) A(y='TESTING!!', x=5) Error: Expected <method 'isupper' of 'str' objects> to be true for 'testing!!' Error: Expected 'HEY' to be no smaller than 5 Error: Expected 'HELLO WORLD!' to be no bigger than 10 Error: Expected 7 to be an str