Merge pull request #6149 from bluetech/cached-property

Add a @cached_property implementation
This commit is contained in:
Ran Benita 2019-11-10 22:11:04 +02:00 committed by GitHub
commit b352e34938
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 68 additions and 13 deletions

View File

@ -10,7 +10,11 @@ import sys
from contextlib import contextmanager from contextlib import contextmanager
from inspect import Parameter from inspect import Parameter
from inspect import signature from inspect import signature
from typing import Callable
from typing import Generic
from typing import Optional
from typing import overload from typing import overload
from typing import TypeVar
import attr import attr
import py import py
@ -20,6 +24,13 @@ from _pytest._io.saferepr import saferepr
from _pytest.outcomes import fail from _pytest.outcomes import fail
from _pytest.outcomes import TEST_OUTCOME 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() NOTSET = object()
@ -374,3 +385,33 @@ if getattr(attr, "__version_info__", ()) >= (19, 2):
ATTRS_EQ_FIELD = "eq" ATTRS_EQ_FIELD = "eq"
else: else:
ATTRS_EQ_FIELD = "cmp" 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 ExceptionChainRepr
from _pytest._code.code import ExceptionInfo from _pytest._code.code import ExceptionInfo
from _pytest._code.code import ReprExceptionInfo from _pytest._code.code import ReprExceptionInfo
from _pytest.compat import cached_property
from _pytest.compat import getfslineno from _pytest.compat import getfslineno
from _pytest.fixtures import FixtureDef from _pytest.fixtures import FixtureDef
from _pytest.fixtures import FixtureLookupError from _pytest.fixtures import FixtureLookupError
@ -448,17 +449,9 @@ class Item(Node):
def reportinfo(self) -> Tuple[str, Optional[int], str]: def reportinfo(self) -> Tuple[str, Optional[int], str]:
return self.fspath, None, "" return self.fspath, None, ""
@property @cached_property
def location(self) -> Tuple[str, Optional[int], str]: def location(self) -> Tuple[str, Optional[int], str]:
try: location = self.reportinfo()
return self._location fspath = self.session._node_location_to_relpath(location[0])
except AttributeError: assert type(location[2]) is str
location = self.reportinfo() return (fspath, location[1], location[2])
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

View File

@ -4,6 +4,7 @@ from functools import wraps
import pytest import pytest
from _pytest.compat import _PytestWrapper from _pytest.compat import _PytestWrapper
from _pytest.compat import cached_property
from _pytest.compat import get_real_func from _pytest.compat import get_real_func
from _pytest.compat import is_generator from _pytest.compat import is_generator
from _pytest.compat import safe_getattr from _pytest.compat import safe_getattr
@ -178,3 +179,23 @@ def test_safe_isclass():
assert False, "Should be ignored" assert False, "Should be ignored"
assert safe_isclass(CrappyClass()) is False 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