attrs icon indicating copy to clipboard operation
attrs copied to clipboard

Typing fails when list of validators where one is custom and one is built in

Open superosku opened this issue 2 years ago • 5 comments

Hi!

I am trying to add multiple validators to a field, which works fine as long as they are both from the attrs library or both written by me. When I mix them up mypy gives me an error. Am I doing something wrong here or is this an issue in the library? Here is a snippet to reproduce the issue:

from typing import Any

from attr import validators, Attribute
import attr


def is_not_empty(instance: Any, attribute: "Attribute[str]", value: str) -> Any:
    if not value.strip():
        raise ValueError("Is not empty")


@attr.s
class MyClass:
    thing1: str = attr.ib(validator=[validators.max_len(10), is_not_empty])  # Mypy complains from this
    thing2: str = attr.ib(validator=[validators.max_len(10)])  # This is fine
    thing3: str = attr.ib(validator=[is_not_empty])  # Fine as well
    thing4: str = attr.ib(validator=[is_not_empty, is_not_empty])  # Even this is fine

This is the error I get from mypy:

snippet.py:14: error: Argument "validator" has incompatible type "list[function]"; expected "Callable[[Any, Attribute[<nothing>], <nothing>], Any] | Sequence[Callable[[Any, Attribute[<nothing>], <nothing>], Any]] | None"  [arg-type]

superosku avatar Oct 31 '23 16:10 superosku

It looks like Mypy type inference isn't strong enough.

reveal_type([validators.max_len(10), is_not_empty])  # Revealed type is "builtins.list[builtins.function]"

Funnily enough, if you hold its hand enough, it works:

from __future__ import annotations
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from attr import _ValidatorType

test: _ValidatorType = is_not_empty

reveal_type([validators.max_len(10), test])  # Revealed type is "builtins.list[def (Any, attr.Attribute[Any], Any) -> Any]"

I couldn't figure out an elegant way of making it work. I'd suggest just sticking a #type: ignore on that line, to be honest.

Tinche avatar Nov 01 '23 23:11 Tinche

I did consider importing the _ValidatorType and that it might fix the issue. But I did not want to do it since it is marked as private (The _ prefix). What do you think about making it public for these types of scenarios?

superosku avatar Nov 06 '23 07:11 superosku

It wouldn't be the end of the world, but I wish we could come up with a better solution.

Tinche avatar Nov 06 '23 12:11 Tinche

Hi, just wanted to say that I'm running into the same issue, so: +1

AdrianSosic avatar Nov 14 '23 09:11 AdrianSosic

I just noticed that this is not restricted to combinations of built-in and custom validators but also holds for lists of built-in validators. Both the following classes do the same job, but class B is flagged by mypy while A passes as expected:

from attrs import define, field
from attrs.validators import and_, instance_of, min_len


@define
class A:
    name: str = field(validator=and_(instance_of(str), min_len(1)))

@define
class B:
    name: str = field(validator=[instance_of(str), min_len(1)])

AdrianSosic avatar Nov 24 '23 10:11 AdrianSosic