-
-
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
Add support for NamedTuple methods #3081
Conversation
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.
Pretty close to the target!
mypy/semanal.py
Outdated
self.fail('Cannot overwrite NamedTuple attribute "{}"'.format(prohibited), | ||
named_tuple_info.names[prohibited].node) | ||
|
||
named_tuple_info.names.update(nt_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.
Could you assert here that the sets of keys merged here are distinct? ISTM if there's a key being overwritten by this update()
call it's problematic.
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 sets will actually always overlap, because build_namedtuple_typeinfo
inserts names for the namedtuple fields, and visiting the class body will insert those names again. I think that's safe because build_namedtuple_typeinfo
is very restrictive in the kind of statements it accepts within a class body, but I'll add some more tests to verify that it's correct (you already suggested some below).
mypy/semanal.py
Outdated
@@ -912,9 +936,11 @@ def check_namedtuple_classdef( | |||
for stmt in defn.defs.body: | |||
if not isinstance(stmt, AssignmentStmt): | |||
# Still allow pass or ... (for empty namedtuples). | |||
# Also allow methods. | |||
if (not isinstance(stmt, PassStmt) and |
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'd rewrite this using positive logic -- if not X and not (Y and Z) and not Q
is a little hard to understand.
mypy/semanal.py
Outdated
@@ -3488,8 +3515,10 @@ def visit_func_def(self, fdef: FuncDef) -> None: | |||
self.errors.pop_function() | |||
|
|||
def visit_class_def(self, tdef: ClassDef) -> None: | |||
for type in tdef.info.bases: | |||
self.analyze(type) | |||
# NamedTuple base classes are special; we don't have to check them again 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.
Call out in which function they are analyzed?
@@ -482,3 +483,76 @@ Y(y=1, x='1').method() | |||
class CallsBaseInit(X): | |||
def __init__(self, x: str) -> None: | |||
super().__init__(x) | |||
|
|||
[case testNewNamedTupleWithMethods] | |||
# flags: --python-version 3.6 |
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.
Do you still need this flag? IIRC mypy now defaults to 3.6 everywhere?
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 feel strongly about it, but it seems mildly useful to call out that this code will only work in 3.6 and not earlier versions.
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.
But we're not doing consistently, so I don't think it adds value.
class MagicalFields(NamedTuple): | ||
x: int | ||
def __slots__(self) -> None: ... # E: Cannot overwrite NamedTuple attribute "__slots__" | ||
def __new__(cls) -> None: ... # E: Cannot overwrite NamedTuple attribute "__new__" |
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'd like to see a test that shows you can't have both a method and a field with the same name (the update()
call I pointed out made me nervous about that).
mypy/semanal.py
Outdated
@@ -151,6 +151,11 @@ | |||
FUNCTION_FIRST_PHASE_POSTPONE_SECOND = 1 # Add to symbol table but postpone body | |||
FUNCTION_SECOND_PHASE = 2 # Only analyze body | |||
|
|||
# Matches "_prohibited" in typing.py | |||
NAMEDTUPLE_PROHIBITED_NAMES = ('__new__', '__init__', '__slots__', '__getnewargs__', |
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.
Maybe you don't need this? There's already an error when you define a field starting with underscore.
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.
That's only for fields, but we also need to disallow methods with these names.
mypy/semanal.py
Outdated
self.enter_class(defn) | ||
named_tuple_info = self.analyze_namedtuple_classdef(defn) | ||
if named_tuple_info is not None: | ||
# temporarily clear the names dict so we don't get errors about duplicate names 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.
While you're at it I prefer comments to start with a capital letter and ended with proper punctuation (except for non-whole-sentence inline comments).
PS. Would also like to see @ilevkivskyi's review. |
Thanks for the review! I'll push an update soon. I realized we still disallow docstrings in namedtuples. Do you mind if I fix that in this PR too? Otherwise I can send another PR. |
Yes please. |
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.
Just few more comments from me.
@@ -2128,6 +2154,7 @@ def add_field(var: Var, is_initialized_in_class: bool = False, | |||
add_field(Var('_field_types', dictype), is_initialized_in_class=True) | |||
add_field(Var('_field_defaults', dictype), is_initialized_in_class=True) | |||
add_field(Var('_source', strtype), is_initialized_in_class=True) | |||
add_field(Var('__annotations__', ordereddictype), is_initialized_in_class=True) |
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 could be unrelated to this PR, but despite the fact that you define it here as ordereddicttype
it is revealed as dict
in tests. Also, __annotations__
is still just a dict
(although it is ordered in 3.6+) and does not have OrderedDict
methods like move_to_end()
.
Also I am not sure why you need it here. Latest typeshed stubs define object.__annotations__
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.
ordereddictype
and dictype
are actually the same thing here (they're initialized the same way). Maybe I should just have cleaned up ordereddictype
.
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.
Also, mypy really does think that NamedTuples don't have __annotations__
if I remove this line.
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.
__annotations__
is in fact an OrderedDict at runtime; see _make_nmtuple
in typing. I tried to actually specify OrderedDict in the code here, but couldn't get it work, maybe because this code runs early enough in semantic analysis that we don't have other modules imported yet.
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.
Also, mypy really does think that NamedTuples don't have
__annotations__
if I remove this line.
This looks like a bug (although unrelated to this PR).
|
||
class Base(NamedTuple): | ||
x: int | ||
def copy(self: T) -> T: |
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 test another recently added feature: overload
ed methods.
|
||
class Child(Base): | ||
def new_method(self) -> int: | ||
return self.x |
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 add tests checking for self[0]
inside a method, its type, access, and error on assignment.
mypy/semanal.py
Outdated
nt_names = named_tuple_info.names | ||
named_tuple_info.names = SymbolTable() | ||
|
||
self.bind_class_type_vars(named_tuple_info) |
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 are few old issues to enable generic named tuples, see e.g. #685
The fact that you are binding an empty symbol table here and then repopulating it later, may complicate the implementation of generic named tuples in future.
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.
Generic namedtuples would be nice, but I'm not sure there's anything actionable here. Or should we approach support for methods in a different way?
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.
Or should we approach support for methods in a different way?
I don't have a strong opinion here, but if there is another possible way in view, I would prefer not to trick SymbolTable
.
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.
Agreed the save/restore trick feels unprincipled.
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 agree that it's not ideal but an alternative implementation would probably be more complicated.
I was testing with an outdated version of typing. Replacing _source does work. I'm also OK with deciding that all of this is too obscure to support and we shouldn't care about special-casing __doc__ and _source.
@@ -583,7 +583,7 @@ class MagicalFields(NamedTuple): | |||
x: int | |||
def __slots__(self) -> None: pass # E: Cannot overwrite NamedTuple attribute "__slots__" | |||
def __new__(cls) -> None: pass # E: Cannot overwrite NamedTuple attribute "__new__" | |||
def _source(self) -> int: pass # E: Cannot overwrite NamedTuple attribute "_source" | |||
def _source(self) -> int: pass # _source is ok |
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 this is an oversight in typing
. _source
should probably be prohibited. I will make a PR to typing
now.
I'll have another look when @ilevkivskyi is happy. |
def __new__(cls) -> None: pass # E: Cannot overwrite NamedTuple attribute "__new__" | ||
def _source(self) -> int: pass # E: Cannot overwrite NamedTuple attribute "_source" | ||
__annotations__ = {'x': float} # E: NamedTuple field name cannot start with an underscore: __annotations__ \ | ||
# E: Invalid statement in NamedTuple definition; expected "field_name: field_type" \ |
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.
While you at it, maybe you could update the error message to use "field_name: field_type [= default]"
?
I am almost happy now, I am only a bit worried about the trick with |
There's a pretty big merge conflict now, I'll try to resolve it tonight. |
Before merging I want to be sure this passes with our internal code bases and also the quick and incremental tests that Jukka is developing. |
Until this is merged, is there a way I can suppress this specific error so that I can use mypy on files with NamedTuples like this? |
You can put a |
It probably still won't recognize that the methods exist though. |
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.
Can you add a test case covering @classmethod
?
It crashes in master but works in this branch
from typing import *
class Bar(NamedTuple):
x: str
@classmethod
def new(cls, f: str) -> 'Bar':
return Bar(x=f)
Also I still get a type error: foo.py:6: error: Invalid statement in NamedTuple definition; expected "field_name: field_type [= default]"
It's probably worth also checking @property
and @staticmethod
Added tests for all those and fixed some issues it uncovered. I needed another hack to make the |
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.
Class level fields fail:
from typing import *
class Foo(NamedTuple):
x: int
y = 1 # Not one of the tuple fields but a class attribute
foo.py:4: error: Invalid statement in NamedTuple definition; expected "field_name: field_type [= default]"
Wow, I hadn't thought of doing that. It does seem to work at runtime. I'd prefer to leave it out of this PR though, since the PR has been going on for long enough as is and I see some issues with that pattern. I think the fact that it works at runtime is probably an accident; it's not tested in If you have a class variable, it becomes very awkward to annotate it. If I make
mypy will tell me that I need to specify a more precise type for the list. But if I do
would work, but that just seems really confusing, since now you get radically different behavior depending on whether you used a type comment or a PEP 526-style annotation. Maybe we can figure out an acceptable way to handle this problem, but it seems complex enough that it's better to separate it out. Let's keep this PR focused on methods (and docstrings, which were trivial to implement). |
That's entirely fair.
…On Fri, May 5, 2017, 10:59 AM Jelle Zijlstra ***@***.***> wrote:
Wow, I hadn't thought of doing that. It does seem to work at runtime. I'd
prefer to leave it out of this PR though, since the PR has been going on
for long enough as is and I see some issues with that pattern. I think the
fact that it works at runtime is probably an accident; it's not tested in
typing and wasn't mentioned as far as I remember in the discussions about
adding the feature there.
If you have a class variable, it becomes very awkward to annotate it. If I
make
class Foo(NamedTuple):
x = []
y: int = 1
mypy will tell me that I need to specify a more precise type for the list.
But if I do
class Foo(NamedTuple):
x: List['Foo'] = []
y: int = 1
x has become an instance field. I suppose
class Foo(NamedTuple):
x = [] # type: List[Foo]
y = 1
would work, but that just seems really confusing, since now you get
radically different behavior depending on whether you used a type comment
or a PEP 526-style annotation. Maybe we can figure out an acceptable way to
handle this problem, but it seems complex enough that it's better to
separate it out. Let's keep this PR focused on methods (and docstrings,
which were trivial to implement).
—
You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub
<#3081 (comment)>, or mute
the thread
<https://github.com/notifications/unsubscribe-auth/AAA5NBuWTJKgS6YEArSjM3asfD5Vwb2Qks5r2zk3gaJpZM4Mte4d>
.
|
It works at runtime since it is not easy to prohibit everything at runtime :-) |
Gah! I thought we had this feature already. What will it take to finish this? |
I think it is waiting for your review.
I am practically happy with this, the |
You need to merge it. :) As far as I'm aware, this code works and the only concern (brought up by you and Ivan above) is that the approach taken is hacky, since it has some strange manual manipulation of the symbol table. I don't know of an easy way to avoid the symtable manipulation, so my preference is to merge this as is and maybe later do a refactoring to make NamedTuples more like normal classes. |
Sorry about that! I'm trying to restore personal state that I've apparently lost. I will review this. |
Finally! Sorry again. |
This is the mypy implementation of python/typing#352. cc @ilevkivskyi who wrote the runtime implementation.
Also fixes #3075.
This was pretty simple to implement.