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: |  | ||||||
|             return self._location |  | ||||||
|         except AttributeError: |  | ||||||
|         location = self.reportinfo() |         location = self.reportinfo() | ||||||
|         fspath = self.session._node_location_to_relpath(location[0]) |         fspath = self.session._node_location_to_relpath(location[0]) | ||||||
|         assert type(location[2]) is str |         assert type(location[2]) is str | ||||||
|             self._location = ( |         return (fspath, location[1], location[2]) | ||||||
|                 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