diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f5e7651839ed8786161c27344e281a5d9cf2ebe..d00e56986db626a22c4689ee99bb5ce326c852a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,3 @@ # v2.18.5 - +* added `cached_property` diff --git a/docs/coding/decorators.rst b/docs/coding/decorators.rst index 52020b492423541999125e7431a576aa3276fbb9..ffa3727fb06304283b8348596b162b9c807560e8 100644 --- a/docs/coding/decorators.rst +++ b/docs/coding/decorators.rst @@ -17,6 +17,8 @@ Decorators .. autofunction:: satella.coding.decorators.cache_memoize +.. autofunction:: satella.coding.decorators.call_with_arguments + .. autofunction:: satella.coding.decorators.queue_get .. autofunction:: satella.coding.decorators.copy_arguments diff --git a/satella/__init__.py b/satella/__init__.py index 55439584313806dde534530c3e94be98ed824605..8f1a63771ab8d1ddebe82fde27c307562c879f4b 100644 --- a/satella/__init__.py +++ b/satella/__init__.py @@ -1 +1 @@ -__version__ = '2.18.5a1' +__version__ = '2.18.5' diff --git a/satella/coding/decorators/__init__.py b/satella/coding/decorators/__init__.py index 3fc063c0f938cee8d73029c01e7754f7d7d9db98..f524f8fadc99916ecfe01c37b5647fe3c5507529 100644 --- a/satella/coding/decorators/__init__.py +++ b/satella/coding/decorators/__init__.py @@ -1,6 +1,7 @@ from .arguments import auto_adapt_to_methods, attach_arguments, for_argument, \ execute_before, copy_arguments, replace_argument_if, transform_result, \ - transform_arguments, execute_if_attribute_none, execute_if_attribute_not_none + transform_arguments, execute_if_attribute_none, execute_if_attribute_not_none, \ + cached_property from .decorators import wraps, chain_functions, has_keys, short_none, memoize, return_as_list, \ default_return, cache_memoize, call_method_on_exception from .flow_control import loop_while, queue_get @@ -13,4 +14,5 @@ __all__ = ['retry', 'transform_result', 'transform_arguments', 'attach_arguments', 'for_argument', 'loop_while', 'memoize', 'copy_arguments', 'replace_argument_if', 'return_as_list', 'default_return', 'cache_memoize', 'call_method_on_exception', - 'execute_if_attribute_none', 'execute_if_attribute_not_none'] + 'execute_if_attribute_none', 'execute_if_attribute_not_none', + 'cached_property'] diff --git a/satella/coding/decorators/arguments.py b/satella/coding/decorators/arguments.py index e7fa1308811e391abd596cff94b292560ddd98de..8e33ad4b7623944c5ef111861655973727a340a5 100644 --- a/satella/coding/decorators/arguments.py +++ b/satella/coding/decorators/arguments.py @@ -410,3 +410,42 @@ def execute_if_attribute_not_none(attribute: str): return inner return outer + +def cached_property(prop_name: str, assume_not_loaded = None): + """ + A decorator to use to create cached properties. + + You job is to only write the value returner. If the value is + currently assume_not_loaded (None by default) your property + method will be called. Otherwise it will be served from + cached attribute, whose value you provide as parameter. + + Use as follows: + + >>> class Example: + >>> def __init__(self): + >>> self._a = None + >>> @property + >>> @cached_property('_a') + >>> def a(self) -> str: + >>> return 'abc' + >>> a = Example() + >>> assert a.a == 'abc' + >>> assert a._a == 'abc' + + :param prop_name: Name of property to store the value in + :param assume_not_loaded: Value if currently the attribute is + equal to this, it is assumed to not have been loaded + """ + def outer(fun): + @wraps(fun) + def inner(self, *args, **kwargs): + attr_v = getattr(self, prop_name) + if attr_v == assume_not_loaded: + attr_v = fun(self, *args, **kwargs) + setattr(self, prop_name, attr_v) + return attr_v + else: + return attr_v + return inner + return outer diff --git a/tests/test_coding/test_decorators.py b/tests/test_coding/test_decorators.py index 425f384fcb756ca0dc63325f391a137a297b2623..cb69d32b2961425980f2388564036d61a5d0d729 100644 --- a/tests/test_coding/test_decorators.py +++ b/tests/test_coding/test_decorators.py @@ -10,7 +10,7 @@ from satella.coding.decorators import auto_adapt_to_methods, attach_arguments, \ execute_before, loop_while, memoize, copy_arguments, replace_argument_if, \ retry, return_as_list, default_return, transform_result, transform_arguments, \ cache_memoize, call_method_on_exception, execute_if_attribute_none, \ - execute_if_attribute_not_none + execute_if_attribute_not_none, cached_property from satella.coding.predicates import x from satella.exceptions import PreconditionError @@ -19,6 +19,23 @@ logger = logging.getLogger(__name__) class TestDecorators(unittest.TestCase): + def test_cached_property(self): + class Example: + def __init__(self): + self._a = None + self.called = 0 + + @property + @cached_property('_a') + def a(self): + self.called += 1 + return 'abc' + a = Example() + self.assertEqual(a.a, 'abc') + self.assertEqual(a.called, 1) + self.assertEqual(a.a, 'abc') + self.assertEqual(a.called, 1) + def test_execute_if_attribute_not_none(self): class ExecIfAttrNone: def __init__(self):