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 2 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
15 changes: 15 additions & 0 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@
ANY_STRATEGY,
MYPYC_NATIVE_INT_NAMES,
OVERLOAD_NAMES,
PROPERTY_DECORATOR_NAMES,
AnyType,
BoolTypeQuery,
CallableType,
Expand Down Expand Up @@ -2318,6 +2319,20 @@ def check_method_override_for_base_with_name(
)
return False

def get_property_instance(self, method: Decorator) -> Instance | None:
property_deco_name = next(
(
name
for d in method.original_decorators
for name in PROPERTY_DECORATOR_NAMES
if refers_to_fullname(d, name)
),
None,
)
if property_deco_name is not None:
return self.named_type(property_deco_name)
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
5 changes: 5 additions & 0 deletions mypy/checker_shared.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from mypy.nodes import (
ArgKind,
Context,
Decorator,
Expression,
FuncItem,
LambdaExpr,
Expand Down Expand Up @@ -279,6 +280,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) -> Instance | None:
raise NotImplementedError


class CheckerScope:
# We keep two stacks combined, to maintain the relative order
Expand Down
13 changes: 10 additions & 3 deletions mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -381,12 +381,15 @@ def analyze_ref_expr(self, e: RefExpr, lvalue: bool = False) -> Type:
# Reference to a global function.
result = function_type(node, self.named_type("builtins.function"))
elif isinstance(node, OverloadedFuncDef):
result = node.type
if node.type 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 isinstance(node.items[0], Decorator):
property_type = self.chk.get_property_instance(node.items[0])
if property_type is not None:
result = property_type
elif isinstance(node, TypeInfo):
# Reference to a type object.
if node.typeddict_type:
Expand All @@ -412,7 +415,11 @@ def analyze_ref_expr(self, e: RefExpr, lvalue: bool = False) -> Type:
# Reference to a module object.
result = self.module_type(node)
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, TypeAlias):
# Something that refers to a type alias appears in runtime context.
# Note that we suppress bogus errors for alias redefinitions,
Expand Down
10 changes: 10 additions & 0 deletions mypy/checkmember.py
Original file line number Diff line number Diff line change
Expand Up @@ -1194,6 +1194,16 @@ def analyze_class_attribute_access(
# C[int].x -> int
t = erase_typevars(expand_type_by_instance(t, isuper), {tv.id for tv in def_vars})

if isinstance(node.node, Decorator) and node.node.func.is_property:
property_type = mx.chk.get_property_instance(node.node)
if property_type is not None:
return property_type
if isinstance(node.node, OverloadedFuncDef) and node.node.is_property:
assert isinstance(node.node.items[0], Decorator)
property_type = mx.chk.get_property_instance(node.node.items[0])
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 @@ -262,6 +262,7 @@
NEVER_NAMES,
OVERLOAD_NAMES,
OVERRIDE_DECORATOR_NAMES,
PROPERTY_DECORATOR_NAMES,
PROTOCOL_NAMES,
REVEAL_TYPE_NAMES,
TPDICT_NAMES,
Expand Down Expand Up @@ -1687,16 +1688,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 @@ -178,6 +178,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",
)

# A placeholder used for Bogus[...] parameters
_dummy: Final[Any] = object()

Expand Down
62 changes: 57 additions & 5 deletions test-data/unit/check-classes.test
Original file line number Diff line number Diff line change
Expand Up @@ -1621,8 +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]
[out]
[builtins fixtures/property-full.pyi]

[case testDynamicallyTypedProperty]
import typing
Expand All @@ -1632,7 +1631,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 @@ -1649,7 +1648,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 @@ -1663,7 +1662,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 @@ -3298,7 +3298,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
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