Skip to content
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

Merged
merged 14 commits into from
May 16, 2017
Merged

Conversation

JelleZijlstra
Copy link
Member

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.

@JelleZijlstra JelleZijlstra changed the title Implement NamedTuple methods Add support for NamedTuple methods Mar 29, 2017
Copy link
Member

@gvanrossum gvanrossum left a 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)
Copy link
Member

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.

Copy link
Member Author

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
Copy link
Member

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
Copy link
Member

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
Copy link
Member

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?

Copy link
Member Author

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.

Copy link
Member

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__"
Copy link
Member

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__',
Copy link
Member

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.

Copy link
Member Author

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
Copy link
Member

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).

@gvanrossum
Copy link
Member

PS. Would also like to see @ilevkivskyi's review.

@JelleZijlstra
Copy link
Member Author

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.

@gvanrossum
Copy link
Member

I realized we still disallow docstrings in namedtuples. Do you mind if I fix that in this PR too?

Yes please.

Copy link
Member

@ilevkivskyi ilevkivskyi left a 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)
Copy link
Member

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__

Copy link
Member Author

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.

Copy link
Member Author

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.

Copy link
Member Author

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.

Copy link
Member

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:
Copy link
Member

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: overloaded methods.


class Child(Base):
def new_method(self) -> int:
return self.x
Copy link
Member

@ilevkivskyi ilevkivskyi Mar 29, 2017

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)
Copy link
Member

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.

Copy link
Member Author

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?

Copy link
Member

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.

Copy link
Member

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.

Copy link
Member Author

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
Copy link
Member

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.

@gvanrossum
Copy link
Member

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" \
Copy link
Member

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]"?

@ilevkivskyi
Copy link
Member

I'll have another look when @ilevkivskyi is happy.

I am almost happy now, I am only a bit worried about the trick with SymbolTable().

@JelleZijlstra
Copy link
Member Author

There's a pretty big merge conflict now, I'll try to resolve it tonight.

@gvanrossum
Copy link
Member

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.

@Daenyth
Copy link

Daenyth commented May 3, 2017

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?

@gvanrossum
Copy link
Member

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 # type: ignore comment on the line where the unwanted error appears.

@JelleZijlstra
Copy link
Member Author

It probably still won't recognize that the methods exist though.

Copy link

@Daenyth Daenyth left a 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

@JelleZijlstra
Copy link
Member Author

Added tests for all those and fixed some issues it uncovered. I needed another hack to make the cls argument to classmethods work.

Copy link

@Daenyth Daenyth left a 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]"

@JelleZijlstra
Copy link
Member Author

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).

@Daenyth
Copy link

Daenyth commented May 5, 2017 via email

@ilevkivskyi
Copy link
Member

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

It works at runtime since it is not easy to prohibit everything at runtime :-)
But seriously, I don't think we need this. Named tuples are intended to be "minimalistic" classes, and possible syntactic solutions for class variables in named tuples seem confusing.

@gvanrossum
Copy link
Member

Gah! I thought we had this feature already. What will it take to finish this?

@ilevkivskyi
Copy link
Member

Gah! I thought we had this feature already. What will it take to finish this?

I think it is waiting for your review.

I'll have another look when @ilevkivskyi is happy.

I am practically happy with this, the SymbolTable() trick looks not too aesthetic to me, but this is probably just a matter of taste.

@JelleZijlstra
Copy link
Member Author

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.

@gvanrossum
Copy link
Member

Sorry about that! I'm trying to restore personal state that I've apparently lost. I will review this.

@gvanrossum gvanrossum merged commit 3589c08 into python:master May 16, 2017
@gvanrossum
Copy link
Member

Finally! Sorry again.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Error reported when '__annotations__' is accessed
4 participants