From 5f43d4232538d06f7b763b2e209087ce26af4702 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Piotr=20Ma=C5=9Blanka?= <pmaslanka@smok.co>
Date: Fri, 15 Oct 2021 16:51:41 +0200
Subject: [PATCH] v2.18.5

---
 CHANGELOG.md                           |  2 +-
 docs/coding/decorators.rst             |  2 ++
 satella/__init__.py                    |  2 +-
 satella/coding/decorators/__init__.py  |  6 ++--
 satella/coding/decorators/arguments.py | 39 ++++++++++++++++++++++++++
 tests/test_coding/test_decorators.py   | 19 ++++++++++++-
 6 files changed, 65 insertions(+), 5 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1f5e7651..d00e5698 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 52020b49..ffa3727f 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 55439584..8f1a6377 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 3fc063c0..f524f8fa 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 e7fa1308..8e33ad4b 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 425f384f..cb69d32b 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):
-- 
GitLab