Skip to content

Commit b352e34

Browse files
authored
Merge pull request #6149 from bluetech/cached-property
Add a @cached_property implementation
2 parents abcedd6 + 42a46ea commit b352e34

File tree

3 files changed

+68
-13
lines changed

3 files changed

+68
-13
lines changed

src/_pytest/compat.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@
1010
from contextlib import contextmanager
1111
from inspect import Parameter
1212
from inspect import signature
13+
from typing import Callable
14+
from typing import Generic
15+
from typing import Optional
1316
from typing import overload
17+
from typing import TypeVar
1418

1519
import attr
1620
import py
@@ -20,6 +24,13 @@
2024
from _pytest.outcomes import fail
2125
from _pytest.outcomes import TEST_OUTCOME
2226

27+
if False: # TYPE_CHECKING
28+
from typing import Type # noqa: F401 (used in type string)
29+
30+
31+
_T = TypeVar("_T")
32+
_S = TypeVar("_S")
33+
2334

2435
NOTSET = object()
2536

@@ -374,3 +385,33 @@ def overload(f): # noqa: F811
374385
ATTRS_EQ_FIELD = "eq"
375386
else:
376387
ATTRS_EQ_FIELD = "cmp"
388+
389+
390+
if sys.version_info >= (3, 8):
391+
# TODO: Remove type ignore on next mypy update.
392+
# https://github.com/python/typeshed/commit/add0b5e930a1db16560fde45a3b710eefc625709
393+
from functools import cached_property # type: ignore
394+
else:
395+
396+
class cached_property(Generic[_S, _T]):
397+
__slots__ = ("func", "__doc__")
398+
399+
def __init__(self, func: Callable[[_S], _T]) -> None:
400+
self.func = func
401+
self.__doc__ = func.__doc__
402+
403+
@overload
404+
def __get__(
405+
self, instance: None, owner: Optional["Type[_S]"] = ...
406+
) -> "cached_property[_S, _T]":
407+
raise NotImplementedError()
408+
409+
@overload # noqa: F811
410+
def __get__(self, instance: _S, owner: Optional["Type[_S]"] = ...) -> _T:
411+
raise NotImplementedError()
412+
413+
def __get__(self, instance, owner=None): # noqa: F811
414+
if instance is None:
415+
return self
416+
value = instance.__dict__[self.func.__name__] = self.func(instance)
417+
return value

src/_pytest/nodes.py

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from _pytest._code.code import ExceptionChainRepr
1616
from _pytest._code.code import ExceptionInfo
1717
from _pytest._code.code import ReprExceptionInfo
18+
from _pytest.compat import cached_property
1819
from _pytest.compat import getfslineno
1920
from _pytest.fixtures import FixtureDef
2021
from _pytest.fixtures import FixtureLookupError
@@ -448,17 +449,9 @@ def add_report_section(self, when: str, key: str, content: str) -> None:
448449
def reportinfo(self) -> Tuple[str, Optional[int], str]:
449450
return self.fspath, None, ""
450451

451-
@property
452+
@cached_property
452453
def location(self) -> Tuple[str, Optional[int], str]:
453-
try:
454-
return self._location
455-
except AttributeError:
456-
location = self.reportinfo()
457-
fspath = self.session._node_location_to_relpath(location[0])
458-
assert type(location[2]) is str
459-
self._location = (
460-
fspath,
461-
location[1],
462-
location[2],
463-
) # type: Tuple[str, Optional[int], str]
464-
return self._location
454+
location = self.reportinfo()
455+
fspath = self.session._node_location_to_relpath(location[0])
456+
assert type(location[2]) is str
457+
return (fspath, location[1], location[2])

testing/test_compat.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import pytest
66
from _pytest.compat import _PytestWrapper
7+
from _pytest.compat import cached_property
78
from _pytest.compat import get_real_func
89
from _pytest.compat import is_generator
910
from _pytest.compat import safe_getattr
@@ -178,3 +179,23 @@ def __class__(self):
178179
assert False, "Should be ignored"
179180

180181
assert safe_isclass(CrappyClass()) is False
182+
183+
184+
def test_cached_property() -> None:
185+
ncalls = 0
186+
187+
class Class:
188+
@cached_property
189+
def prop(self) -> int:
190+
nonlocal ncalls
191+
ncalls += 1
192+
return ncalls
193+
194+
c1 = Class()
195+
assert ncalls == 0
196+
assert c1.prop == 1
197+
assert c1.prop == 1
198+
c2 = Class()
199+
assert ncalls == 1
200+
assert c2.prop == 2
201+
assert c1.prop == 1

0 commit comments

Comments
 (0)