-
-
Notifications
You must be signed in to change notification settings - Fork 408
Description
Validation and conversion of attribute values is currently only done if the user passes the corresponding arguments to attrib().
In contrast, libs like Pydantic automatically validate and convert all values (in __init__() as well as __setattr__()). It also allows the user to easily pass the most common validators like ge=0 or maxlength=50.
This lets you do fancy stuff like this, which is very useful, e.g., for REST API server models and clients, or generally for loading nested JSON data. Settings/config management is another usecase:
>>> from dateatime import datetime
>>> from pydantic import BaseModel
>>>
>>> class Child(BaseModel):
... x: int
... y: int
... d: datetime
...
>>> class Parent(BaseModel):
... name: str
... child: Child
...
>>> data = {
... 'name': 'spam',
... 'child': {
... 'x': 23,
... 'y': '42', # sic!
... 'd': '2020-05-04T13:37:00',
... },
... }
>>> Parent(**data)
Parent(name='spam', child=Child(x=23, y=42, d=datetime.datetime(2020, 5, 4, 13, 37))) While pydantic works quite nicely, it is way slower than attrs and also uses inheritance which is not always what users may want.
I tried implementing a similar functionality but failed. IMHO, there are some changes needed directly within attr’s class-buildng facilities, as the following example illustrates:
from datetime import datetime
from functools import partial
import attr
# Two examples for pre-defined validators for later use
def check_ge(inst, attr, value, bound):
if value < bound:
raise ValueError(f'"{attr.name}" is not >= {bound}: {value}')
def check_le(inst, attr, value, bound):
if value > bound:
raise ValueError(f'"{attr.name}" is not <= {bound}: {value}')
def validating_class(auto_converters=True, **kwargs):
# Extension wrapper for "attrs()"
def wrap(cls):
# if auto_converters:
# Here, I'd have to manually iterate/evaluate all attribs.
# This duplicates work in "attr.s()".
cls = attr.s(auto_attribs=True, frozen=True, slots=True, **kwargs)(cls)
# if auto_converters:
# It is very hard (impossible?) to update "cls" here and add (optional)
# converters to all attributes
return cls
return wrap
def field(default, ge=None, le=None, **kwargs):
# Extension wrapper for "field()"
v = []
# This does not work, b/c I cannot access type information here
# if attr_type is int:
# if ge is not None:
# v.append(partial(check_ge, bound=ge))
# if le is not None:
# v.append(partial(check_le, bound=le))
if 'validator' in kwargs:
v.append(validator)
if v:
kwargs['validator'] = v
return attr.ib(converter=int, validator=v)
@validating_class()
class Child:
x: int = field(ge=0, le=100)
y: int = field(ge=0, le=100)
d: datetime # auto-add "field(converter=datertime.fromisoformat)"
@validating_class()
class Parent:
name: str
child: Child = # auto-add "field(converter=lambda d: Child(**d))"
data = {'name': 'spam', 'child': {'x': 23, 'y': '42', 'd': '2020-05-04T13:37:00'}}
p = Parent(**data)
print(p)
# Parent(name='spam', child=Child(x=-1, y=0, datetime(2020, 5, 4, 13, 37)))I already talked about this with @hynek and he asked me create this issue.
I also created an issue for orjson asking to add support for attrs.
Summary (what’s needed)
- Optional conversions (and validation) of all attributes. Conversion should only happen when necessary (e.g., do not
list(value)ifvalueis already a list). This is the most important part. - Shortcuts for commonly used validators:
attrib(ge=0, le=10). - An
asjson()function that uses stdlib and provides an encoder that can handle attrs classes and datetimes and similar common types. - Explicit orjson support might not be needed when Add attrs support ijl/orjson#92 gets implemented