Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions src/_pytest/compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@
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
Expand All @@ -20,6 +24,13 @@
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()

Expand Down Expand Up @@ -374,3 +385,33 @@ def overload(f): # noqa: F811
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
19 changes: 6 additions & 13 deletions src/_pytest/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
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
Expand Down Expand Up @@ -448,17 +449,9 @@ def add_report_section(self, when: str, key: str, content: str) -> None:
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
location = self.reportinfo()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With regard to this I've noticed that we'd want to cache reportinfo() probably/maybe - any ideas how to do this? Should/could we wrap it in a property with a different name then? lru might also be an option, but seems bad with just a single item?!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think for methods we can implement a simple decorator that does something similar to cached_property, say cached_method. It should be even simpler than cached_property.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lru might also be an option, but seems bad with just a single item?!

lru_cache is very fast in my experience (in Python terms...). However, it can't really be used on methods, it leaks self.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lets leave the method caching for later for now,
it can trivially be implemented as shim for a new private property if necessary

fspath = self.session._node_location_to_relpath(location[0])
assert type(location[2]) is str
return (fspath, location[1], location[2])
21 changes: 21 additions & 0 deletions testing/test_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

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
Expand Down Expand Up @@ -178,3 +179,23 @@ def __class__(self):
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