Skip to content

Commit 3b2c257

Browse files
authored
Reuse functools.cached_property definition instead of defining our own (typeddjango#1771)
Mypy has special handling for functools.cached_property, making it compatible with classvars and the @Property decorator (mypy-play example). But this handling did not extend to our django.utils.functional.cached_property. By simply re-exporting functools.cached_property, we can piggyback on the already existing infrastructure in typeshed/mypy. Note that typeshed did not define functools.cached_property on Python 3.7 and older. So this will break for users of older Python versions. But I think that's fine since we advertise 3.8 as the minimal Python version.
1 parent 2e6e716 commit 3b2c257

File tree

3 files changed

+67
-18
lines changed

3 files changed

+67
-18
lines changed

django-stubs/utils/functional.pyi

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,14 @@
11
from collections.abc import Callable, Sequence
2+
3+
# Mypy has special handling for functools.cached_property, reuse typeshed's definition instead of defining our own
4+
from functools import cached_property as cached_property
25
from typing import Any, Generic, Protocol, SupportsIndex, TypeVar, overload
36

47
from django.db.models.base import Model
58
from typing_extensions import Self, TypeAlias
69

710
_T = TypeVar("_T")
811

9-
class cached_property(Generic[_T]):
10-
func: Callable[[Any], _T]
11-
name: str | None
12-
def __init__(self, func: Callable[[Any], _T], name: str | None = ...) -> None: ...
13-
@overload
14-
def __get__(self, instance: None, cls: type[Any] | None = ...) -> Self: ...
15-
@overload
16-
def __get__(self, instance: object, cls: type[Any] | None = ...) -> _T: ...
17-
def __set_name__(self, owner: type[Any], name: str) -> None: ...
18-
1912
# Promise is only subclassed by a proxy class defined in the lazy function
2013
# so it makes sense for it to have all the methods available in that proxy class
2114
class Promise:

scripts/stubtest/allowlist.txt

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,3 +143,46 @@ django.middleware.csrf.REASON_BAD_TOKEN
143143

144144
# RemovedInDjango41
145145
django.core.cache.backends.memcached.MemcachedCache
146+
147+
# We re-export `functools.cached_property` which has different semantics
148+
django.utils.functional.cached_property.__class_getitem__
149+
django.utils.functional.cached_property.__init__
150+
django.utils.functional.cached_property.__set__
151+
django.utils.functional.cached_property.name
152+
153+
# Ignore @cached_property error "cannot reconcile @property on stub with runtime object"
154+
django.db.migrations.RenameField.new_name_lower
155+
django.db.migrations.RenameField.old_name_lower
156+
django.db.migrations.RenameIndex.new_name_lower
157+
django.db.migrations.RenameIndex.old_name_lower
158+
django.db.migrations.RenameModel.new_name_lower
159+
django.db.migrations.RenameModel.old_name_lower
160+
django.db.migrations.operations.RenameField.new_name_lower
161+
django.db.migrations.operations.RenameField.old_name_lower
162+
django.db.migrations.operations.RenameIndex.new_name_lower
163+
django.db.migrations.operations.RenameIndex.old_name_lower
164+
django.db.migrations.operations.RenameModel.new_name_lower
165+
django.db.migrations.operations.RenameModel.old_name_lower
166+
django.db.migrations.operations.fields.FieldOperation.model_name_lower
167+
django.db.migrations.operations.fields.FieldOperation.name_lower
168+
django.db.migrations.operations.fields.RenameField.new_name_lower
169+
django.db.migrations.operations.fields.RenameField.old_name_lower
170+
django.db.migrations.operations.models.AlterTogetherOptionOperation.option_value
171+
django.db.migrations.operations.models.IndexOperation.model_name_lower
172+
django.db.migrations.operations.models.ModelOperation.name_lower
173+
django.db.migrations.operations.models.RenameIndex.new_name_lower
174+
django.db.migrations.operations.models.RenameIndex.old_name_lower
175+
django.db.migrations.operations.models.RenameModel.new_name_lower
176+
django.db.migrations.operations.models.RenameModel.old_name_lower
177+
django.db.migrations.state.ModelState.name_lower
178+
django.db.migrations.state.ProjectState.apps
179+
django.middleware.csrf.CsrfViewMiddleware.allowed_origin_subdomains
180+
django.middleware.csrf.CsrfViewMiddleware.allowed_origins_exact
181+
django.middleware.csrf.CsrfViewMiddleware.csrf_trusted_origins_hosts
182+
django.urls.URLPattern.lookup_str
183+
django.urls.URLResolver.url_patterns
184+
django.urls.URLResolver.urlconf_module
185+
django.urls.resolvers.URLPattern.lookup_str
186+
django.urls.resolvers.URLResolver.url_patterns
187+
django.urls.resolvers.URLResolver.urlconf_module
188+
django.utils.connection.BaseConnectionHandler.settings

tests/typecheck/utils/test_functional.yml

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,36 @@
11
- case: cached_property_class_vs_instance_attributes
22
main: |
33
from django.utils.functional import cached_property
4-
from typing import List
4+
from typing import List, ClassVar
55
66
class Foo:
77
@cached_property
88
def attr(self) -> List[str]: ...
9-
@cached_property # E: Argument 1 to "cached_property" has incompatible type "Callable[[Foo, str], List[str]]"; expected "Callable[[Any], List[str]]" [arg-type]
9+
10+
@cached_property # E: Too many arguments for property [misc]
1011
def attr2(self, arg2: str) -> List[str]: ...
1112
12-
reveal_type(attr) # N: Revealed type is "django.utils.functional.cached_property[builtins.list[builtins.str]]"
13-
reveal_type(attr.name) # N: Revealed type is "Union[builtins.str, None]"
13+
reveal_type(attr) # N: Revealed type is "def (self: main.Foo) -> builtins.list[builtins.str]"
1414
15-
reveal_type(Foo.attr) # N: Revealed type is "django.utils.functional.cached_property[builtins.list[builtins.str]]"
16-
reveal_type(Foo.attr.func) # N: Revealed type is "def (Any) -> builtins.list[builtins.str]"
15+
reveal_type(Foo.attr) # N: Revealed type is "def (self: main.Foo) -> builtins.list[builtins.str]"
1716
1817
f = Foo()
19-
reveal_type(f.attr) # N: Revealed type is "builtins.list[builtins.str]"
20-
f.attr.name # E: "List[str]" has no attribute "name" [attr-defined]
18+
reveal_type(f.attr) # N: Revealed type is "builtins.list[builtins.str]"
19+
f.attr.func # E: "List[str]" has no attribute "func" [attr-defined]
20+
21+
# May be overridden by @property
22+
class Bar(Foo):
23+
@property
24+
def attr(self) -> List[str]: ...
25+
26+
# May be overridden by ClassVar
27+
class Quux(Foo):
28+
attr: ClassVar[List[str]] = []
29+
30+
# ClassVar may not be overridden by cached_property
31+
class Baz(Quux):
32+
@cached_property
33+
def attr(self) -> List[str]: ... # E: Cannot override writeable attribute with read-only property [override]
2134
2235
- case: str_promise_proxy
2336
main: |

0 commit comments

Comments
 (0)