Skip to content

What's the exact logic of on_setattr, validators and converters? #1148

@beelze

Description

@beelze

At first, thank for the great and inspiring package!

But let me explain my question:

def before(instance, attrib, new_value):
    print(f'before: {attrib.name}={new_value!r}')
    return new_value

def after(instance, attrib, new_value):
    print(f'after: {attrib.name}={new_value!r}')
    return new_value

@define(on_setattr=[before, setters.convert, setters.validate, after])
class A:
    b: Optional[int] = field(default=1, validator=validators.instance_of(int))

In [45]: A()
Out[45]: A(b=1)  # before/after NOT called
In [46]: A(b=2)
Out[46]: A(b=2)  # again there is no before/after
In [47]: A().b=3  # they are work **only** when attrib is assigned explicitly
before: b=3
after: b=3
  1. Why on_setattr is ignored on initialization stage? Please explain the logic behind it. It was unexpected for me.
  2. What are before and after here? converters or validators? At least before is called as validator, receiving instance, attrib, new_value, but return value is used further in chain like in converter.
  3. I realize that attrs is not a validator class, but:
    • converters can't access attributes values (including «current» one). This is pretty common when one attribute depends on another.
    • validators can't access «original» or «previous in chain» value, leading to incorrect exceptions (because new_value may be converted at this stage)
  4. Is there any way to skip setters chain for default attribute value? Sometimes initial value can be «invalid» from the point of subsequent assignment, i.e. we can have None as initial attribute value, but assigning None to an attribute is invalid
  5. It can be convenient writing/using simple converters/validators that throws the same class-bound Exception in the end, something like:
@define(setter_exception=ErrorA)
class A:
    b: int = field(converter=lambda v: int(v), validator=[validators.ge(1), validators(le(3))], setter_error_msg='expected to be ⩾1 and ⩽3')

A().b = 'invalid' throws ErrorA(assigned_value, Attribute(), exception_caught)
class B can be defined with setter_exception=ErrorB. So we can use one error handler for assignments for both classes while having all info to produce useful error message, using «original» invalid value, underlying exception and all Attribute() attributes, including predefined error_msg
I've tried to extend attrs in such a way, but failed.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions