python-attrs / attrs

Python Classes Without Boilerplate
https://www.attrs.org/
MIT License
5.3k stars 373 forks source link

mypy returns "Incompatible types in assignment" with on_setattr=setters.convert #889

Open anthrotype opened 2 years ago

anthrotype commented 2 years ago

Not sure if this is a mypy issue or attrs or both.

I have an attrs class that defines a field with a converter that coerces the input value to a custom dict subclass. It works fine when creating a new object, I pass in a dict and the field gets converted to the my dict subclass. I think #710 is responsible for inferring __init__ annotations from the converters'.

Now I would like to do the same with on_setattr, by using attrs.setters.convert: i.e. I want to set the field to a plain dict and have it converted automatically using the field converter to my dict subclass.

This works fine at runtime, but when I run mypy on my code, it complains that Incompatible types in assignment (expression has type Dict[...], variable has type "...") when I do that.

Here's a made up example:

from __future__ import annotations
from typing import Any, Dict, Mapping, Union
from attr import define, field, setters

class Lib(Dict[str, Any]):
    pass

def _convert_lib(v: Mapping[str, Any]) -> Lib:
    return v if isinstance(v, Lib) else Lib(v)

@define(on_setattr=setters.convert)
class Glyph:
    lib: Lib = field(factory=Lib, converter=_convert_lib)

g = Glyph({"foo": [1, 2, 3]})

assert isinstance(g.lib, Lib)

g.lib = {"bar": [4, 5, 6]}

assert isinstance(g.lib, Lib)
$ mypy /tmp/test_mypy_attrs.py
/tmp/test_mypy_attrs.py:23: error: Incompatible types in assignment (expression has type "Dict[str, List[int]]", variable has type "Lib")

Is there a way around this besides an ugly # type: ignore on every line I use the attrs generated setter-cum-converter?

thanks in advance

anthrotype commented 2 years ago

I think this is the same issue as https://github.com/python/mypy/issues/10187

anthrotype commented 2 years ago

Even without the on_setattr feature, just using a plain property setter decorator that calls the converter, I get the same error from mypy about Incompatible types in assignment.

@define(on_setattr=setters.NO_OP)
class Glyph:
    _lib: Lib = field(factory=Lib, converter=_convert_lib)

    @property
    def lib(self) -> Lib:
        return self._lib

    @lib.setter
    def lib(self, value: Mapping[str, Any]) -> None:
        self._lib = _convert_lib(value)

It appears that mypy doesn't support "asymmetric properties" whereby the setter annotation is different from the getter => https://github.com/python/mypy/issues/3004

But am not sure if that's the same or related issue to the on_setattr=convert one.

hynek commented 2 years ago

My gut feeling is that mypy's support for setters is simply too spotty but maybe the typing experts could chime in. :-/