from typing import TypeVar, Type, Any, Annotated, Callable, Dict, get_args, get_origin
T = TypeVar('T')
ValidationRule = Callable[[Any], bool]
ValidationRules = Dict[str, ValidationRule]
class ValidatedProperty:
"""Экземпляры этого класса будут свойствами в валидируемых классах, и будут заниматься валидацией."""
def __init__(self, name: str, storagename: str, rules: ValidationRules):
self.name = name # как называется свойство
self.storagename = storagename # где его хранить
self.rules = rules # какие правила применять
def __get__(self, instance, owner=None):
return getattr(instance, self.storagename) # при чтении просто возвращаем свойство
def __set__(self, instance, value): # при записи валидируем
for message, rule in self.rules.items():
if not rule(value): # если правило нарушено, выкидываем исключение с сообщением
raise ValueError(f'{instance.__class__.__name__}.{self.name}: {message}')
setattr(instance, self.storagename, value)
def validated(klass: Type[T]) -> Type[T]:
"""Декоратор для валидируемых классов."""
for name, annot in klass.__annotations__.items(): # проверяем список аннотаций в классе
base = get_origin(annot) or annot
if base is not Annotated: # нас интересуют только те, которые помечены как Annotated
continue
args = get_args(annot)
rules = [arg for arg in args if isinstance(arg, dict)]
if not rules: # и только если один (любой) из аргументов Annotated - словарь
continue
# в этом случае мы считаем, что словарь содержит правила валидации, и создаём свойство класса
setattr(klass, name, ValidatedProperty(name, f'_{name}', rules[0]))
return klass # не забываем вернуть изменённый класс!
@validated
class Person:
name: Annotated[str, {'must not be empty': lambda v: bool(v)}]
age: Annotated[int, {'must be positive': lambda v: v > 0}]
def __init__(self, name: str, age: int):
self.name = name # валидация отработает уже здесь
self.age = age # валидация отработает уже здесь
def __repr__(self) -> str:
return f'Person(name={self.name!r}, age={self.age!r})'
try:
Person('John Doe', 23) # отработает успешно
except Exception as err:
print('Failed to create person 1')
print(f'{err.__class__.__name__}: {err!s}')
else:
print('Person 1 created')
try:
Person('', 23) # выкинет исключение
except Exception as err:
print('Failed to create person 2')
print(f'{err.__class__.__name__}: {err!s}')
else:
print('Person 2 created')
try:
Person('Jane Doe', -23) # выкинет исключение
except Exception as err:
print('Failed to create person 3')
print(f'{err.__class__.__name__}: {err!s}')
else:
print('Person 3 created')
p = Person('John Doe', 23)
try:
p.name = '' # выкинет исключение
except Exception as err:
print('Failed to modify person')
print(f'{err.__class__.__name__}: {err!s}')
try:
p.age = 0 # выкинет исключение
except Exception as err:
print('Failed to modify person')
print(f'{err.__class__.__name__}: {err!s}')
try:
p.age = 24 # отработает успешно
except Exception as err:
print('Failed to modify person')
print(f'{err.__class__.__name__}: {err!s}')
print(p)
print(vars(p))