From 2cf77a5a70c1c6fa49366d5decd9d59caa40f78c Mon Sep 17 00:00:00 2001 From: Ali Ebrahim Date: Tue, 9 Jan 2024 20:42:39 +0000 Subject: [PATCH] Add support for `@property` for once_per_instance. --- once/__init__.py | 11 ++++++++++- once_test.py | 19 +++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/once/__init__.py b/once/__init__.py index e07ead0..937d710 100644 --- a/once/__init__.py +++ b/once/__init__.py @@ -15,7 +15,7 @@ def _is_method(func: collections.abc.Callable): """Determine if a function is a method on a class.""" - if isinstance(func, (classmethod, staticmethod)): + if isinstance(func, (classmethod, staticmethod, property)): return True sig = inspect.signature(func) return "self" in sig.parameters @@ -309,6 +309,8 @@ def __get__(self, obj, cls) -> collections.abc.Callable: class once_per_instance: # pylint: disable=invalid-name """A version of once for class methods which runs once per instance.""" + is_property: bool + @classmethod def with_options(cls, per_thread: bool = False, retry_exceptions=False): return lambda func: cls(func, per_thread=per_thread, retry_exceptions=retry_exceptions) @@ -339,6 +341,11 @@ def once_factory(self) -> _ONCE_FACTORY_TYPE: def _inspect_function(self, func: collections.abc.Callable): if isinstance(func, (classmethod, staticmethod)): raise SyntaxError("Must use @once.once_per_class on classmethod and staticmethod") + if isinstance(func, property): + func = func.fget + self.is_property = True + else: + self.is_property = False if not _is_method(func): raise SyntaxError( "Attempting to use @once.once_per_instance method-only decorator " @@ -357,4 +364,6 @@ def __get__(self, obj, cls) -> collections.abc.Callable: bound_func, self.once_factory(), self.fn_type, self.retry_exceptions ) self.callables[obj] = callable + if self.is_property: + return callable() return callable diff --git a/once_test.py b/once_test.py index 84c7e75..81bc420 100644 --- a/once_test.py +++ b/once_test.py @@ -835,6 +835,25 @@ def execute(i): self.assertEqual(min(results), 1) self.assertEqual(max(results), math.ceil(_N_WORKERS / 4)) + def test_once_per_instance_property(self): + counter = Counter() + + class _CallOnceClass: + @once.once_per_instance + @property + def value(self): + nonlocal counter + return counter.get_incremented() + + a = _CallOnceClass() + b = _CallOnceClass() + self.assertEqual(a.value, 1) + self.assertEqual(b.value, 2) + self.assertEqual(a.value, 1) + self.assertEqual(b.value, 2) + self.assertEqual(_CallOnceClass().value, 3) + self.assertEqual(_CallOnceClass().value, 4) + def test_once_per_class_classmethod(self): counter = Counter()