cattrs
cattrs copied to clipboard
validation options when structuring
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?
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 ;)