Skip to content
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
21 changes: 18 additions & 3 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -703,6 +703,12 @@ def _visit_overloaded_func_def(self, defn: OverloadedFuncDef) -> None:
# TODO: keep precise type for callables with tricky but valid signatures.
setter_type = fallback_setter_type
defn.items[0].var.setter_type = setter_type
if isinstance(defn.type, Overloaded):
# Update legacy property type for decorated properties.
getter_type = self.extract_callable_type(defn.items[0].var.type, defn)
if getter_type is not None:
getter_type.definition = defn.items[0]
defn.type.items[0] = getter_type
for i, fdef in enumerate(defn.items):
assert isinstance(fdef, Decorator)
if defn.is_property:
Expand Down Expand Up @@ -730,7 +736,7 @@ def _visit_overloaded_func_def(self, defn: OverloadedFuncDef) -> None:
assert isinstance(item, Decorator)
item_type = self.extract_callable_type(item.var.type, item)
if item_type is not None:
item_type.definition = item.func
item_type.definition = item
item_types.append(item_type)
if item_types:
defn.type = Overloaded(item_types)
Expand Down Expand Up @@ -3509,6 +3515,7 @@ def check_compatibility_all_supers(self, lvalue: RefExpr, rvalue: Expression) ->
continue

base_type, base_node = self.node_type_from_base(lvalue_node.name, base, lvalue)
# TODO: if the r.h.s. is a descriptor, we should check setter override as well.
custom_setter = is_custom_settable_property(base_node)
if isinstance(base_type, PartialType):
base_type = None
Expand Down Expand Up @@ -4494,6 +4501,8 @@ def set_inferred_type(self, var: Var, lvalue: Lvalue, type: Type) -> None:
if isinstance(p_type, Overloaded):
# TODO: in theory we can have a property with a deleter only.
var.is_settable_property = True
assert isinstance(definition, Decorator)
var.setter_type = definition.var.setter_type

