Skip to content

Commit 4fdda24

Browse files
Allow TypedDict for cache implementation (#105)
1 parent f89c519 commit 4fdda24

File tree

3 files changed

+43
-8
lines changed

3 files changed

+43
-8
lines changed

.coveragerc

+4-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@ _site-packages-to-src-mapping =
1313

1414
[report]
1515
exclude_also =
16-
^\s*@pytest\.mark\.xfail
16+
if TYPE_CHECKING
17+
assert False
18+
: \.\.\.(\s*#.*)?$
19+
^ +\.\.\.$
1720
# small library, don't fail when running without C-extension
1821
fail_under = 50.00
1922
skip_covered = true

src/propcache/_helpers_py.py

+10-6
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Various helper functions."""
22

33
import sys
4+
from collections.abc import Mapping
45
from functools import cached_property
56
from typing import Any, Callable, Generic, Optional, Protocol, TypeVar, Union, overload
67

@@ -13,10 +14,13 @@
1314
Self = Any
1415

1516
_T = TypeVar("_T")
17+
# We use Mapping to make it possible to use TypedDict, but this isn't
18+
# technically type safe as we need to assign into the dict.
19+
_Cache = TypeVar("_Cache", bound=Mapping[str, Any])
1620

1721

18-
class _CacheImpl(Protocol):
19-
_cache: dict[str, Any]
22+
class _CacheImpl(Protocol[_Cache]):
23+
_cache: _Cache
2024

2125

2226
class under_cached_property(Generic[_T]):
@@ -29,7 +33,7 @@ class under_cached_property(Generic[_T]):
2933
variable. It is, in Python parlance, a data descriptor.
3034
"""
3135

32-
def __init__(self, wrapped: Callable[..., _T]) -> None:
36+
def __init__(self, wrapped: Callable[[Any], _T]) -> None:
3337
self.wrapped = wrapped
3438
self.__doc__ = wrapped.__doc__
3539
self.name = wrapped.__name__
@@ -38,10 +42,10 @@ def __init__(self, wrapped: Callable[..., _T]) -> None:
3842
def __get__(self, inst: None, owner: Optional[type[object]] = None) -> Self: ...
3943

4044
@overload
41-
def __get__(self, inst: _CacheImpl, owner: Optional[type[object]] = None) -> _T: ...
45+
def __get__(self, inst: _CacheImpl[Any], owner: Optional[type[object]] = None) -> _T: ... # type: ignore[misc]
4246

4347
def __get__(
44-
self, inst: Optional[_CacheImpl], owner: Optional[type[object]] = None
48+
self, inst: Optional[_CacheImpl[Any]], owner: Optional[type[object]] = None
4549
) -> Union[_T, Self]:
4650
if inst is None:
4751
return self
@@ -52,5 +56,5 @@ def __get__(
5256
inst._cache[self.name] = val
5357
return val
5458

55-
def __set__(self, inst: _CacheImpl, value: _T) -> None:
59+
def __set__(self, inst: _CacheImpl[Any], value: _T) -> None:
5660
raise AttributeError("cached property is read-only")

tests/test_under_cached_property.py

+29-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import sys
22
from collections.abc import Callable
3-
from typing import TYPE_CHECKING, Any, Protocol, TypeVar
3+
from typing import TYPE_CHECKING, Any, Protocol, TypedDict, TypeVar
44

55
import pytest
66

@@ -40,6 +40,34 @@ def prop2(self) -> str:
4040
assert a.prop2 == "foo"
4141

4242

43+
def test_under_cached_property_typeddict(propcache_module: APIProtocol) -> None:
44+
"""Test static typing passes with TypedDict."""
45+
46+
class _Cache(TypedDict, total=False):
47+
prop: int
48+
prop2: str
49+
50+
class A:
51+
def __init__(self) -> None:
52+
self._cache: _Cache = {}
53+
54+
@propcache_module.under_cached_property
55+
def prop(self) -> int:
56+
return 1
57+
58+
@propcache_module.under_cached_property
59+
def prop2(self) -> str:
60+
return "foo"
61+
62+
a = A()
63+
if sys.version_info >= (3, 11):
64+
assert_type(a.prop, int)
65+
assert a.prop == 1
66+
if sys.version_info >= (3, 11):
67+
assert_type(a.prop2, str)
68+
assert a.prop2 == "foo"
69+
70+
4371
def test_under_cached_property_assignment(propcache_module: APIProtocol) -> None:
4472
class A:
4573
def __init__(self) -> None:

0 commit comments

Comments
 (0)