-
-
Notifications
You must be signed in to change notification settings - Fork 2.9k
Plugin to typecheck attrs-generated classes #4397
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
Conversation
2c92b81
to
c38d3ac
Compare
Ok this supports quite a bit of features now. Things that aren't working correctly: I think everything else is working fairly well. I kind of wish everything wasn't so hard coded on the method names. Many users of attrs (us included) create helper functions to add metadata and other stuff. I'll probably have to make the code a bit more adaptable to that. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for working on this and sorry for a delay. Here are some comments.
mypy/plugin.py
Outdated
cast | ||
|
||
from mypy import messages | ||
from mypy.nodes import Expression, StrExpr, IntExpr, UnaryExpr, Context, \ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Our style is to use (...)
for multiline imports instead of \
, see for example right below from mypy.types import
etc.
mypy/plugin.py
Outdated
arg_names = [arg.variable.name() for arg in args] | ||
arg_kinds = [arg.kind for arg in args] | ||
assert None not in arg_types | ||
signature = CallableType(cast(List[Type], arg_types), arg_kinds, arg_names, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it possible to avoid the cast
, why it is necessary? We try to avoid using casts.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The named tuple code does the same thing. arg_types is technically a List[Optional[Type]]
but note the assert None not in arg_types
right above to make sure it's right.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the cast is not required with that assert. Have you actually tried removing it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I get:
mypy/plugin.py:580: error: Argument 1 to "CallableType" has incompatible type "List[Optional[Type]]"; expected "List[Type]"
When running mypy mypy/plugin.py --strict
mypy/plugin.py
Outdated
default: Optional[bool]) -> Optional[bool]: | ||
for arg_name, arg_value in zip(call.arg_names, call.args): | ||
if arg_name == name: | ||
# TODO: Handle None being returned here. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't forget to fix TODO items before the PR is merged.
mypy/plugin.py
Outdated
return | ||
|
||
# To support nested classes we use fullname(). But fullname is <filename>.<variable name> | ||
# and named_type() expects only the name. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't understand this comment. Why do you need the self type here? It is normally bound at the call site (i.e. type variables of a generic type are fixed).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh I see. AnyType(TypeOfAny.unannotated)
is what i need here! Thanks!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh wait. I need it for
def __lt__(self, other: SelfType)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, then please use it only in methods that need it. Also please double-check that this reflects the actual runtime semantics/docs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, I found out that if i do ctx.api.accept(func)
on the FuncDef I create it does a lot of things for me (setting the type of the self arg, being one of them). Is that a reasonable thing to do?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is that a reasonable thing to do?
I am not sure it is really needed, but if you find a case where this is necessary then go ahead.
mypy/plugin.py
Outdated
for stmt in info.defn.defs.body: | ||
if isinstance(stmt, AssignmentStmt) and isinstance(stmt.lvalues[0], NameExpr): | ||
lhs = stmt.lvalues[0] | ||
name = lhs.name.lstrip("_") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why this is necessary? Does attr
has specific treatment of underscores? If yes, then please add a comment.
mypy/test/data.py
Outdated
@@ -101,6 +101,13 @@ def parse_test_cases( | |||
src_path = join(os.path.dirname(path), arg) | |||
with open(src_path) as f: | |||
files.append((join(base_path, 'typing.pyi'), f.read())) | |||
elif p[i].id == 'add-module': |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This whole block is not necessary, just put the "pseudo stub" for attrs
in mypy/test-data/unit/lib-stub
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok cool. I didn't know if that was ok since it's not a stdlib.
test-data/unit/fixtures/attr.pyi
Outdated
@@ -0,0 +1,14 @@ | |||
from typing import TypeVar, overload, Callable, Any |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Move this file to mypy/test-data/unit/lib-stub
as I explained above.
test-data/unit/check-attr.test
Outdated
UnTyped(1, 2) == UnTyped(2, 3) | ||
UnTyped(1, 2) >= UnTyped(2, 3) | ||
[builtins fixtures/attr_builtins.pyi] | ||
[add-module fixtures/attr.pyi] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You don't need these lines.
test-data/unit/check-attr.test
Outdated
[builtins fixtures/attr_builtins.pyi] | ||
[add-module fixtures/attr.pyi] | ||
|
||
[case testTypedAttrS] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This and some other tests are too large. Try splitting it in half (like the previous test).
test-data/unit/check-attr.test
Outdated
reveal_type(C) # E: Revealed type is 'def (y: Any) -> __main__.C' | ||
reveal_type(C.D) # E: Revealed type is 'def (x: builtins.int) -> __main__.C.D' | ||
[builtins fixtures/attr_builtins.pyi] | ||
[add-module fixtures/attr.pyi] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you please add more tests? Check not only that definitions are processed correctly, but how they interact with various other concepts. For example, try to have good coverage at least for:
- type aliases (both inside and outside of a class)
- type variables
- decorated generic classes
- type inference
- forward references (both inside and outside of class)
- importing decorated classes
- instance and class methods in decorated types
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The only tests I couldn't write because I wasn't sure what you meant were "type inference" But I think I covered everything else. Let me know if you want more tests.
Support for data classes is a separate topic/PR. Also, they will probably be supported directly like named tuples (not via plugin). |
Actually attr has a
That's why there was a smiley. Thanks for the review. I'll probably play with things again after the break. |
Ah, OK, I didn't know. |
Ok I believe this is now feature complete. It's pretty long because the attrs feature set is pretty large. Let me know what other changes you want. |
mypy/plugin.py
Outdated
|
||
|
||
# The names of the different functions that create classes or arguments. | ||
attr_class_makers = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I could use some advice/opinion here. It's fairly common when using attrs to write your own wrappers around the functions to make coding easier. However the plugins only work on specific names so I tried to leave some way for users to add their own "class makers" and "attrib makers" to the plugin. But that's probably not the right way to do this.
I think it would be good if @ambv looked at it. IIRC he is a big attrs user
(and co-author of PEP 484).
…On Dec 26, 2017 3:37 PM, "David Euresti" ***@***.***> wrote:
Ok I believe this is now feature complete. It's pretty long because the
attrs feature set is pretty large. Let me know what other changes you want.
—
You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub
<#4397 (comment)>, or mute
the thread
<https://github.com/notifications/unsubscribe-auth/ACwrMk24KiF9UznjZSUVtIg0gMCy0lD-ks5tEXU8gaJpZM4RH9QS>
.
|
This is a good idea. I already mentioned that it would be great if someone familiar with Anyway, I will also make another pass of review soon. |
This stuff is way over my head :) but one thing I’ve noticed: |
Thanks! This is exactly the kind of feedback I wanted to see. Also later (when there will be more tests), you can comment if the error messages given by mypy are reasonable. |
I can add support for |
Ok I've added support for I'm a little unhappy because in order to support subclassing correctly I had to add something to the Another way would be to add a I'm also starting to think that the code is pretty large to all live in one method and am wondering if I should put in a separate file and even maybe a separate plugin from |
(I'm hoping Ivan will be able to review this in the next few weeks and guide it to completion.) |
Ok I'm now mostly happy with the code. I got rid of the magic numbers, the MRO works correctly, and the tests pass. Feel free to review when you're ready. Also this is my first big mypy project so I expect I did lots of things wrong. And a musing for v2: The only other thing we might want is a way to allow users to say: this function returns an attrib or this function basically creates an attr class. I was thinking it could be a decorator. Something like:
At runtime the decorator does nothing, but in mypy we'd see the decorator and add the function to the list of functions we process. Additionally the decorator could change the default values for the methods (e.g. auto_attribs) etc. However since that requires commits in attrs, typeshed and mypy I'd figure that's best left to v2. :) |
Sigh. Just noticed that my caching of |
Regarding tests, I've added pretty thorough tests to my attrs PR for pyi stubs which cover both the attrs stubs and mypy plugin. I'm not sure where this should ultimately live: mypy, typeshed, attrs, or some combination thereof? |
@euresti There is no specific timeline IIUC, but I think we could get this in (including typeshed PR) in next few days. This is almost ready. As for the incremental mode, maybe we can just allow plugins to add keys to a special @attr.s(frozen=True)
class C:
x: int = attr.ib(init=False)
y: str Then you can just write (pseudocode, hopefully you get the idea): type_info.metadata['attrs'] = {'fileds': {'x': [], 'y': ['init']}, 'flags': ['frozen']} Taking into account the flags (both global and per-field) and the types that you can find in class symbol table, you should be able to reconstruct all the necessary info from a deserialized base class. |
Since decorators are important for |
Definitely on my mind. Wanna get the serialization working first though. |
Dang that was hard. But it works! Comments welcome! |
Cool! This looks much better. I won't block you on this basis alone but we might want to split As I said though, I won't bother you with doing this right now. Just something to keep in mind for future work. |
Ok I refactored a bit more and cleaned up some stuff related to the converter method. Will leave this alone for a bit until I hear something. Thanks! |
23d5e04
to
feab654
Compare
@ilevkivskyi @ambv Any more comments on this PR? I've already started using it on our code base and it's been pretty great. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is most probably my last review on this. I didn't look carefully through the code, but I already reviewed it few times and my understanding is that the latest changes are purely "logistical", i.e. writing some code as helper functions.
Thanks for this great work, this will be a useful addition for mypy users (and sorry for being pedantic sometimes :-)
mypy/attrs_plugin.py
Outdated
@@ -0,0 +1,498 @@ | |||
"""Plugin for supporting the attrs library (http://www.attrs.org)""" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would put this in a separate folder, not directly in mypy
. I think mypy/plugins/attrs.py
would be better.
[out1] | ||
main:6: error: Revealed type is 'def (x: builtins.int) -> __main__.B' | ||
[out2] | ||
main:6: error: Revealed type is 'def (x: builtins.int) -> __main__.B' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please add more tests for incremental mode. Here are some ideas:
- Check situations with more attributes
- Checks that access to attributes after deserialization results in a correct type and "kind" (i.e. being frozen etc.)
- Check that signatures of deserialized dunder methods are correct.
- Check situations where base attr class in module
a.py
is not changed in the second run, but its subclass in moduleb.py
is changed so that this produces a type error. - Also vice-versa (error to no error)
- Add test with more than two files
- Add test with more than two incremental runs (for example: no error -> error -> no error in a subclass)
Here are two additional notes about incremental tests:
- If a file results in errors, it is never actually deserialized from cache, this includes
reveal_type
. So although you can keep current tests (to check serialization, which happens always), please avoidreveal_type
in future incremental tests, instead use an assignment to check that the type is as expected. - To add changes between runs, use this pattern:
[file a.py]
import b
...
[file a.py.2]
import b
...
[file b.py]
...
Important note: you don't need to add .2
, (and .3
in one test) to all files, only to the files that are actually changed. It is better to actually not do this (to avoid touch
ing unchanged file, and therefore considering its cache stale).
Don't add too many tests, just one case for each item I listed above.
Thanks for the review. I've added a couple more tests. I hope I got all your points. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is still some roughness, but I think we should move forward. Maybe we can polish implementation details later. If no-one is against, then I will merge this soon.
+1 on merging!
|
python/mypy#4397 to be present in a release
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.@x.default
and@y.validator
decorators which are only part of class creation.Fixes #2088