-
-
Notifications
You must be signed in to change notification settings - Fork 2.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Typechecking attrs-generated classes #2088
Comments
Hi Laurens, this looks like something that should be doable with the (as yet hypothetical) plugin architecture: #1240 |
A bit more detail, as the problem here isn't just that you can't validate Here's a script from pathlib import Path
from attr import attrib, attrs
from attr.validators import instance_of
@attrs()
class ThingWithPath(object):
_path = attrib(validator=instance_of(Path))
def exists(self):
return self._path.exists()
thing1 = ThingWithPath(Path("."))
print("thing1 exists:", thing1.exists())
thing2 = ThingWithPath(path=Path("."))
print("thing2 exists:", thing2.exists()) Which runs thusly (note the runtime type validation, if that's a thing you like): wsanchez$ python thing.py
thing1 exists: True
thing2 exists: True
Traceback (most recent call last):
File "thing.py", line 21, in <module>
thing3 = ThingWithPath(".")
File "<attrs generated init 72ca13b0f10a00b12fb26accfd84d196fc2af0c6>", line 5, in __init__
File "/Users/wsanchez/Dropbox/Developer/PIE/wsanchez/ConfigService/.tox/py35-coverage/lib/python3.5/site-packages/attr/validators.py", line 24, in __call__
attr, self.type, value,
TypeError: ("'_path' must be <class 'pathlib.Path'> (got '.' that is a <class 'str'>).", Attribute(name='_path', default=NOTHING, validator=<instance_of validator for type <class 'pathlib.Path'>>, repr=True, cmp=True, hash=True, init=True, convert=None, metadata=mappingproxy({})), <class 'pathlib.Path'>, '.') Running thing.py:15: error: Too many arguments for "ThingWithPath"
thing.py:18: error: Unexpected keyword argument "path" for "ThingWithPath"
thing.py:21: error: Too many arguments for "ThingWithPath" And |
It's unfortunate, but not unique to attrs -- this is a general problem when you have a metaclass or a class decorator that affects the API of a class. I suppose we could teach mypy to special-case attrs, but that just moves the problem to every other library engaging in such tricks. A more principled approach would be to add some new magic primitive to PEP 484 (and support in mypy of course) that would allow one to write a stub for attrs that explains to mypy what the effect of the class decorator is. This might be easier now than when this issue was created given that we already have some support for metaclasses now (#2475), so some of the necessary scaffolding in mypy already exists. You could also imagine that mypy could just parse the full source code for the attrs package and understand what it does without needing any help. But I think that's unlikely to be successful, due to the difficulty for mypy to understand the dynamic tricks such an implementation undoubtedly uses. (I haven't looked.) In general, supporting code that does runtime manipulation of interfaces is not high on the list of things that mypy and PEP 484 are likely to support soon. It's generally a bit antithetic to the idea of static types, after all. :-) |
Yup, I was just trying to be more specific about the issue here. I'm not sure I agree that That said, I'm fairly new to mypy and the typing system. I agree that being able to write a stub would be useful absent more robust but considerably harder to implement solutions. |
I agree with this assessment. attrs itself is not antithetical, though it's current implementation obviously is. It's really about supporting better use of types, by making them simpler to declare and be useful. I'm sure that if there were a way to support type systems at the same time, that would definitely fall in line with the goals of attrs. Whether the real solution is pluggability for the type system to allow for this kind of compile-time transformations, or whether the only real solution is to have something that fulfills the goals of attrs built into the language, I'm not sure, but they are two approaches that are both trying to encourage the proper use of types. The dynamic implementation is just that, an implementation detail. |
Well, mypy doesn't execute the program. It just reads the source code. So in order to validate the |
If it helps anyone, we solved this problem recently (specifically with attrs) where we define the init ourselves for attrs. Maybe it could help someone else too! Reusing the example from above: @attrs(init=False)
class ThingWithPath(object):
_path = attrib(validator=instance_of(Path))
def __init__(self, _path):
self._path = _path
def exists(self):
return self._path.exists() |
There's discussion on an API in attrs (here: python-attrs/attrs#151) that would help bridging this. I'm following along since I like both typing and attrs for removed (and always correct!) boilerplate. |
@fxfitz: As @ambv notes, the problem there is that your solution removes one of the big benefits of I suppose that if @attrs()
class ThingWithPath(object):
_path = attrib(validator=instance_of(Path))
def __init__(self, _path: Path): # Note added type annotation here
pass # This implementation isn't used, so pass
def exists(self):
return self._path.exists() It would be confusing for someone who may put code into |
@wsanchez Ah right, I forget that if you |
Hello, I'm one of the attrs maintainers. I think it's fair to say mypy will have a really hard time ever working out of the box with attrs since we do some really filthy things under the hood. I also think making attrs work with mypy would be a very worthwhile thing to try to do, since the point of attrs is to make people use structured data more and this is very complimentary to the idea of static typing checks. I'd be interested in maybe being involved with the effort to develop a plugin API, if that's the direction we're going in, and ultimately developing a plugin for attrs. |
Great plan! I wonder if you could approach this from the other direction: first write a mypy patch that supports attrs, hard-coding recognition of the special constructs it supports; and then, learning from that experience, figure out how a plugin API should be designed. |
I'll give it a shot. No promises on the timeline :) |
Mypy now has the beginnings of a plugin architecture. It can't quite support attrs yet, but it shouldn't be too hard to extend it with a few extra hooks to allow attrs support. |
Is there any update on this? |
No updates yet. |
I spent a week in the summer going through mypy's source. I came to the conclusion we would need another plugin type, something that runs in between semantic analysis second pass and type checking and modifies attrs classes (IIRC). |
That sounds reasonable. The new plugin type could be triggered by having a specific class decorator, base class or metaclass. For attrs specifically a class decorator would be the only needed trigger, right? |
We just need to be sure that this will work correctly with forward references :-) |
@JukkaL It'd be great if it could be triggered on a class decorator that resolved to a specific name (in this case exactly "attr.s" or "attr.attrs"). I say "resolved" to handle people wrapping This could be reused for dataclasses (PEP 557) if it gets passed. It would also be awesome if this kind of plugin could be pip installed and enabled somehow. Then we could version the plugin on PyPI and do synced releases with attrs. |
It would be super cool if we could get some movement into this! 🎉 attrs has just merged type support based on both PEP 526 and as an argument which should lay some groundwork I believe? Would it be easier to add support only if PEP 526 is supported for now? That would be totally good enough for me. :) |
I support having a plugin for this; I think the plugin will have to be contributed (Tin, Hynek, @ambv?). Jukka should be able to help you getting you started (the plugin API is not yet well-documented), but you'll have to do most of the work yourself, as this is not on the roadmap (and neither is PEP 557, dataclasses BTW). |
I seem to remember that @chadrik mentioned that he'd like to get involved too. I won't be able to work on the plugin but will happily help with work that's necessary on the attrs side. |
I will probably have some time second half of October to take care about dataclasses. Concerning the plugins, I already mentioned this, I would prefer not to allow plugins to replace |
Yeah, just directly modifying We might eventually need some additional features to support specific libraries such as Django models. For example, I believe that Django supports declaring an attribute type using a string literal without explicitly importing the target class, which could make things hard for mypy. The extensions can wait until we have basic support in, however. |
@Tinche, I believe that the right point for a plugin is as a replacement for metaclass[1]. I.e. mypy will read the class, summarize it and send the result to the plugin. The latter, in turn, will return a call to some It should be possible to port existing "plugins" such as support for namedtuple, enum and typeddict. I would like to work on this API, but I think it should wait until after (significant part of) #4083. [1] I am speaking about metaclass but this is applicable for decorators too. |
I tried playing around with this. I hacked up mypy to call
Anyway I believe that if there was a plugin for a class decorator then this could be easily done. I tried looking into where one would add such a thing but I must confess that I got scared. Any pointers? I have a medium sized code base that uses attrs everywhere and so I can't bring in mypy so I'm motivated to work on this in my spare time. |
I don't think there are any docs for plugins, and that's semi-intended -- we don't want people to go develop their own plugins yet and then have them break when the interface changes. So your only source of information is reading the code of the existing plugins. That said, I think it's important that we have this particular plugin in the mypy codebase, so we will take any PRs you send our way seriously! |
It is a good idea to add a processing step in Also note this older comment
You can ask me if you need some more help with this. |
Cool. Just so I see if I'm going into the correct code, It looks like In there I can check the decorator and do one of a couple of things.
or
Based on your comment I assume 2 is what we would want. Now what's the best way to create this SymbolTableNode? I tried Hmm. Maybe I should join a chat channel somewhere instead of commenting here. Let me know. |
Yes, you can add a plugin hook somewhre after
Yes, but I think for some technical reasons you will need to create a "fake" add_method('__init__', ret=NoneTyp(), name=info.name(),
args=[make_init_arg(var) for var in vars]) Something like this should be in your plugin that will be called by the hook.
Like any other class, by just calling it. Again, check this in info.names[var.name()] = SymbolTableNode(MDEF, var)
I think it is OK to discuss here. Or instead you can make a WIP PR and we can continue there. You can mention @Tinche there, he also seems to be interested in this. Just to reiterate, there are two separate steps:
|
This makes it a lot easier to define simple classes. Plus, something like it is going to be standardized soon: https://www.python.org/dev/peps/pep-0557/ Note that an incompatibility between attr and mypy leads to typechecking errors when using @DataClass constructors because the __init__ function is generated. A plugin should be on the way: python/mypy#2088
Ok cool. I've played with You mentioned that we should only be modifying I'm thinking something like this could work.
Also which part of the code should do the Ok here's a proposal and you can tell me if I'm way off. The plugin takes the This should make it possible to allow the plugin to add other methods if it wants. Let me know! |
Oh wait. Just re-read the thread:
And the function will return |
Yes. You will need to add a new plugin kind. If you have time, I think it makes sense (to be future proof) to ensure that plugins can be triggered not only by a specific decorator full name, but also by a specific metaclass or base class full name.
Yes, this looks like a reasonable API for the plugin. The plugin should modify |
Also use the plugin to demo adding __init__ to attr.s Helps python#2088
@ilevkivskyi Just added an RFC PR. (Sorry for pinging, I don't know if github notifies when PRs are added) |
@Tinche @hynek Now that the plugin scaffolding is nearly finished I started working on the actual plugin. You can see the code here: https://github.com/euresti/mypy/blob/attrs_plugin/mypy/plugin.py#L421 It actually works!
Which now gives the following output from mypy:
But I'm not sure if this code belongs in Also I imagine |
Cool! I can take a deeper look when I have time. Now might be a good time to consider where this code might actually live. Does mypy support this plugin living outside and somehow picking it up after it being pip installed? We will want to have this plugin versioned according to which attrs version it can support. |
Great work David, I'm super excited that someone found the time to pull this thru! I think part of the question where to put it also kind of depends on how the plugin APIs will evolve? It would be unfortunate if certain versions of attrs only worked with certain versions of mypy. So I think it might make sense to to put it either into mypy or start a new package in the python-attrs orga? attrs proper seems like the worst possible place at this point. |
@euresti Stub files should go to typeshed. After PEP 561 is accepted and implemented, you can host the stub wherever you want and distribute them via PyPI/ As for the plugin it is less clear. Currently, mypy doesn't support plugins installed via
I think option 1 is the fastest way to get the |
I think option 1 is the best for both projects until your plugin API stabilises. In the end you’ll get failing tests if you break something. :) Once the plugin API is solid, we can talk about extracting it into a separate project.
|
I agree that including the plugin with mypy is the right course of action. It may be a while before mypy supports installing plugins through pip. |
@euresti OK, it looks like everyone likes the first option, so go ahead and make a PR to mypy. |
Such plugin kinds will be useful for projects like attrs, see #2088
See http://www.attrs.org/en/stable/how-does-it-work.html for information on how attrs works. The plugin walks the class declaration (including superclasses) looking for "attributes" then depending on how the decorator was called, makes modification to the classes as follows: * init=True adds an __init__ method. * cmp=True adds all of the necessary __cmp__ methods. * frozen=True turns all attributes into properties to make the class read only. * Remove any @x.default and @y.validator decorators which are only part of class creation. Fixes #2088
attrs removes object boilerplate:
This has many advantages, like eq/hash behavior working, reprs working, easy to add runtime validators, &c. However, it's not obvious to me how I can typecheck creating instances of this, because there's no
__init__
. (I'm using Python 2.7, so comment syntax, if it matters :))The text was updated successfully, but these errors were encountered: