10000 Make overloads support classmethod and staticmethod by Michael0x2a · Pull Request #5224 · python/mypy · GitHub
[go: up one dir, main page]

Skip to content

Make overloads support classmethod and staticmethod #5224

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

Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Broaden class/static method checks to catch overloads
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.
  • Loading branch information
Michael0x2a committed Jun 15, 2018
commit f0a12e90e1a3da8273c29532acd8a05ba8af8f6d
10 changes: 4 additions & 6 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.)

isinstance(override.definition, FuncBase)):
if ((original.definition.is_static or original.definition.is_class) and
not (override.definition.is_static or override.definition.is_class)):
fail = True
Expand Down Expand Up @@ -3923,8 +3923,6 @@ def is_untyped_decorator(typ: Optional[Type]) -> bool:
def is_static(func: Union[FuncBase, Decorator]) -> bool:
if isinstance(func, Decorator):
return is_static(func.func)
elif isinstance(func, OverloadedFuncDef):
return any(is_static(item) for item in func.items)
elif isinstance(func, FuncItem):
elif isinstance(func, FuncBase):
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"?

3 changes: 2 additions & 1 deletion mypy/checkmember.py
Original file line number Diff line number Diff line change
Expand Up @@ -448,7 +448,8 @@ def analyze_class_attribute_access(itype: Instance,
return handle_partial_attribute_type(t, is_lvalue, msg, symnode)
if not is_method and (isinstance(t, TypeVarType) or get_type_vars(t)):
msg.fail(messages.GENERIC_INSTANCE_VAR_CLASS_ACCESS, context)
is_classmethod = is_decorated and cast(Decorator, node.node).func.is_class
is_classmethod = ((is_decorated and cast(Decorator, node.node).func.is_class)
or (isinstance(node.node, FuncBase) and node.node.is_class))
return add_class_tvars(t, itype, is_classmethod, builtin_type, original_type)
elif isinstance(node.node, Var):
not_ready_callback(name, context)
Expand Down
4 changes: 4 additions & 0 deletions mypy/strconv.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,10 @@ def visit_overloaded_func_def(self, o: 'mypy.nodes.OverloadedFuncDef') -> str:
a.insert(0, o.type)
if o.impl:
a.insert(0, o.impl)
if o.is_static:
a.insert(-1, 'Static')
if o.is_class:
a.insert(-1, 'Class')
return self.dump(a, o)

def visit_class_def(self, o: 'mypy.nodes.ClassDef') -> str:
Expand Down
3 changes: 3 additions & 0 deletions mypy/treetransform.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,9 @@ def visit_overloaded_func_def(self, node: OverloadedFuncDef) -> OverloadedFuncDe
new._fullname = node._fullname
new.type = self.optional_type(node.type)
new.info = node.info
new.is_static = node.is_static
new.is_class = node.is_class
new.is_property = node.is_property
if node.impl:
new.impl = cast(OverloadPart, node.impl.accept(self))
return new
Expand Down
319 changes: 319 additions & 0 deletions test-data/unit/check-overloading.test
Original file line number Diff line number Diff line change
Expand Up @@ -2937,3 +2937,322 @@ def add_proxy(x, y):
tup = (1, '2')
reveal_type(foo(lambda (x, y): add_proxy(x, y), tup)) # E: Revealed type is 'builtins.str*'
[builtins fixtures/primitives.pyi]

[case testOverloadWithClassMethods]
from typing import overload

class Wrapper:
@overload
@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])?

def foo(cls, x: str) -> str: ...
@classmethod
def foo(cls, x): pass

reveal_type(Wrapper.foo(3)) # E: Revealed type is 'builtins.int'
reveal_type(Wrapper.foo("foo")) # E: Revealed type is 'builtins.str'

[builtins fixtures/classmethod.pyi]

[case testOverloadWithInconsistentClassMethods]
from typing import overload

class Wrapper1:
@overload # E: Overload does not consistently use the "@classmethod" decorator on all function signatures.
@classmethod
def foo(cls, x: int) -> int: ...
@overload
@classmethod
def foo(cls, x: str) -> str: ...
def foo(cls, x): pass

class Wrapper2:
@overload # E: Overload does not consistently use the "@classmethod" decorator on all function signatures.
@classmethod
def foo(cls, x: int) -> int: ...
@overload
def foo(cls, x: str) -> str: ...
@classmethod
def foo(cls, x): pass

class Wrapper3:
@overload # E: Overload does not consistently use the "@classmethod" decorator on all function signatures.
def foo(cls, x: int) -> int: ...
@overload
def foo(cls, x: str) -> str: ...
@classmethod
def foo(cls, x): pass

[builtins fixtures/classmethod.pyi]

[case testOverloadWithSwappedDecorators]
from typing import overload

class Wrapper1:
@classmethod
@overload
def foo(cls, x: int) -> int: ...

@classmethod
@overload
def foo(cls, x: str) -> str: ...

@classmethod
def foo(cls, x): pass

class Wrapper2:
@classmethod
@overload
def foo(cls, x: int) -> int: ...

@overload
@classmethod
def foo(cls, x: str) -> str: ...

@classmethod
def foo(cls, x): pass

class Wrapper3:
@classmethod # E: Overload does not consistently use the "@classmethod" decorator on all function signatures.
@overload
def foo(cls, x: int) -> int: ...

@overload
def foo(cls, x: str) -> str: ...

def foo(cls, x): pass

reveal_type(Wrapper1.foo(3)) # E: Revealed type is 'builtins.int'
reveal_type(Wrapper2.foo(3)) # E: Revealed type is 'builtins.int'