def set_inference_error_fallback_type(self, var: Var, lvalue: Lvalue, type: Type) -> None:
"""Store best known type for variable if type inference failed.
Expand Down Expand Up @@ -5356,6 +5365,8 @@ def visit_decorator_inner(
self.check_untyped_after_decorator(sig, e.func)
self.require_correct_self_argument(sig, e.func)
sig = set_callable_name(sig, e.func)
if isinstance(sig, CallableType):
sig.definition = e
e.var.type = sig
e.var.is_ready = True
if e.func.is_property:
Expand Down Expand Up @@ -8651,17 +8662,21 @@ def visit_type_alias_type(self, t: TypeAliasType) -> Type:
return t.copy_modified(args=[a.accept(self) for a in t.args])


def is_classmethod_node(node: Node | None) -> bool | None:
def is_classmethod_node(node: SymbolNode | None) -> bool | None:
"""Find out if a node describes a classmethod."""
if isinstance(node, Decorator):
node = node.func
if isinstance(node, FuncDef):
return node.is_class
if isinstance(node, Var):
return node.is_classmethod
return None


def is_node_static(node: Node | None) -> bool | None:
def is_node_static(node: SymbolNode | None) -> bool | None:
"""Find out if a node describes a static function method."""
if isinstance(node, Decorator):
node = node.func
if isinstance(node, FuncDef):
return node.is_static
if isinstance(node, Var):
Expand Down
9 changes: 8 additions & 1 deletion mypy/checkmember.py
Original file line number Diff line number Diff line change
Expand Up @@ -976,7 +976,14 @@ def expand_and_bind_callable(
freeze_all_type_vars(expanded)
if not var.is_property:
return expanded
# TODO: a decorated property can result in Overloaded here.
if isinstance(expanded, Overloaded):
# Legacy way to store settable properties is with overloads. Also in case it is
# an actual overloaded property, selecting first item that passed check_self_arg()
# is a good approximation, long-term we should use check_call() inference below.
if not expanded.items:
# A broken overload, error should be already reported.
return AnyType(TypeOfAny.from_error)
expanded = expanded.items[0]
assert isinstance(expanded, CallableType)
if var.is_settable_property and mx.is_lvalue and var.setter_type is not None:
if expanded.variables:
Expand Down
8 changes: 4 additions & 4 deletions mypy/fixup.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,8 +181,7 @@ def visit_overloaded_func_def(self, o: OverloadedFuncDef) -> None:
if isinstance(o.type, Overloaded):
# For error messages we link the original definition for each item.
for typ, item in zip(o.type.items, o.items):
if isinstance(item, Decorator):
typ.definition = item.func
typ.definition = item

def visit_decorator(self, d: Decorator) -> None:
if self.current_info is not None:
Expand All @@ -193,8 +192,9 @@ def visit_decorator(self, d: Decorator) -> None:
d.var.accept(self)
for node in d.decorators:
node.accept(self)
if isinstance(d.var.type, ProperType) and isinstance(d.var.type, CallableType):
d.var.type.definition = d.func
typ = d.var.type
if isinstance(typ, ProperType) and isinstance(typ, CallableType):
typ.definition = d.func

def visit_class_def(self, c: ClassDef) -> None:
for v in c.type_vars:
Expand Down
28 changes: 19 additions & 9 deletions mypy/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
CallExpr,
ClassDef,
Context,
Decorator,
Expression,
FuncDef,
IndexExpr,
Expand Down Expand Up @@ -2938,10 +2939,13 @@ def format_single(arg: Type) -> str:

def pretty_class_or_static_decorator(tp: CallableType) -> str | None:
"""Return @classmethod or @staticmethod, if any, for the given callable type."""
if tp.definition is not None and isinstance(tp.definition, SYMBOL_FUNCBASE_TYPES):
if tp.definition.is_class:
definition = tp.definition
if isinstance(definition, Decorator):
definition = definition.func
if definition is not None and isinstance(definition, SYMBOL_FUNCBASE_TYPES):
if definition.is_class:
return "@classmethod"
if tp.definition.is_static:
if definition.is_static:
return "@staticmethod"
return None

Expand Down Expand Up @@ -2991,12 +2995,15 @@ def [T <: int] f(self, x: int, y: T) -> None
slash = True

# If we got a "special arg" (i.e: self, cls, etc...), prepend it to the arg list
definition = tp.definition
if isinstance(definition, Decorator):
definition = definition.func
if (
isinstance(tp.definition, FuncDef)
and hasattr(tp.definition, "arguments")
isinstance(definition, FuncDef)
and hasattr(definition, "arguments")
and not tp.from_concatenate
):
definition_arg_names = [arg.variable.name for arg in tp.definition.arguments]
definition_arg_names = [arg.variable.name for arg in definition.arguments]
if (
len(definition_arg_names) > len(tp.arg_names)
and definition_arg_names[0]
Expand All @@ -3005,7 +3012,7 @@ def [T <: int] f(self, x: int, y: T) -> None
if s:
s = ", " + s
s = definition_arg_names[0] + s
s = f"{tp.definition.name}({s})"
s = f"{definition.name}({s})"
elif tp.name:
first_arg = get_first_arg(tp)
if first_arg:
Expand Down Expand Up @@ -3051,9 +3058,12 @@ def [T <: int] f(self, x: int, y: T) -> None


def get_first_arg(tp: CallableType) -> str | None:
if not isinstance(tp.definition, FuncDef) or not tp.definition.info or tp.definition.is_static:
definition = tp.definition
if isinstance(definition, Decorator):
definition = definition.func
if not isinstance(definition, FuncDef) or not definition.info or definition.is_static:
return None
return tp.definition.original_first_arg
return definition.original_first_arg


def variance_string(variance: int) -> str:
Expand Down
2 changes: 2 additions & 0 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -1243,6 +1243,7 @@ def analyze_overloaded_func_def(self, defn: OverloadedFuncDef) -> None:
bare_setter_type = self.analyze_property_with_multi_part_definition(defn)
typ = function_type(first_item.func, self.named_type("builtins.function"))
assert isinstance(typ, CallableType)
typ.definition = first_item
types = [typ]
else:
# This is a normal overload. Find the item signatures, the
Expand Down Expand Up @@ -1374,6 +1375,7 @@ def analyze_overload_sigs_and_impl(
if isinstance(item, Decorator):
callable = function_type(item.func, self.named_type("builtins.function"))
assert isinstance(callable, CallableType)
callable.definition = item
if not any(refers_to_fullname(dec, OVERLOAD_NAMES) for dec in item.decorators):
if i == len(defn.items) - 1 and not self.is_stub_file:
# Last item outside a stub is impl
Expand Down
3 changes: 3 additions & 0 deletions mypy/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -1887,6 +1887,9 @@ def __init__(
self.fallback = fallback
assert not name or "<bound method" not in name
self.name = name
# The rules for what exactly is considered a definition:
# * If it is a non-decorated function, FuncDef is the definition
# * If it is a decorated function, enclosing Decorator is the definition
self.definition = definition
self.variables = variables
self.is_ellipsis_args = is_ellipsis_args
Expand Down
30 changes: 30 additions & 0 deletions test-data/unit/check-classes.test
Original file line number Diff line number Diff line change
Expand Up @@ -9211,3 +9211,33 @@ class CGT(BG[T]): ...

reveal_type(CGI) # N: Revealed type is "def () -> __main__.CGI"
reveal_type(CGT) # N: Revealed type is "def [T] () -> __main__.CGT[T`1]"

[case testSettablePropertyAlias]
from typing import Any, TypeVar

class A:
@property
def prop(self: Any) -> str: ...
@prop.setter
def prop(self, val: str) -> None: ...

T = TypeVar("T")
class AT:
@property
def prop(self: T) -> T: ...
@prop.setter
def prop(self: T, val: list[T]) -> None: ...

class B:
prop: str
prop_t: str

class C(B):
prop = A.prop
prop_t = AT.prop # E: Incompatible types in assignment (expression has type "C", base class "B" defined the type as "str")

reveal_type(C().prop) # N: Revealed type is "builtins.str"
C().prop = "no" # E: Invalid self argument "C" to attribute function "prop" with type "Callable[[A, str], None]"
reveal_type(C().prop_t) # N: Revealed type is "__main__.C"
C().prop_t = 1 # E: Incompatible types in assignment (expression has type "int", variable has type "list[C]")
[builtins fixtures/property.pyi]
2 changes: 1 addition & 1 deletion test-data/unit/check-dataclasses.test
Original file line number Diff line number Diff line change
Expand Up @@ -2055,7 +2055,7 @@ from dataclasses import dataclass, replace, InitVar
from typing import ClassVar

@dataclass
class A:
class A: # N: "replace" of "A" defined here
x: int
q: InitVar[int]
q2: InitVar[int] = 0
Expand Down
98 changes: 2 additions & 96 deletions test-data/unit/fine-grained.test
Original file line number Diff line number Diff line change
Expand Up @@ -7312,9 +7312,7 @@ class C:
==
mod.py:9: error: Incompatible types in assignment (expression has type "int", variable has type "str")

[case testOverloadedMethodSupertype-only_when_cache]
-- Different cache/no-cache tests because
-- CallableType.def_extras.first_arg differs ("self"/None)
[case testOverloadedMethodSupertype]
from typing import overload, Any
import b
class Child(b.Parent):
Expand Down Expand Up @@ -7355,49 +7353,6 @@ main:4: note: def f(self, arg: int) -> int
main:4: note: @overload
main:4: note: def f(self, arg: str) -> str

[case testOverloadedMethodSupertype2-only_when_nocache]
-- Different cache/no-cache tests because
-- CallableType.def_extras.first_arg differs ("self"/None)
from typing import overload, Any
import b
class Child(b.Parent):
@overload # Fail
def f(self, arg: int) -> int: ...
@overload
def f(self, arg: str) -> str: ...
def f(self, arg: Any) -> Any: ...
[file b.py]
from typing import overload, Any
class C: pass
class Parent:
@overload
def f(self, arg: int) -> int: ...
@overload
def f(self, arg: str) -> str: ...
def f(self, arg: Any) -> Any: ...
[file b.py.2]
from typing import overload, Any
class C: pass
class Parent:
@overload
def f(self, arg: int) -> int: ...
@overload
def f(self, arg: str) -> C: ...
def f(self, arg: Any) -> Any: ...
[out]
==
main:4: error: Signature of "f" incompatible with supertype "b.Parent"
main:4: note: Superclass:
main:4: note: @overload
main:4: note: def f(self, arg: int) -> int
main:4: note: @overload
main:4: note: def f(self, arg: str) -> C
main:4: note: Subclass:
main:4: note: @overload
main:4: note: def f(arg: int) -> int
main:4: note: @overload
main:4: note: def f(arg: str) -> str

[case testOverloadedInitSupertype]
import a
[file a.py]
Expand Down Expand Up @@ -8486,9 +8441,7 @@ class D:
==
a.py:3: error: Cannot override final attribute "meth" (previously declared in base class "C")

[case testFinalBodyReprocessedAndStillFinalOverloaded-only_when_cache]
-- Different cache/no-cache tests because
-- CallableType.def_extras.first_arg differs ("self"/None)
[case testFinalBodyReprocessedAndStillFinalOverloaded]
import a
[file a.py]
from c import C
Expand Down Expand Up @@ -8533,53 +8486,6 @@ a.py:3: note: def meth(self, x: str) -> str
a.py:3: note: Subclass:
a.py:3: note: def meth(self) -> None

[case testFinalBodyReprocessedAndStillFinalOverloaded2-only_when_nocache]
-- Different cache/no-cache tests because
-- CallableType.def_extras.first_arg differs ("self"/None)
import a
[file a.py]
from c import C
class A:
def meth(self) -> None: ...

[file a.py.3]
from c import C
class A(C):
def meth(self) -> None: ...

[file c.py]
from typing import final, overload, Union
from d import D

class C:
@overload
def meth(self, x: int) -> int: ...
@overload
def meth(self, x: str) -> str: ...
@final
def meth(self, x: Union[int, str]) -> Union[int, str]:
D(int())
return x
[file d.py]
class D:
def __init__(self, x: int) -> None: ...
[file d.py.2]
from typing import Optional
class D:
def __init__(self, x: Optional[int]) -> None: ...
[out]
==
==
a.py:3: error: Cannot override final attribute "meth" (previously declared in base class "C")
a.py:3: error: Signature of "meth" incompatible with supertype "c.C"
a.py:3: note: Superclass:
a.py:3: note: @overload
a.py:3: note: def meth(x: int) -> int
a.py:3: note: @overload
a.py:3: note: def meth(x: str) -> str
a.py:3: note: Subclass:
a.py:3: note: def meth(self) -> None

[case testIfMypyUnreachableClass]
from a import x

Expand Down
1 change: 1 addition & 0 deletions test-data/unit/pythoneval.test
Original file line number Diff line number Diff line change
Expand Up @@ -1970,6 +1970,7 @@ a2 = replace()
a2 = replace(a, x='spam')
a2 = replace(a, x=42, q=42)
[out]
_testDataclassReplace.py:4: note: "replace" of "A" defined here
_testDataclassReplace.py:9: note: Revealed type is "_testDataclassReplace.A"
_testDataclassReplace.py:10: error: Too few arguments for "replace"
_testDataclassReplace.py:11: error: Argument "x" to "replace" of "A" has incompatible type "str"; expected "int"
Expand Down
Loading