cattrs icon indicating copy to clipboard operation
cattrs copied to clipboard

validation options when structuring

Open mmerickel opened this issue 3 years ago • 1 comments

cattrs validation is exclusively type-based right now from what I've been able to surmise. Is there a plan to extend this to any ability to pass options to structure hook on a per-field basis?

At least for classes using attrs/dataclass they can provide per-field metadata that would be great to expose to the hooks.

@attrs.define
class User:
    id: int = attrs.field(metadata=cattrs.metadata(explicit_type=True))

def cattrs_int(value, type):
    # XXX no way to use the attrs.field metadata here
    return int(value)
cattrs.register_structure_hook(int, cattrs_int)

Allowing me to configure a restriction to avoid casting a string to an int. Note the comment and that the type param is int and not the attrs.field leaving it inaccessible.

In other situations that aren't using attrs/dataclass there's likely a similar desire to define per-field options to the hooks.

Right now the workaround seems to be to define my own custom subclasses which is obviously less than ideal:

class ExplicitInt(int):
    @classmethod
    def from_cattrs_structure(cls, value, type):
        if not isinstance(value, int):
            raise Exception('value is not an int')
        return cls(value)

@attrs.define
class User:
    id: ExplicitInt

cattrs.register_structure_hook(ExplicitInt, ExplicitInt.from_cattrs_structure)

Is there a better way than this?

mmerickel avatar Sep 19 '22 18:09 mmerickel

Hey,

I'm on vacation so can't respond quickly.

First, are you aware of attrs validators? (https://www.attrs.org/en/stable/examples.html#validators) This is an easy way to add per-field validation, but it won't help you with your issue of validating ints because cattrs processes the field values before attrs sees them.

As for enabling overriding per-field hooks, I think that's a good idea and we should totally do this.

Brainstorming a little here, the first step would be adding a structure_hook (or maybe just structure?) (and unstructure_hook, I guess) arguments to cattrs.override. So when you'd be able to:

>>> from cattrs.gen import make_dict_structure_fn
>>> converter.register_structure_hook(User, make_dict_structure_fn(User, converter, id=override(structure=explicit_int)))

Next step, I feel the current way of doing customizations like this is too difficult. I was planning on introducing a helper, converter.customize, removing some of the boilerplate.

Then, it'd boil down to:

>>> converter.customize(User, id=override(structure=explicit_int))

I have other things in the queue though. If you'd be willing to give this a go, I can give advice ;)

Tinche avatar Oct 02 '22 17:10 Tinche