[builtins fixtures/classmethod.pyi]

[case testOverloadFaultyClassMethodInheritance]
from typing import overload

class A: pass
class B(A): pass
class C(B): pass

class Parent:
@overload
@classmethod
def foo(cls, x: B) -> int: ...

@overload
@classmethod
def foo(cls, x: str) -> str: ...

@classmethod
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.

@classmethod
def foo(cls, x: C) -> int: ...

@overload
@classmethod
def foo(cls, x: str) -> str: ...

@classmethod
def foo(cls, x): pass

class GoodChild(Parent):
@overload
@classmethod
def foo(cls, x: A) -> int: ...

@overload
@classmethod
def foo(cls, x: str) -> str: ...

@classmethod
def foo(cls, x): pass

[builtins fixtures/classmethod.pyi]

[case testOverloadClassMethodImplementation]
from typing import overload, Union

class Wrapper:
@classmethod
def other(cls) -> str:
return "..."

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

@overload
@classmethod
def foo(cls, x: str) -> str: ...

@classmethod # E: Overloaded function implementation cannot produce return type of signature 1
def foo(cls, x: Union[int, str]) -> str:
reveal_type(cls) # E: Revealed type is 'def () -> __main__.Wrapper'
reveal_type(cls.other()) # E: Revealed type is 'builtins.str'
return "..."

[builtins fixtures/classmethod.pyi]

[case testOverloadWithStaticMethods]
from typing import overload

class Wrapper:
@overload
@staticmethod
def foo(x: int) -> int: ...
@overload
@staticmethod
def foo(x: str) -> str: ...
@staticmethod
def foo(x): pass

reveal_type(Wrapper.foo(3)) # E: Revealed type is 'builtins.int'
reveal_type(Wrapper.foo("foo")) # E: Revealed type is 'builtins.str'

[builtins fixtures/staticmethod.pyi]

[case testOverloadWithInconsistentStaticMethods]
from typing import overload, Union

class Wrapper1:
@overload # E: Overload does not consistently use the "@staticmethod" decorator on all function signatures.
@staticmethod
def foo(x: int) -> int: ...
@overload
@staticmethod
def foo(x: str) -> str: ...
def foo(x): pass

class Wrapper2:
@overload # E: Overload does not consistently use the "@staticmethod" decorator on all function signatures.
@staticmethod
def foo(x: int) -> int: ...
@overload
def foo(x: str) -> str: ... # E: Self argument missing for a non-static method (or an invalid type for self)
@staticmethod
def foo(x): pass

class Wrapper3:
@overload # E: Overload does not consistently use the "@staticmethod" decorator on all function signatures.
@staticmethod
def foo(x: int) -> int: ...
@overload
@staticmethod
def foo(x: str) -> str: ...
def foo(x: Union[int, str]): pass # E: Self argument missing for a non-static method (or an invalid type for self)
[builtins fixtures/staticmethod.pyi]

[case testOverloadWithSwappedDecorators]
from typing import overload

class Wrapper1:
@staticmethod
@overload
def foo(x: int) -> int: ...

@staticmethod
@overload
def foo(x: str) -> str: ...

@staticmethod
def foo(x): pass

class Wrapper2:
@staticmethod
@overload
def foo(x: int) -> int: ...

@overload
@staticmethod
def foo(x: str) -> str: ...

@staticmethod
def foo(x): pass

class Wrapper3:
@staticmethod # E: Overload does not consistently use the "@staticmethod" decorator on all function signatures.
@overload
def foo(x: int) -> int: ...

@overload
def foo(x: str) -> str: ... # E: Self argument missing for a non-static method (or an invalid type for self)

@staticmethod
def foo(x): pass

reveal_type(Wrapper1.foo(3)) # E: Revealed type is 'builtins.int'
reveal_type(Wrapper2.foo(3)) # E: Revealed type is 'builtins.int'

[builtins fixtures/staticmethod.pyi]

[case testOverloadFaultyStaticMethodInheritance]
from typing import overload

class A: pass
class B(A): pass
class C(B): pass

class Parent:
@overload
@staticmethod
def foo(x: B) -> int: ...

@overload
@staticmethod
def foo(x: str) -> str: ...

@staticmethod
def foo(x): pass

class BadChild(Parent):
@overload # E: Signature of "foo" incompatible with supertype "Parent"
@staticmethod
def foo(x: C) -> int: ...

@overload
@staticmethod
def foo(x: str) -> str: ...

@staticmethod
def foo(x): pass

class GoodChild(Parent):
@overload
@staticmethod
def foo(x: A) -> int: ...

@overload
@staticmethod
def foo(x: str) -> str: ...

@staticmethod
def foo(x): pass

[builtins fixtures/staticmethod.pyi]

[case testOverloadStaticMethodImplementation]
from typing import overload, Union

class Wrapper:
@staticmethod
def other() -> str:
return "..."

@overload
@staticmethod
def foo(x: int) -> int: ...

@overload
@staticmethod
def foo(x: str) -> str: ...

@staticmethod # E: Overloaded function implementation cannot produce return type of signature 1
def foo(x: Union[int, str]) -> str:
return 3 # E: Incompatible return value type (got "int", expected "str")

[builtins fixtures/staticmethod.pyi]
1 change: 1 addition & 0 deletions test-data/unit/fixtures/staticmethod.pyi
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ class int:
class str: pass
class unicode: pass
class bytes: pass
class ellipsis: pass
0