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:
JavaScript
x
31
31
1
from dataclasses import dataclass
2
3
def validate_str(max_length):
4
def _validate(f):
5
def wrapper(self, value):
6
if type(value) is not str:
7
raise TypeError(f"Expected str, got: {type(value)}")
8
elif len(value) > max_length:
9
raise ValueError(
10
f"Expected string of max length {max_length}, got string of length {len(value)} : {value}" # noqa
11
)
12
else:
13
return f(self, value)
14
15
return wrapper
16
17
return _validate
18
19
@dataclass
20
class Example:
21
"""Class for keeping track of an item in inventory."""
22
23
@property
24
def name(self):
25
return self._name
26
27
@name.setter
28
@validate_str(max_length=50)
29
def name(self, value):
30
self._name = value
31
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:
JavaScript
1
7
1
@dataclass
2
class InventoryItem:
3
"""Class for keeping track of an item in inventory."""
4
name: str = validate_somehow()
5
unit_price: float = validate_somehow()
6
quantity_on_hand: int = 0
7
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:
JavaScript
1
87
87
1
from abc import ABC, abstractmethod
2
from dataclasses import dataclass, MISSING
3
4
5
class Validator(ABC):
6
7
def __set_name__(self, owner, name):
8
self.private_name = '_' + name
9
10
def __get__(self, obj, obj_type=None):
11
return getattr(obj, self.private_name)
12
13
def __set__(self, obj, value):
14
self.validate(value)
15
setattr(obj, self.private_name, value)
16
17
@abstractmethod
18
def validate(self, value):
19
"""Note: subclasses must implement this method"""
20
21
22
class String(Validator):
23
24
# You may or may not want a default value
25
def __init__(self, default: str = MISSING, minsize=None, maxsize=None, predicate=None):
26
self.default = default
27
self.minsize = minsize
28
self.maxsize = maxsize
29
self.predicate = predicate
30
31
# override __get__() to return a default value if one is not passed in to __init__()
32
def __get__(self, obj, obj_type=None):
33
return getattr(obj, self.private_name, self.default)
34
35
def validate(self, value):
36
37
if not isinstance(value, str):
38
raise TypeError(f'Expected {value!r} to be an str')
39
40
if self.minsize is not None and len(value) < self.minsize:
41
raise ValueError(
42
f'Expected {value!r} to be no smaller than {self.minsize!r}'
43
)
44
45
if self.maxsize is not None and len(value) > self.maxsize:
46
raise ValueError(
47
f'Expected {value!r} to be no bigger than {self.maxsize!r}'
48
)
49
50
if self.predicate is not None and not self.predicate(value):
51
raise ValueError(
52
f'Expected {self.predicate} to be true for {value!r}'
53
)
54
55
56
@dataclass
57
class A:
58
y: str = String(default='DEFAULT', minsize=5, maxsize=10, predicate=str.isupper) # Descriptor instance
59
x: int = 5
60
61
62
a = A()
63
print(a)
64
65
a = A('TESTING!!')
66
print(a)
67
68
try:
69
a.y = 'testing!!'
70
except Exception as e:
71
print('Error:', e)
72
73
try:
74
a = A('HEY')
75
except Exception as e:
76
print('Error:', e)
77
78
try:
79
a = A('HELLO WORLD!')
80
except Exception as e:
81
print('Error:', e)
82
83
try:
84
a.y = 7
85
except Exception as e:
86
print('Error:', e)
87
Output:
JavaScript
1
7
1
A(y='DEFAULT', x=5)
2
A(y='TESTING!!', x=5)
3
Error: Expected <method 'isupper' of 'str' objects> to be true for 'testing!!'
4
Error: Expected 'HEY' to be no smaller than 5
5
Error: Expected 'HELLO WORLD!' to be no bigger than 10
6
Error: Expected 7 to be an str
7