Merge pull request #6149 from bluetech/cached-property
Add a @cached_property implementation
This commit is contained in:
commit
b352e34938
|
@ -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
|
||||||
|
|
|
@ -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
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue