attrs icon indicating copy to clipboard operation
attrs copied to clipboard

Type checking unexpectedly passing when misusing `default` and `converter`

Open alexpizarroj opened this issue 5 years ago • 4 comments

I was playing around with type annotations and Attrs, and I think I may have found an issue.

Given the following Python 2/3 script:

from __future__ import absolute_import

from pprint import pprint as pp

import attr


def _convert_int(val):
    # type: (int) -> int
    if val <= 0:
        return -1
    return val


@attr.s
class A(object):
    x = attr.ib(default=0, converter=_convert_int, type=int)
    y = attr.ib(default=None, converter=_convert_int, type=int)  # `default` is incompatible with our declared `type`


def main():
    # type: () -> None
    a1 = A(5, 5)        # correct: type checks
    pp(a1)
    a2 = A(5, None)     # correct: doesn't type check
    pp(a2)
    a3 = A(5)           # incorrect: type checks but shouldn't!
    pp(a3)


if __name__ == '__main__':
    main()

I'd expect MyPy to complain about the relationship between default and converter for y. Nevertheless, I'm getting:

image

Which is one error less than expected.

Should I perhaps report this to MyPy or Typeshed as well/instead?

alexpizarroj avatar Dec 27 '19 13:12 alexpizarroj

Is this related to #518? 🤔

I remember that there were issues with converters and typing but I don't remember the details.

hynek avatar Dec 30 '19 10:12 hynek

Yeah this is the same bug. Converters are extremely hard to handle. :)

euresti avatar Dec 31 '19 17:12 euresti

Just cause I am reading through issues: is the root problem not using converters.optional? Or alternatively, adding a None check in the local validator?

from pprint import pprint as pp

import attr
from attr import converters


def _convert_int(val):
    # type: (int) -> int
    if val <= 0:
        return -1
    return val


@attr.s
class A(object):
    x = attr.ib(default=0, converter=_convert_int, type=int)
    y = attr.ib(default=None, converter=converters.optional(_convert_int), type=int)  # `default` is incompatible with our declared `type`


def main():
    # type: () -> None
    a1 = A(5, 5)  # correct: type checks
    pp(a1)
    a2 = A(5, None)  # correct: doesn't type check
    pp(a2)
    a3 = A(5)  # incorrect: type checks but shouldn't!
    pp(a3)


if __name__ == '__main__':
    main()

seems to produce the expected response

stevetarver avatar Aug 03 '20 16:08 stevetarver

@stevetarver In my example, I want y to not allow None.

In any case, I believe this goes beyond being an issue with converters only, since I would have expected the following excerpt to fail at class definition:

import attr


@attr.s
class Foo(object):
    bar = attr.ib(default=None, type=int)


a = Foo(5)
b = Foo(None)  # L10
c = Foo()

Strict MyPy v0.782 output (mypy --strict ~/tmp/test.py):

/Users/apizarro/tmp/test.py:10: error: Argument 1 to "Foo" has incompatible type "None"; expected "int"
Found 1 error in 1 file (checked 1 source file)

ghost avatar Aug 03 '20 17:08 ghost