Skip to content

Consider property access from class objects (now for real) #18969

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

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
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
31 changes: 31 additions & 0 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@
ANY_STRATEGY,
MYPYC_NATIVE_INT_NAMES,
OVERLOAD_NAMES,
PROPERTY_DECORATOR_NAMES,
AnyType,
BoolTypeQuery,
CallableType,
Expand Down Expand Up @@ -2335,6 +2336,29 @@ def check_method_override_for_base_with_name(
)
return False

def get_property_instance(self, method: Decorator | OverloadedFuncDef) -> Instance | None:
if method.type is None:
return None
deco = method if isinstance(method, Decorator) else method.items[0]
if not isinstance(deco, Decorator):
return None
property_deco_name = next(
(
name
for d in deco.original_decorators
for name in PROPERTY_DECORATOR_NAMES
if refers_to_fullname(d, name)
),
None,
)
if property_deco_name is not None:
# Extra attr preserves the underlying node to support alias assignments
# (see testPropertyAliasInClassBody)
return self.named_type(property_deco_name).copy_with_extra_attr(
"__mypy-wrapped-property", method.type
)
return None

def get_op_other_domain(self, tp: FunctionLike) -> Type | None:
if isinstance(tp, CallableType):
if tp.arg_kinds and tp.arg_kinds[0] == ARG_POS:
Expand Down Expand Up @@ -4394,6 +4418,13 @@ def set_inferred_type(self, var: Var, lvalue: Lvalue, type: Type) -> None:
refers to the variable (lvalue). If var is None, do nothing.
"""
if var and not self.current_node_deferred:
p_type = get_proper_type(type)
if (
isinstance(p_type, Instance)
and p_type.extra_attrs
and "__mypy-wrapped-property" in p_type.extra_attrs.attrs
):
type = p_type.extra_attrs.attrs["__mypy-wrapped-property"]
var.type = type
var.is_inferred = True
var.is_ready = True
Expand Down
6 changes: 6 additions & 0 deletions mypy/checker_shared.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,13 @@
from mypy.nodes import (
ArgKind,
Context,
Decorator,
Expression,
FuncItem,
LambdaExpr,
MypyFile,
Node,
OverloadedFuncDef,
RefExpr,
SymbolNode,
TypeInfo,
Expand Down Expand Up @@ -277,6 +279,10 @@ def checking_await_set(self) -> Iterator[None]:
def get_precise_awaitable_type(self, typ: Type, local_errors: ErrorWatcher) -> Type | None:
raise NotImplementedError

@abstractmethod
def get_property_instance(self, method: Decorator | OverloadedFuncDef) -> Instance | None:
raise NotImplementedError

@abstractmethod
def is_defined_in_stub(self, typ: Instance, /) -> bool:
raise NotImplementedError
Expand Down
13 changes: 9 additions & 4 deletions mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -384,14 +384,19 @@ def analyze_ref_expr(self, e: RefExpr, lvalue: bool = False) -> Type:
if isinstance(result, PartialType):
result = self.chk.handle_partial_var_type(result, lvalue, node, e)
elif isinstance(node, Decorator):
result = self.analyze_var_ref(node.var, e)
property_type = self.chk.get_property_instance(node)
if property_type is not None:
result = property_type
else:
result = self.analyze_var_ref(node.var, e)
elif isinstance(node, OverloadedFuncDef):
if node.type is None:
result = node.type
if result is None:
if self.chk.in_checked_function() and node.items:
self.chk.handle_cannot_determine_type(node.name, e)
result = AnyType(TypeOfAny.from_error)
else:
result = node.type
elif property_type := self.chk.get_property_instance(node):
result = property_type
elif isinstance(node, (FuncDef, TypeInfo, TypeAlias, MypyFile, TypeVarLikeExpr)):
result = self.analyze_static_reference(node, e, e.is_alias_rvalue or lvalue)
else:
Expand Down
10 changes: 10 additions & 0 deletions mypy/checkmember.py
Original file line number Diff line number Diff line change
Expand Up @@ -1226,6 +1226,16 @@ def analyze_class_attribute_access(
erase_vars.add(itype.type.self_type)
t = erase_typevars(t, {tv.id for tv in erase_vars})

if (
isinstance(node.node, Decorator)
and node.node.func.is_property
or isinstance(node.node, OverloadedFuncDef)
and node.node.is_property
):
property_type = mx.chk.get_property_instance(node.node)
if property_type is not None:
return property_type

is_classmethod = (
(is_decorated and cast(Decorator, node.node).func.is_class)
or (isinstance(node.node, SYMBOL_FUNCBASE_TYPES) and node.node.is_class)
Expand Down
12 changes: 2 additions & 10 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,7 @@
NEVER_NAMES,
OVERLOAD_NAMES,
OVERRIDE_DECORATOR_NAMES,
PROPERTY_DECORATOR_NAMES,
PROTOCOL_NAMES,
REVEAL_TYPE_NAMES,
TPDICT_NAMES,
Expand Down Expand Up @@ -1691,16 +1692,7 @@ def visit_decorator(self, dec: Decorator) -> None:
removed.append(i)
dec.func.is_explicit_override = True
self.check_decorated_function_is_method("override", dec)
elif refers_to_fullname(
d,
(
"builtins.property",
"abc.abstractproperty",
"functools.cached_property",
"enum.property",
"types.DynamicClassAttribute",
),
):
elif refers_to_fullname(d, PROPERTY_DECORATOR_NAMES):
removed.append(i)
dec.func.is_property = True
dec.var.is_property = True
Expand Down
9 changes: 9 additions & 0 deletions mypy/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,15 @@
# Supported @override decorator names.
OVERRIDE_DECORATOR_NAMES: Final = ("typing.override", "typing_extensions.override")

# Supported property decorators
PROPERTY_DECORATOR_NAMES: Final = (
"builtins.property",
"abc.abstractproperty",
"functools.cached_property",
"enum.property",
"types.DynamicClassAttribute",
)

ELLIPSIS_TYPE_NAMES: Final = ("builtins.ellipsis", "types.EllipsisType")

# A placeholder used for Bogus[...] parameters
Expand Down
1 change: 1 addition & 0 deletions test-data/unit/check-callable.test
Original file line number Diff line number Diff line change
Expand Up @@ -628,6 +628,7 @@ class A:

g2 = f2

reveal_type(A().f) # N: Revealed type is "builtins.int"
reveal_type(A().g) # N: Revealed type is "builtins.int"
reveal_type(A().g2) # N: Revealed type is "builtins.int"
A().g = 1 # E: Property "g" defined in "A" is read-only
Expand Down
61 changes: 57 additions & 4 deletions test-data/unit/check-classes.test
Original file line number Diff line number Diff line change
Expand Up @@ -1621,7 +1621,7 @@ class A:
self.x = 1
self.x = '' # E: Incompatible types in assignment (expression has type "str", variable has type "int")
return ''
[builtins fixtures/property.pyi]
[builtins fixtures/property-full.pyi]

[case testPropertyNameIsChecked]
class A:
Expand Down Expand Up @@ -1706,7 +1706,7 @@ class A:
a = A()
a.f.xx
a.f = '' # E: Property "f" defined in "A" is read-only
[builtins fixtures/property.pyi]
[builtins fixtures/property-full.pyi]

[case testPropertyWithSetter]
import typing
Expand All @@ -1723,7 +1723,7 @@ a.f.x # E: "int" has no attribute "x"
a.f = '' # E: Incompatible types in assignment (expression has type "str", variable has type "int")
a.f = 1
reveal_type(a.f) # N: Revealed type is "builtins.int"
[builtins fixtures/property.pyi]
[builtins fixtures/property-full.pyi]

[case testPropertyWithDeleterButNoSetter]
import typing
Expand All @@ -1737,7 +1737,60 @@ class A:
a = A()
a.f = a.f # E: Property "f" defined in "A" is read-only
a.f.x # E: "int" has no attribute "x"
[builtins fixtures/property.pyi]
[builtins fixtures/property-full.pyi]

[case testPropertyAccessOnClass]
class Foo:
@property
def bar(self) -> bool:
return True

reveal_type(bar) # N: Revealed type is "builtins.property"

reveal_type(Foo.bar) # N: Revealed type is "builtins.property"
reveal_type(Foo.bar(Foo())) # E: "property" not callable \
# N: Revealed type is "Any"
reveal_type(Foo.bar.fget(Foo())) # E: "None" not callable \
# N: Revealed type is "Any"

class Bar:
@property
def bar(self) -> bool:
return True
@bar.setter
def bar(self, bar: bool) -> None:
pass

reveal_type(bar) # N: Revealed type is "builtins.property"

reveal_type(Bar.bar) # N: Revealed type is "builtins.property"
reveal_type(Bar.bar(Bar())) # E: "property" not callable \
# N: Revealed type is "Any"
reveal_type(Bar.bar.fget(Bar())) # E: "None" not callable \
# N: Revealed type is "Any"
[builtins fixtures/property-full.pyi]

[case testPropertyAccessOnClass2]
import functools
from functools import cached_property

class Foo:
@cached_property
def foo(self) -> bool:
return True

@functools.cached_property
def bar(self) -> bool:
return True

reveal_type(foo) # N: Revealed type is "functools.cached_property[Any]"
reveal_type(bar) # N: Revealed type is "functools.cached_property[Any]"

reveal_type(Foo.foo) # N: Revealed type is "functools.cached_property[Any]"
reveal_type(Foo.bar) # N: Revealed type is "functools.cached_property[Any]"
Foo.foo(Foo()) # E: "cached_property[Any]" not callable
Foo.bar(Foo()) # E: "cached_property[Any]" not callable
[builtins fixtures/property-full.pyi]

-- Descriptors
-- -----------
Expand Down
2 changes: 1 addition & 1 deletion test-data/unit/check-functions.test
Original file line number Diff line number Diff line change
Expand Up @@ -3321,7 +3321,7 @@ class A:
@decorator
def f(self) -> int: ...

reveal_type(A.f) # N: Revealed type is "__main__.something_callable"
reveal_type(A.f) # N: Revealed type is "builtins.property"
reveal_type(A().f) # N: Revealed type is "builtins.str"
[builtins fixtures/property.pyi]

Expand Down
2 changes: 1 addition & 1 deletion test-data/unit/check-protocols.test
Original file line number Diff line number Diff line change
Expand Up @@ -3346,7 +3346,7 @@ def test(arg: P) -> None: ...
# TODO: skip type mismatch diagnostics in this case.
test(B) # E: Argument 1 to "test" has incompatible type "type[B]"; expected "P" \
# N: Following member(s) of "B" have conflicts: \
# N: foo: expected "int", got "Callable[[B], int]" \
# N: foo: expected "int", got "property" \
# N: Only class variables allowed for class object access on protocols, foo is an instance variable of "B"
test(C) # E: Argument 1 to "test" has incompatible type "type[C]"; expected "P" \
# N: Only class variables allowed for class object access on protocols, foo is an instance variable of "C"
Expand Down
43 changes: 43 additions & 0 deletions test-data/unit/fixtures/property-full.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from typing import Any, Callable, Generic, TypeVar

_T = TypeVar('_T')

class object:
def __init__(self) -> None: pass

class type:
def __init__(self, x: Any) -> None: pass

class function: pass

class property:
fget: Callable[[Any], Any] | None
fset: Callable[[Any, Any], None] | None
fdel: Callable[[Any], None] | None
__isabstractmethod__: bool

def __init__(
self,
fget: Callable[[Any], Any] | None = ...,
fset: Callable[[Any, Any], None] | None = ...,
fdel: Callable[[Any], None] | None = ...,
doc: str | None = ...,
) -> None: ...
def getter(self, fget: Callable[[Any], Any], /) -> property: ...
def setter(self, fset: Callable[[Any, Any], None], /) -> property: ...
def deleter(self, fdel: Callable[[Any], None], /) -> property: ...
def __get__(self, instance: Any, owner: type | None = None, /) -> Any: ...
def __set__(self, instance: Any, value: Any, /) -> None: ...
def __delete__(self, instance: Any, /) -> None: ...
class classmethod: pass

class list: pass
class dict: pass
class int: pass
class float: pass
class str: pass
class bytes: pass
class bool: pass
class ellipsis: pass

class tuple(Generic[_T]): pass
2 changes: 1 addition & 1 deletion test-data/unit/fixtures/property.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ class type:

class function: pass

property = object() # Dummy definition
class property: pass # Dummy definition
class classmethod: pass

class list(typing.Generic[_T]): pass
Expand Down