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

Make overloads support classmethod and staticmethod #5224

Conversation

Michael0x2a
Copy link
Collaborator

This pull request adds support for mixing classmethods, staticmethods, and overloads.

It does so by adding in some extra logic into the semantic analysis phase to detect when an overload is using classmethods or staticmethods and adds that information into OverloadedFuncDef. This allowed me to add is_class and is_static fields to FuncBase, the common base class for overloads and other function-related things.

This PR does not attempt to handle overloads + other arbitrary decorators.

Resolves #328 (which unblocks #2254?)

This commit moves the `is_class` and `is_static` fields into FuncBase.
It also cleans up the list of flags so they don't repeat the
'is_property' entry, which is now present in `FUNCBASE_FLAGS`.

The high-level plan is to modify the `is_class` and `is_static` fields
in OverloadedFuncDef for use later in mypy.
This commit adjusts the semantic analysis phase to detect and record
when an overload appears to be a classmethod or staticmethod.
This commit modifies mypy to use the `is_static` and `is_class` fields
of OverloadedFuncDef as appropriate.

I found the code snippets to modify by asking PyCharm for all instances
of code using those two fields and modified the surrounding code as
appropriate.
Both the attrs and dataclasses plugins manually patch classmethods -- we
do the same for overloads.
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.

Thanks! Here are few comments.

@@ -481,9 +490,9 @@ def set_line(self, target: Union[Context, int], column: Optional[int] = None) ->
self.variable.set_line(self.line, self.column)


FUNCITEM_FLAGS = [
FUNCITEM_FLAGS = FUNCBASE_FLAGS + [
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change adds is_property, is this intentional?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is. I made this change partly on principle since is_property actually is a field of FuncBase -- it felt cleaner to just force all subclasses to preserve that field no matter what.

This change also doesn't actually change the serialized output in practice. FuncItem currently has only two subtypes: FuncDef and LambdaExpr. The former subclass previously explicitly set and serialized the is_property field so this change makes no difference there. The latter subclass never really uses is_property but also doesn't have any serialize/deserialize methods, which makes this change moot.

if stmt.impl is not None:
assert isinstance(stmt.impl, Decorator)
if isinstance(stmt.impl.func.type, CallableType):
stmt.impl.func.type.arg_types[0] = class_type
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's good that you also take care about plugins.

@@ -370,13 +370,20 @@ def __str__(self) -> str:
return 'ImportedName(%s)' % self.target_fullname


FUNCBASE_FLAGS = [
'is_property', 'is_class', 'is_static',
]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we also update astdiff.py according to these flag reshuffling? This may break fine grained incremental mode (in some corner cases).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ooh, good point -- I didn't even know that file existed.

I tried making the change + tried adding an test to one of the fine-grained incremental tests. (I'm pretty unfamiliar with fine-grained incremental stuff though, so let me know if I did it incorrectly.)

mypy/semanal.py Outdated
elif isinstance(item, FuncDef):
inner = item
else:
raise AssertionError()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a message to all assertion errors. Typically we write assert False, "Impossible blah-blah-blah".

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

@attr.s
class A:
a: int
b: str
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 rather make these attr.ibs, to check that the cls signature below is generated correctly by the plugin.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also done

def foo(cls, x): pass

class BadChild(Parent):
@overload # E: Signature of "foo" incompatible with supertype "Parent"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe add few more tests to check overriding an overloaded instance method, with overloaded class methods and vice versa?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah whoops, I completely forgot to handle this case. Fixed.

mypy/checker.py Outdated
@@ -1290,8 +1290,8 @@ def check_override(self, override: FunctionLike, original: FunctionLike,
fail = True

if isinstance(original, CallableType) and isinstance(override, CallableType):
if (isinstance(original.definition, FuncItem) and
isinstance(override.definition, FuncItem)):
if (isinstance(original.definition, FuncBase) and
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that this PR (in particular because of this change) may have conflicts with my type aliases refactoring. In turned out that the type_override (that I killed) was also used for some overload hacks.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I'm completely following -- is there now going to be some major difference between FuncItem and FuncBase?

(I also ended up streamlining this code to handle the "overriding a classmethod with an instance method" case more cleanly -- not sure if that change makes the potential conflict better or worse.)

@ilevkivskyi ilevkivskyi self-assigned this Jun 15, 2018
This commit:

1. Updates astdiff.py and adds a case to one of the fine-grained
   dependency test files.

2. Adds some helper methods to FunctionLike.

3. Performs a few misc cleanups.
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.

Thanks! Here are few more comments. I just checked locally your changes work well with my refactoring.

mypy/checker.py Outdated
return func.is_static
return False
raise AssertionError("Unexpected func type: {}".format(type(func)))
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 please reformat this as assert False, "message"?

mypy/nodes.py Outdated
class FuncBase(Node):
"""Abstract base class for function-like nodes"""

__slots__ = ('type',
'unanalyzed_type',
'info',
'is_property',
'is_class',
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 please add a comment # Uses @classmethod? as before (and probably also for is_static) below. Currently we have is_class, is_classmethod, and is_classmethod_class (the latter I would say is quite bad name), and I want to avoid potential confusions.

mypy/semanal.py Outdated
elif isinstance(defn.impl, FuncDef):
inner = defn.impl
else:
raise AssertionError()
Copy link
Member

@ilevkivskyi ilevkivskyi Jun 16, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you also please add an assertion message here, ad reformat like above via assert False?

@@ -173,7 +173,7 @@ def snapshot_definition(node: Optional[SymbolNode],
signature = snapshot_type(node.type)
else:
signature = snapshot_untyped_signature(node)
return ('Func', common, node.is_property, signature)
return ('Func', common, node.is_property, node.is_class, node.is_property, signature)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is_property now appears twice in the list, did you mean is_static? Also maybe the isinstance above can use FuncBase?

@classmethod
def foo(cls, x: int) -> int: ...
@overload
@classmethod
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One more suggestion: could you please add to your TODO list adding few tests to check that overloads work well with self-types (both instance, and class methods using cls: Type[T])?

main:3: error: Revealed type is 'builtins.int'
==
main:3: error: Revealed type is 'Any'
main:3: error: No overload variant of "foo" of "Wrapper" matches argument type "int"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this test looks good.

@Michael0x2a
Copy link
Collaborator Author

@ilevkivskyi -- ok, done. I went ahead and just added some self-type tests to this commit.

@ilevkivskyi ilevkivskyi merged commit 29889c8 into python:master Jun 16, 2018
@Michael0x2a Michael0x2a deleted the make-overloads-support-classmethod-and-staticmethod branch July 9, 2018 04:18
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.

2 participants