python / mypy

Optional static typing for Python
https://www.mypy-lang.org/
Other
18.5k stars 2.83k forks source link

Allow to register new class/dataclass/attribute makers in attrs plugin #5406

Open hynek opened 6 years ago

hynek commented 6 years ago

This is a feature request.

attrs is very much intended to be composable (e.g. https://github.com/hynek/environ_config) but currently the attrs plugin only works on the core library's functions: https://github.com/python/mypy/blob/b2d61741623f2a0573f2b1bdb8391ceed862cf69/mypy/plugins/attrs.py#L24-L37

I would like to be able to signal somehow to mypy, that some function is a wrapper around @attr.s/@attr.ib/@attr.dataclass. Not only would third-party applications benefit, but I could really use it while experimenting on https://github.com/python-attrs/attrs/issues/408.

Any ideas/plans how we could achieve that?

cc also @euresti & @chadrik

euresti commented 6 years ago

FYI in our code I literally append to these lists in a mypy plugin.

I did have a vision that the plugin could support an @class_maker(...) decorator that would do nothing at runtime but let the plugin know that this method (along with some default values) is a class_maker orattrib_maker.

So you'd do:

@class_maker(auto_attribs_default=True)
def my_class_maker(...):
   ...

Or something like that.

ilevkivskyi commented 6 years ago

Another possible pattern we can support is:

if TYPE_CHECKING:
    custom_maker = attr.s
else:
    def custom_maker(cls):
        ...
ilevkivskyi commented 5 years ago

The same problem exists for dataclasses, see https://github.com/python/mypy/issues/6239

ilevkivskyi commented 5 years ago

Just FTR cross-posting the workaround that currently works:

from typing import TYPE_CHECKING
if TYPE_CHECKING:
    from attr import s as custom_maker
else:
    def custom_maker(cls):
        ...

but it is arguably worse than https://github.com/python/mypy/issues/5406#issuecomment-409593090, that currently doesn't work.

euresti commented 5 years ago

Heh. Here's what I did in our code base. I added a plugin that adds some methods when imported. (Did y'all wonder why I made those variables globals?)

from mypy.plugin import Plugin
from mypy.plugins.attrs import (
    attr_attrib_makers,
    attr_class_makers,
    attr_dataclass_makers,
)

# These work just like attr.dataclass
attr_dataclass_makers.add("my_module.method_looks_like_attr_dataclass")

# This work just like attr.s
attr_class_makers.add("my_module.method_looks_like_attr_s")

# These are our attr.ib makers.
attr_attrib_makers.add("my_module.method_looks_like_attrib")

class MyPlugin(Plugin):
    # Our plugin does nothing but it has to exist so this file gets loaded.
    pass

def plugin(version):
    return MyPlugin