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 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 | ||||
|  |  | |||
|  | @ -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]) | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue