HeinzKurt
HeinzKurt

Reputation: 612

Python attrs package: validators after instantiation

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

Answers (3)

Odysseus
Odysseus

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

FHTMitchell
FHTMitchell

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

Andrew_Lvov
Andrew_Lvov

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

Related Questions