Add a @cached_property implementation

This is a useful utility to abstract the caching property idiom.

It is in compat.py since eventually it will be replaced by
functools.cached_property.

Fixes #6131.
This commit is contained in:
Ran Benita 2019-11-06 10:24:09 +02:00
parent 710e3c40e0
commit 42a46ea786
3 changed files with 68 additions and 13 deletions

View File

@ -10,7 +10,11 @@ import sys
from contextlib import contextmanager
from inspect import Parameter
from inspect import signature
from typing import Callable
from typing import Generic
from typing import Optional
from typing import overload
from typing import TypeVar
import attr
import py
@ -20,6 +24,13 @@ from _pytest._io.saferepr import saferepr
from _pytest.outcomes import fail
from _pytest.outcomes import TEST_OUTCOME
if False: # TYPE_CHECKING
from typing import Type # noqa: F401 (used in type string)
_T = TypeVar("_T")
_S = TypeVar("_S")
NOTSET = object()
@ -374,3 +385,33 @@ if getattr(attr, "__version_info__", ()) >= (19, 2):
ATTRS_EQ_FIELD = "eq"
else:
ATTRS_EQ_FIELD = "cmp"
if sys.version_info >= (3, 8):
# TODO: Remove type ignore on next mypy update.
# https://github.com/python/typeshed/commit/add0b5e930a1db16560fde45a3b710eefc625709
from functools import cached_property # type: ignore
else:
class cached_property(Generic[_S, _T]):
__slots__ = ("func", "__doc__")
def __init__(self, func: Callable[[_S], _T]) -> None:
self.func = func
self.__doc__ = func.__doc__
@overload
def __get__(
self, instance: None, owner: Optional["Type[_S]"] = ...
) -> "cached_property[_S, _T]":
raise NotImplementedError()
@overload # noqa: F811
def __get__(self, instance: _S, owner: Optional["Type[_S]"] = ...) -> _T:
raise NotImplementedError()
def __get__(self, instance, owner=None): # noqa: F811
if instance is None:
return self
value = instance.__dict__[self.func.__name__] = self.func(instance)
return value

View File

@ -15,6 +15,7 @@ import _pytest._code
from _pytest._code.code import ExceptionChainRepr
from _pytest._code.code import ExceptionInfo
from _pytest._code.code import ReprExceptionInfo
from _pytest.compat import cached_property
from _pytest.compat import getfslineno
from _pytest.fixtures import FixtureDef
from _pytest.fixtures import FixtureLookupError
@ -448,17 +449,9 @@ class Item(Node):
def reportinfo(self) -> Tuple[str, Optional[int], str]:
return self.fspath, None, ""
@property
@cached_property
def location(self) -> Tuple[str, Optional[int], str]:
try:
return self._location
except AttributeError:
location = self.reportinfo()
fspath = self.session._node_location_to_relpath(location[0])
assert type(location[2]) is str
self._location = (
fspath,
location[1],
location[2],
) # type: Tuple[str, Optional[int], str]
return self._location
return (fspath, location[1], location[2])

View File

@ -4,6 +4,7 @@ from functools import wraps
import pytest
from _pytest.compat import _PytestWrapper
from _pytest.compat import cached_property
from _pytest.compat import get_real_func
from _pytest.compat import is_generator
from _pytest.compat import safe_getattr
@ -178,3 +179,23 @@ def test_safe_isclass():
assert False, "Should be ignored"
assert safe_isclass(CrappyClass()) is False
def test_cached_property() -> None:
ncalls = 0
class Class:
@cached_property
def prop(self) -> int:
nonlocal ncalls
ncalls += 1
return ncalls
c1 = Class()
assert ncalls == 0
assert c1.prop == 1
assert c1.prop == 1
c2 = Class()
assert ncalls == 1
assert c2.prop == 2
assert c1.prop == 1