-
-
Notifications
You must be signed in to change notification settings - Fork 2.9k
Support for declared_type
, to give types to decorated functions
#3291
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
Changes from all commits
4e81716
f15779a
6af0979
c739cef
f0f30f4
a5ebef7
7bba610
5b927ac
19a0964
8c803da
1c8b1a9
4e3c442
145e386
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2266,8 +2266,14 @@ def visit_decorator(self, e: Decorator) -> None: | |
[nodes.ARG_POS], e, | ||
callable_name=fullname) | ||
sig = cast(FunctionLike, sig) | ||
sig = set_callable_name(sig, e.func) | ||
e.var.type = sig | ||
if e.var.type is not None: | ||
# We have a declared type, check it. | ||
self.check_subtype(sig, e.var.type, e, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @JukkaL Shouldn't this be other way around? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think that this is right -- this is not a cast but a declaration. But it would be good to have a test case where the ordering of this check is important. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The problem with this is that it will be then impossible to give a more precise type for a decorated function when it is known. From the discussion on python/peps tracker I somehow understood that this should work like a cast. I however could imagine situations where one might want to declare narrower argument types, so maybe this check is not needed and we can allow both ways? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This can be used to declare a more precise type with respect to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OK, I see, I was thinking about hijacking this decorator for #2087 |
||
subtype_label="inferred decorated type", | ||
supertype_label="declared decorated type") | ||
else: | ||
e.var.type = sig | ||
e.var.type = set_callable_name(e.var.type, e.func) | ||
e.var.is_ready = True | ||
if e.func.is_property: | ||
self.check_incompatible_property_override(e) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2633,6 +2633,19 @@ def visit_decorator(self, dec: Decorator) -> None: | |
elif refers_to_fullname(d, 'typing.no_type_check'): | ||
dec.var.type = AnyType() | ||
no_type_check = True | ||
elif isinstance(d, CallExpr) and ( | ||
refers_to_fullname(d.callee, 'typing.declared_type') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As discussed above, maybe also accept |
||
or refers_to_fullname(d.callee, 'mypy_extensions.declared_type')): | ||
removed.append(i) | ||
if i != 0: | ||
self.fail('"declared_type" must be the topmost decorator', d) | ||
elif len(d.args) != 1: | ||
self.fail('"declared_type" takes exactly one argument', d) | ||
else: | ||
dec.var.type = self.expr_to_analyzed_type(d.args[0]) | ||
elif (refers_to_fullname(d, 'typing.declared_type') or | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same as above. |
||
refers_to_fullname(d, 'mypy_extensions.declared_type')): | ||
self.fail('"declared_type" must have a type as an argument', d) | ||
for i in reversed(removed): | ||
del dec.decorators[i] | ||
if not dec.is_overload or dec.var.is_property: | ||
|
@@ -3838,6 +3851,13 @@ def visit_decorator(self, dec: Decorator) -> None: | |
engine just for decorators. | ||
""" | ||
super().visit_decorator(dec) | ||
if dec.var.type is not None: | ||
# We already have a declared type for this decorated thing. | ||
return | ||
if dec.func.is_awaitable_coroutine: | ||
# The type here will be fixed up by checker.py, but we can't infer | ||
# anything here. | ||
return | ||
if dec.var.is_property: | ||
# Decorators are expected to have a callable type (it's a little odd). | ||
if dec.func.type is None: | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -734,6 +734,188 @@ a = None # type: A | |
a.f() | ||
a.f(None) # E: Too many arguments for "f" of "A" | ||
|
||
[case testMethodWithDeclaredDecoratedType] | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Style nit: Redundant empty line (and similar cases in other test cases). |
||
from typing import Callable, Any | ||
from mypy_extensions import declared_type | ||
|
||
def dec(f): pass | ||
|
||
# Note that the decorated type must account for the `self` argument -- It's applied pre-binding | ||
class Foo: | ||
@declared_type(Callable[[Any, int], str]) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Test similar case where you use a non- |
||
@dec | ||
def f(self): pass | ||
|
||
foo = Foo() | ||
|
||
foo.f("a") # E: Argument 1 to "f" of "Foo" has incompatible type "str"; expected "int" | ||
x: str = foo.f(1) | ||
y: int = foo.f(1) # E: Incompatible types in assignment (expression has type "str", variable has type "int") | ||
|
||
reveal_type(foo.f) # E: Revealed type is 'def (builtins.int) -> builtins.str' | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Another context where we don't tend to have empty lines. |
||
[builtins fixtures/dict.pyi] | ||
|
||
[case testClassMethodWithDeclaredDecoratedType] | ||
|
||
from typing import Callable, Any | ||
from mypy_extensions import declared_type | ||
|
||
def dec(f): pass | ||
|
||
# Note that the decorated type must account for the `cls` argument -- It's applied pre-binding | ||
class Foo: | ||
@declared_type(Callable[[Any, int], str]) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Test similar case where you use |
||
@classmethod | ||
@dec | ||
def f(cls): pass | ||
|
||
|
||
Foo.f("a") # E: Argument 1 to "f" of "Foo" has incompatible type "str"; expected "int" | ||
x: str = Foo.f(1) | ||
y: int = Foo.f(1) # E: Incompatible types in assignment (expression has type "str", variable has type "int") | ||
|
||
reveal_type(Foo.f) # E: Revealed type is 'def (builtins.int) -> builtins.str' | ||
|
||
[builtins fixtures/dict.pyi] | ||
|
||
[case testStaticMethodWithDeclaredDecoratedType] | ||
|
||
from typing import Callable | ||
from mypy_extensions import declared_type | ||
|
||
def dec(f): pass | ||
|
||
class Foo: | ||
@declared_type(Callable[[int], str]) | ||
@staticmethod | ||
@dec | ||
def f(): pass | ||
|
||
|
||
Foo.f("a") # E: Argument 1 to "f" of "Foo" has incompatible type "str"; expected "int" | ||
x: str = Foo.f(1) | ||
y: int = Foo.f(1) # E: Incompatible types in assignment (expression has type "str", variable has type "int") | ||
|
||
reveal_type(Foo.f) # E: Revealed type is 'def (builtins.int) -> builtins.str' | ||
|
||
[builtins fixtures/dict.pyi] | ||
|
||
[case testUntypedDecoratorWithDeclaredType] | ||
|
||
from typing import Callable | ||
from mypy_extensions import declared_type | ||
|
||
def dec(f): pass | ||
|
||
@declared_type(Callable[[int], str]) | ||
@dec | ||
def f(): pass | ||
|
||
f("a") # E: Argument 1 to "f" has incompatible type "str"; expected "int" | ||
x: str = f(1) | ||
y: int = f(1) # E: Incompatible types in assignment (expression has type "str", variable has type "int") | ||
|
||
reveal_type(f) # E: Revealed type is 'def (builtins.int) -> builtins.str' | ||
|
||
[builtins fixtures/dict.pyi] | ||
|
||
[case testDecoratorWithDeclaredTypeBare] | ||
|
||
from typing import Callable | ||
from mypy_extensions import declared_type | ||
|
||
@declared_type(Callable[[int], str]) | ||
def f(x): pass | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe reveal type of |
||
|
||
f("a") # E: Argument 1 to "f" has incompatible type "str"; expected "int" | ||
x: str = f(1) | ||
y: int = f(1) # E: Incompatible types in assignment (expression has type "str", variable has type "int") | ||
reveal_type(f) # E: Revealed type is 'def (builtins.int) -> builtins.str' | ||
|
||
[builtins fixtures/dict.pyi] | ||
|
||
[case testDecoratorWithDeclaredTypeIncompatible] | ||
|
||
from typing import Callable, TypeVar | ||
from mypy_extensions import declared_type | ||
|
||
T = TypeVar('T') | ||
|
||
def dec(f: T) -> T: pass | ||
|
||
@declared_type(Callable[[int], str]) # E: Incompatible types (inferred decorated type Callable[[str], str], declared decorated type Callable[[int], str]) | ||
@dec | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also test |
||
def f(x: str) -> str: pass | ||
|
||
[builtins fixtures/dict.pyi] | ||
|
||
[case testDecoratorWithDeclaredTypeCompatible] | ||
|
||
from typing import Callable, TypeVar | ||
from mypy_extensions import declared_type | ||
|
||
T = TypeVar('T') | ||
|
||
def dec(f: T) -> T: pass | ||
|
||
class A: pass | ||
class B(A): pass | ||
|
||
@declared_type(Callable[[B], str]) | ||
@dec | ||
def f(x: A) -> str: pass | ||
|
||
reveal_type(f) # E: Revealed type is 'def (__main__.B) -> builtins.str' | ||
[builtins fixtures/dict.pyi] | ||
|
||
[case testDecoratorWithDeclaredTypeNotTop] | ||
|
||
from typing import Callable | ||
from mypy_extensions import declared_type | ||
|
||
def dec(f): pass | ||
|
||
@dec | ||
@declared_type(Callable[[int], str]) # E: "declared_type" must be the topmost decorator | ||
def f(): pass | ||
|
||
reveal_type(f) # E: Revealed type is 'Any' | ||
|
||
[builtins fixtures/dict.pyi] | ||
|
||
[case testDecoratorWithDeclaredTypeNoArgs] | ||
|
||
from typing import Callable | ||
from mypy_extensions import declared_type | ||
|
||
def dec(f): pass | ||
|
||
@declared_type() # E: "declared_type" takes exactly one argument | ||
@dec | ||
def f(): pass | ||
|
||
reveal_type(f) # E: Revealed type is 'Any' | ||
|
||
[builtins fixtures/dict.pyi] | ||
|
||
[case testDecoratorWithDeclaredTypeNoCall] | ||
|
||
from typing import Callable | ||
from mypy_extensions import declared_type | ||
|
||
def dec(f): pass | ||
|
||
@declared_type # E: "declared_type" must have a type as an argument | ||
@dec | ||
def f(): pass | ||
|
||
# NB: The revealed type below is technically correct, as weird as it looks | ||
reveal_type(f) # E: Revealed type is 'def [T] (T`-1) -> T`-1' | ||
|
||
[builtins fixtures/dict.pyi] | ||
|
||
[case testNestedDecorators] | ||
from typing import Any, Callable | ||
def dec1(f: Callable[[Any], None]) -> Callable[[], None]: pass | ||
|
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 wonder if there is a context where it's useful to use this to decorate a class?