-
-
Notifications
You must be signed in to change notification settings - Fork 385
Add **kwargs
to validator function calls for more flexibility
#1421
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
I've played around with attrs a lot since opening this issue, and I think I've narrowed down my critiques somewhat. Here's a (fairly) simple example of the kind of syntax I'm trying to gain. Imagine that I'm writing a suite of validators, where each one is only run if the given def conditional(severity):
"""
Only run the validator if `mode` is greater than a given severity.
If an `errors` list is provided, mutate that instead of raising.
"""
def decorator(meth):
def validator(self, attr, value, mode=None, errors=None):
if mode is None or mode < severity:
return
try:
meth(self, attr, value) # No kwargs needed (yet)
except Exception as e:
if errors is None:
raise e
else:
errors.append(e)
return validator
return decorator
@attrs.define
class Example:
x: int = attrs.field(default=10)
@x.validator
@conditional(ValidationMode.STRICT)
def _validate_x(self, attr, value):
raise TypeError("blah")
def validate(self, mode):
"""
Runs all the validators on `self`. Essentially the same as `attrs.validate`
but with the `mode` and `errors` argument.
"""
error_list = []
for attr in attrs.fields(type(self)):
if attr.validator is None:
continue
attr.validator(self, attr, getattr(self, attr.name), mode=mode, errors=error_list)
return error_list
# This works pretty well:
e = Example()
error_list = e.validate(ValidationMode.STRICT)
print(error_list) # [TypeError('blah')]
error_list = e.validate(ValidationMode.NONE)
print(error_list) # [] However, it becomes impossible for me to simply add more than one decorated validator to @x.validator
@conditional(ValidationMode.PEDANTIC)
def _validate_x_2(self, attr, value):
raise UserWarning("blah 2") ... because
On its own, this is not catastrophic; I've already implemented my own from attr._make import _CountingAttr
def my_validator_decorator(self, meth):
...
_CountingAttr.validator = my_validator_decorator But while this is great for my circumstance, it entirely messes with any other library using this decorator, which is a problem waiting to happen. A better pattern would probably be to subclass class _MyCountingAttr(_CountingAttr):
def validator(self, meth):
# I could even probably move the `conditional` decorator logic into here,
# saving even more keystrokes across my codebase
...
def unstructure(self, meth):
# Register a cattrs hook for this specific field on this specific class
...
# etc.
def custom_field(**kwargs):
return _MyCountingAttr(**kwargs) Unfortunately, I cannot do this alone because attrs specifically checks for instances of ...
elif auto_attribs is True:
ca_names = {
name
for name, attr in cd.items()
if attr.__class__ is _CountingAttr
}
ca_list = []
annot_names = set()
for attr_name, type in anns.items():
if _is_class_var(type):
continue
annot_names.add(attr_name)
a = cd.get(attr_name, NOTHING)
if a.__class__ is not _CountingAttr:
a = attrib(a)
ca_list.append((attr_name, a))
... I could maybe convert my instances of def custom_define(cls):
for name, value in {k: v for k, v in cls.__dict__.items() if isinstance(v, _MyCountingAttr)}.items():
setattr(
cls,
name,
_CountingAttr(
default=value._default,
validator=value._validator,
repr=value.repr,
cmp=None,
hash=value.hash,
init=value.init,
converter=value.converter,
metadata=dict(value.metadata),
type=value.type,
kw_only=value.kw_only,
eq=value.eq,
eq_key=value.eq_key,
order=value.order,
order_key=value.order_key,
on_setattr=value.on_setattr,
alias=value.alias,
)
)
return attrs.define(cls)
@custom_define
class Example:
... But this is not particularly elegant. Reviewing solutions again with this in mind:
|
**kwargs
to validator function calls for more flexibility
Part of the reason why attrs is so appealing is that all validations are essentially just plain functions that are run at certain times. However, I find myself pining for a little bit more control on how those functions are run. Notably, I'm looking for the ability to collect a report of all exceptions/warnings issued from validators in some object returned to users, which can then be manipulated and optionally reissued (somewhat similar to how cattrs uses
ExceptionGroup
s, but on the attrs instance itself). In a trivial case, consider a toy example where two validators always raise an exception:The simplest solution I can think of is to simply change the signature of
validate()
and validators to handlekwargs
:This would allow you pass whatever values you wanted to
attrs.validate()
and they would be passed straight through to every evaluated function. In cases where no extra args are specified (such as during attribute setting) then they would either error (appropriately) or resolve to defaults:To achieve simple error collection, I could simply pass in an optional list into kwargs and have errors be collected there if a validator detects said list:
In essence, this works in the same way as Pydantic's
context
argument. Writing a decorator like@collect_errors
to remove the boilerplate of the if statement would be trivial, while also preserving the notion that validators are just "regular" functions. This (to me) seems a very simple and unobtrusive way to expand the functionality of existing validators while also maintaining backwards compatibility, though it unfortunately is only limited to cases where you explicitly callattrs.validate()
.I don't believe cattrs itself is a solution here, because from what I understand cattrs is only concerned with validation of the object as it is being converted to/from a "raw" format (such as a dictionary to an attrs instance). In my case, I want the validators to run on manipulation of the attrs class itself, which only attrs itself is in charge of.
The text was updated successfully, but these errors were encountered: