Reputation: 612
The attrs package for python provides a simple way to validate passed variables upon instantiation (example taken from attrs page):
>>> @attr.s
... class C(object):
... x = attr.ib(validator=attr.validators.instance_of(int))
>>> C(42)
C(x=42)
>>> C("42")
Traceback (most recent call last):
...
TypeError: ("'x' must be <type 'int'> (got '42' that is a <type 'str'>).", Attribute(name='x', default=NOTHING, factory=NOTHING, validator=<instance_of validator for type <type 'int'>>, type=None), <type 'int'>, '42')
This works well, as the thrown exception proves. However, when I change the value of x after instantiation, no exception is thrown:
c = C(30)
c.x = '30'
For static objects this behavior can be OK, but it seems heavily dangerous to me to assume that an object is static. Is there a workaround to get validators with attrs that also work after instantiation?
Upvotes: 2
Views: 5200
Reputation: 151
In version 20.1.0 they have added on_setattr
:
A callable that is run whenever the user attempts to set an attribute (either by assignment like i.x = 42 or by using setattr like setattr(i, "x", 42)). It receives the same arguments as validators: the instance, the attribute that is being modified, and the new value.
So, adding:
import attr
@attr.s
class C(object):
x = attr.ib(
validator=attr.validators.instance_of(int),
on_setattr = attr.setters.validate, # new in 20.1.0
)
yields
C(42)
# C(x=42)
C("42")
# TypeError: ("'x' must be <class 'int'>
Also, specifically for string input like in your example, you may find attrs
converters handy. For example, to automatically convert:
@attr.s
class CWithConvert(object):
x = attr.ib(
converter=int,
validator=attr.validators.instance_of(int),
on_setattr = attr.setters.validate,
)
CWithConvert(42)
# CWithConvert(x=42)
CWithConvert("42")
# CWithConvert(x=42) # converted!
CWithConvert([42])
# TypeError: int() argument must be a string, a bytes-like object or a number, not 'list'
Careful though with:
CWithConvert(0.8)
# CWithConvert(x=0) # float to int!
Upvotes: 3
Reputation: 12156
One way which keeps mutability is like so:
@attr.s
class C(object):
_x = attr.ib(validator=attr.validators.instance_of(int))
@property
def x(self):
return self._x
@x.setter
def x(self, value):
assert isinstance(value, int), repr(value) # or whatever error you want
self._x = value
But even this is not safe against c._x = '30'
.
The problem isn't with attrs
, it's with python. a.b = c
is always going to work when a.b
is just a variable. This is due to pythons concept of "we're all consenting adults here" -- i.e. everything is public, and everything is modifiable. If you edit something you shouldn't, it's your fault.
That being said, attrs
does provide a hack to prevent attribute assignment to give an illusion of immutability:
@attr.s(frozen=True)
class C(object):
x = attr.ib(validator=attr.validators.instance_of(int))
c = C(1)
c.x = 30 # raises FrozenInstanceError
Upvotes: 2
Reputation: 4668
As per discussion in thread for attrs
So the original implementation of the validation code actually also ran the validators on assignment.
I’ve removed it before merging because in dynamic languages like Python there’s just too many ways to circumvent it and I personally prefer to not mutate my objects anyways. Hence attrs rich support for freezing classes and creating new instances with changed attributes (assoc).
Of course you can add such a feature yourself by implementing a setattr method that calls your validators whenever you try to set an attribute.
Upvotes: 1