Skip to content

Commit

Permalink
Reuse functools.cached_property definition instead of defining our own (
Browse files Browse the repository at this point in the history
#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.
  • Loading branch information
intgr authored Oct 24, 2023
1 parent 2e6e716 commit 3b2c257
Show file tree
Hide file tree
Showing 3 changed files with 67 additions and 18 deletions.
13 changes: 3 additions & 10 deletions django-stubs/utils/functional.pyi
Original file line number Diff line number Diff line change
@@ -1,21 +1,14 @@
from collections.abc import Callable, Sequence

# Mypy has special handling for functools.cached_property, reuse typeshed's definition instead of defining our own
from functools import cached_property as cached_property
from typing import Any, Generic, Protocol, SupportsIndex, TypeVar, overload

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

_T = TypeVar("_T")

class cached_property(Generic[_T]):
func: Callable[[Any], _T]
name: str | None
def __init__(self, func: Callable[[Any], _T], name: str | None = ...) -> None: ...
@overload
def __get__(self, instance: None, cls: type[Any] | None = ...) -> Self: ...
@overload
def __get__(self, instance: object, cls: type[Any] | None = ...) -> _T: ...
def __set_name__(self, owner: type[Any], name: str) -> None: ...

# Promise is only subclassed by a proxy class defined in the lazy function
# so it makes sense for it to have all the methods available in that proxy class
class Promise:
Expand Down
43 changes: 43 additions & 0 deletions scripts/stubtest/allowlist.txt
Original file line number Diff line number Diff line change
Expand Up @@ -143,3 +143,46 @@ django.middleware.csrf.REASON_BAD_TOKEN

# RemovedInDjango41
django.core.cache.backends.memcached.MemcachedCache

# We re-export `functools.cached_property` which has different semantics
django.utils.functional.cached_property.__class_getitem__
django.utils.functional.cached_property.__init__
django.utils.functional.cached_property.__set__
django.utils.functional.cached_property.name

# Ignore @cached_property error "cannot reconcile @property on stub with runtime object"
django.db.migrations.RenameField.new_name_lower
django.db.migrations.RenameField.old_name_lower
django.db.migrations.RenameIndex.new_name_lower
django.db.migrations.RenameIndex.old_name_lower
django.db.migrations.RenameModel.new_name_lower
django.db.migrations.RenameModel.old_name_lower
django.db.migrations.operations.RenameField.new_name_lower
django.db.migrations.operations.RenameField.old_name_lower
django.db.migrations.operations.RenameIndex.new_name_lower
django.db.migrations.operations.RenameIndex.old_name_lower
django.db.migrations.operations.RenameModel.new_name_lower
django.db.migrations.operations.RenameModel.old_name_lower
django.db.migrations.operations.fields.FieldOperation.model_name_lower
django.db.migrations.operations.fields.FieldOperation.name_lower
django.db.migrations.operations.fields.RenameField.new_name_lower
django.db.migrations.operations.fields.RenameField.old_name_lower
django.db.migrations.operations.models.AlterTogetherOptionOperation.option_value
django.db.migrations.operations.models.IndexOperation.model_name_lower
django.db.migrations.operations.models.ModelOperation.name_lower
django.db.migrations.operations.models.RenameIndex.new_name_lower
django.db.migrations.operations.models.RenameIndex.old_name_lower
django.db.migrations.operations.models.RenameModel.new_name_lower
django.db.migrations.operations.models.RenameModel.old_name_lower
django.db.migrations.state.ModelState.name_lower
django.db.migrations.state.ProjectState.apps
django.middleware.csrf.CsrfViewMiddleware.allowed_origin_subdomains
django.middleware.csrf.CsrfViewMiddleware.allowed_origins_exact
django.middleware.csrf.CsrfViewMiddleware.csrf_trusted_origins_hosts
django.urls.URLPattern.lookup_str
django.urls.URLResolver.url_patterns
django.urls.URLResolver.urlconf_module
django.urls.resolvers.URLPattern.lookup_str
django.urls.resolvers.URLResolver.url_patterns
django.urls.resolvers.URLResolver.urlconf_module
django.utils.connection.BaseConnectionHandler.settings
29 changes: 21 additions & 8 deletions tests/typecheck/utils/test_functional.yml
Original file line number Diff line number Diff line change
@@ -1,23 +1,36 @@
- case: cached_property_class_vs_instance_attributes
main: |
from django.utils.functional import cached_property
from typing import List
from typing import List, ClassVar
class Foo:
@cached_property
def attr(self) -> List[str]: ...
@cached_property # E: Argument 1 to "cached_property" has incompatible type "Callable[[Foo, str], List[str]]"; expected "Callable[[Any], List[str]]" [arg-type]
@cached_property # E: Too many arguments for property [misc]
def attr2(self, arg2: str) -> List[str]: ...
reveal_type(attr) # N: Revealed type is "django.utils.functional.cached_property[builtins.list[builtins.str]]"
reveal_type(attr.name) # N: Revealed type is "Union[builtins.str, None]"
reveal_type(attr) # N: Revealed type is "def (self: main.Foo) -> builtins.list[builtins.str]"
reveal_type(Foo.attr) # N: Revealed type is "django.utils.functional.cached_property[builtins.list[builtins.str]]"
reveal_type(Foo.attr.func) # N: Revealed type is "def (Any) -> builtins.list[builtins.str]"
reveal_type(Foo.attr) # N: Revealed type is "def (self: main.Foo) -> builtins.list[builtins.str]"
f = Foo()
reveal_type(f.attr) # N: Revealed type is "builtins.list[builtins.str]"
f.attr.name # E: "List[str]" has no attribute "name" [attr-defined]
reveal_type(f.attr) # N: Revealed type is "builtins.list[builtins.str]"
f.attr.func # E: "List[str]" has no attribute "func" [attr-defined]
# May be overridden by @property
class Bar(Foo):
@property
def attr(self) -> List[str]: ...
# May be overridden by ClassVar
class Quux(Foo):
attr: ClassVar[List[str]] = []
# ClassVar may not be overridden by cached_property
class Baz(Quux):
@cached_property
def attr(self) -> List[str]: ... # E: Cannot override writeable attribute with read-only property [override]
- case: str_promise_proxy
main: |
Expand Down

0 comments on commit 3b2c257

Please sign in to comment.