Skip to content

Add under_cached_property_with_name and under_cache_name (#200)#230

Draft
aiolibsbot wants to merge 3 commits into
aio-libs:masterfrom
aiolibsbot:koan/issue-200-custom-cache-attr
Draft

Add under_cached_property_with_name and under_cache_name (#200)#230
aiolibsbot wants to merge 3 commits into
aio-libs:masterfrom
aiolibsbot:koan/issue-200-custom-cache-attr

Conversation

@aiolibsbot
Copy link
Copy Markdown
Contributor

@aiolibsbot aiolibsbot commented May 17, 2026

What

Implements GH-200: adds a variant of under_cached_property whose cache attribute name is configurable, plus a decorator-factory class for binding the attribute name once and reusing it.

Why

under_cached_property is hard-coded to read/write inst._cache. Several patterns can't use it as-is:

  • __slots__-based classes that want a slot name other than _cache (e.g. to avoid clashes with the underscored cache attribute naming convention of parent classes).
  • Classes that maintain multiple cache buckets (e.g. user-level vs. session-level) and want to route different cached properties into different dicts.

The requester (GH-200) provided a concrete sketch of two new classes; this PR is a cleaned-up implementation of that idea.

How

Two new public symbols are exported from propcache.api (and re-exported at top level):

  • under_cached_property_with_name(wrapped, cache_name) — descriptor identical in behavior to under_cached_property except it looks up the cache via getattr(inst, cache_name) instead of inst._cache.
  • under_cache_name(cache_name) — decorator factory that binds a cache name once. Calling an instance with a function returns an under_cached_property_with_name already wired to that attribute. Implemented as a class (not a closure) so the Cython implementation can keep the call cheap, per the requester's note.

Both implementations live in _helpers_py.py and _helpers_c.pyx so the C-extension path is on par with under_cached_property. The dispatch in _helpers.py and the public surface in api.py / __init__.py mirror the existing pattern.

Testing

  • New tests/test_under_cached_property_with_name.py covers: basic decoration, caching idempotency, read-only __set__, missing cache attribute, multiple isolated buckets, direct descriptor construction, __slots__ compatibility, and the factory's cache_name attribute.
  • Existing test_api.py and test_init.py extended to assert the new symbols are reachable through propcache.api and the top-level propcache facade.
  • Full suite: pytest tests/ — 63 passed, 4 codspeed-only tests skipped.
  • pre-commit run clean (ruff, pyupgrade, mypy-strict, cython-lint, codespell).

Notes for reviewers

  • API naming follows the existing under_ prefix convention; happy to rename if you'd prefer a shorter symbol like reify or cached_in.
  • I left the descriptor's cache attribute lookup at getattr(inst, self.cache_name) so subclasses can override with a property if they need lazy initialization — happy to switch to a faster direct __dict__ lookup if that trade-off is unwelcome.
  • Draft so naming and surface area can be discussed before final review.

Closes #200.


Quality Report

Changes: 10 files changed, 459 insertions(+), 5 deletions(-)

Code scan: clean

Tests: failed (FAILED)

Branch hygiene: clean

Generated by Kōan post-mission quality pipeline

Adds a variant of ``under_cached_property`` whose cache attribute name is
configurable, plus a decorator-factory class that binds the attribute name
once for reuse across many class definitions. The new descriptor unlocks
use cases that the existing ``under_cached_property`` could not support,
notably classes that define ``__slots__`` and want to share a single cache
dict layout across instances.

Both pure-Python and Cython implementations are provided to keep the
attribute-lookup hot path on par with the existing under_cached_property.
@psf-chronographer psf-chronographer Bot added the bot:chronographer:provided There is a change note present in this PR label May 17, 2026
self.cache_name = cache_name

@overload
def __get__(self, inst: None, owner: type[object] | None = None) -> Self: ...
def __get__(self, inst: None, owner: type[object] | None = None) -> Self: ...

@overload
def __get__(self, inst: object, owner: type[object] | None = None) -> _T: ...
class _CacheNameFactory(Protocol):
def __call__(
self, wrapped: Callable[[Any], _T_co]
) -> "under_cached_property_with_name[_T_co]": ...
@staticmethod
def under_cached_property_with_name(
func: Callable[[Any], _T_co], cache_name: str
) -> "under_cached_property_with_name[_T_co]": ...
) -> "under_cached_property_with_name[_T_co]": ...

@staticmethod
def under_cache_name(name: str) -> _CacheNameFactory: ...
@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented May 17, 2026

Merging this PR will not alter performance

✅ 4 untouched benchmarks


Comparing aiolibsbot:koan/issue-200-custom-cache-attr (c4a7537) with master (eeedfdb)

Open in CodSpeed

@codecov
Copy link
Copy Markdown

codecov Bot commented May 17, 2026

Codecov Report

❌ Patch coverage is 98.00000% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 97.76%. Comparing base (eeedfdb) to head (c4a7537).

Files with missing lines Patch % Lines
src/propcache/_helpers_py.py 93.93% 0 Missing and 2 partials ⚠️
tests/test_under_cached_property_with_name.py 98.34% 0 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #230      +/-   ##
==========================================
+ Coverage   97.72%   97.76%   +0.03%     
==========================================
  Files          17       18       +1     
  Lines         881     1076     +195     
  Branches       44       67      +23     
==========================================
+ Hits          861     1052     +191     
  Misses         12       12              
- Partials        8       12       +4     
Flag Coverage Δ
CI-GHA 97.76% <98.00%> (+0.03%) ⬆️
MyPy 91.12% <86.73%> (-1.05%) ⬇️
OS-Linux 97.46% <97.45%> (-0.03%) ⬇️
OS-Windows 94.59% <94.90%> (+0.06%) ⬆️
OS-macOS 94.59% <94.90%> (+0.06%) ⬆️
Py-3.10.11 92.73% <92.99%> (+0.02%) ⬆️
Py-3.10.20 94.08% <92.99%> (-0.45%) ⬇️
Py-3.11.15 95.77% <94.90%> (-0.36%) ⬇️
Py-3.11.9 94.42% <94.90%> (+0.12%) ⬆️
Py-3.12.10 94.42% <94.90%> (+0.12%) ⬆️
Py-3.12.13 95.77% <94.90%> (-0.36%) ⬇️
Py-3.13.13 95.77% <94.90%> (-0.36%) ⬇️
Py-3.14.4 94.39% <94.90%> (+0.13%) ⬆️
Py-3.14.5 95.75% <94.90%> (-0.35%) ⬇️
Py-3.14.5t 95.75% <94.90%> (-0.35%) ⬇️
Py-pypy3.10.16-7.3.19 81.92% <92.99%> (+3.79%) ⬆️
VM-macos-latest 94.59% <94.90%> (+0.06%) ⬆️
VM-ubuntu-latest 97.46% <97.45%> (-0.03%) ⬇️
VM-windows-11-arm 94.42% <94.90%> (+0.12%) ⬆️
VM-windows-latest 94.59% <94.90%> (+0.06%) ⬆️
pytest 97.46% <97.45%> (-0.03%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Copy Markdown
Member

@Vizonex Vizonex left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Surprised to see that my idea is being implemented here. I have no complaints about this since this was something I thought would be good for developers who need for a more beginner friendly interface.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bot:chronographer:provided There is a change note present in this PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

add an addtional class that can define a custom cache attribute

4 participants