attrs icon indicating copy to clipboard operation
attrs copied to clipboard

`deep_iterable` and `deep_mapping` inconsistencies

Open AdrianSosic opened this issue 2 years ago • 2 comments

Hi 👋 In the last days, I noticed some inconsistent behavior of deep_iterable and deep_mapping. Some of the problems have already been mentioned in other issues (links provided below) but for others it seems there is no open issue yet. Since they are partly related, I thought it would be good to have a single overview of the necessary open fixes:

Lists/tuples of validators

Some of the inner validators accept lists while others still require the explicit use of and_. For consistency, I think all of them should ideally be able to handle lists. For the member_validator, the change has been in implemented in #925, but iterable_validator still struggles with lists. For example, the following piece of code works, but replacing any_ with a list will make it fail:

@define
class A:
    x: List[str] = field(
        validator=deep_iterable(
            member_validator=[instance_of(str), min_len(1)],
            iterable_validator=and_(instance_of(list), min_len(1)),
        ),
    )

For deep_mapping, lists/tuples not supported for any of its three arguments.

Typing support

When a list of validators is passed, mypy complains about the type, as already pointed out in #1197. However, when a tuple is used instead, mypy is happy.

Wrong error message

When a validation error in one of the inner validators of deep_iterable or deep_mapping occurs, an exception is thrown but the contained message is wrong. For example, using the above code and calling A(["abc", ""]), you get the following error:

ValueError: Length of 'x' must be => 1: 0

It rightfully complains about the length of the second item in the list, but note that the message refers to attribute name x, which is incorrect, since it's not x that is too short but one of its items.

Optional validators

For deep_iterable, only the iterable_validator is optional, which makes sense because if no member_validator was provided, one should simply use a regular validator in the first place. However, for deep_mapping, both value_validator and key_validator are required. This makes its use unnecessarily complicated in certain situations. For example, validating only dictionary keys requires a rather cumbersome workaround and also results in unnecessary calls of the value_validator:

@define
class B:
    x: Dict[str, Any] = field(
        validator=deep_mapping(
            key_validator=and_(instance_of(str), min_len(1)),
            value_validator=lambda *x: None,
        )
    )

Instead, I guess the user-friendly way would be to check that at least one of them is provided, but not necessarily both.

AdrianSosic avatar Nov 24 '23 12:11 AdrianSosic

Thanks for the summary – would you mind filing the bugs (additionally) individually, so we can check them off? Have you perchance debugged why the error message is wrong?

hynek avatar Dec 29 '23 15:12 hynek

Sure, will do so 👍🏼

Regarding your question about the error message: Well, I think there is just not enough context information passed when calling the validators on the individual members. I guess what we would ideally like to have is some message that provides the name of the iterable plus the index of the failing element, e.g. in this case

ValueError: Length of 'x[1]' must be => 1: 0

Of cource, even better would be an ExceptionGroup that reports all failing elements, like cattrs would do it.

However, these are the current two lines from _DeepIterable that trigger the validation and there is simply no information about the index passed.

for member in value:
    self.member_validator(inst, attr, member)

AdrianSosic avatar Jan 03 '24 19:01 AdrianSosic

Hi @hynek, here you go, the four issues are now separately tracked in #1243, #1244, #1245 and #1246 😎. So feel free to close this issue if you prefer to keep only the individual ones 👍🏼

AdrianSosic avatar Feb 21 '24 15:02 AdrianSosic

yes thanks we've got enough open ones 🙈

hynek avatar Feb 22 '24 04:02 hynek