Implement cached property tool
Change-Id: I169265771f018d56a518357615afafbe3bf4885a
This commit is contained in:
parent
48795f0a47
commit
5f143a6e69
tobiko
@ -14,6 +14,7 @@
|
|||||||
from __future__ import absolute_import
|
from __future__ import absolute_import
|
||||||
|
|
||||||
from tobiko.common import _asserts
|
from tobiko.common import _asserts
|
||||||
|
from tobiko.common import _cached
|
||||||
from tobiko.common import _config
|
from tobiko.common import _config
|
||||||
from tobiko.common import _detail
|
from tobiko.common import _detail
|
||||||
from tobiko.common import _exception
|
from tobiko.common import _exception
|
||||||
@ -33,6 +34,9 @@ details_content = _detail.details_content
|
|||||||
FailureException = _asserts.FailureException
|
FailureException = _asserts.FailureException
|
||||||
fail = _asserts.fail
|
fail = _asserts.fail
|
||||||
|
|
||||||
|
cached = _cached.cached
|
||||||
|
CachedProperty = _cached.CachedProperty
|
||||||
|
|
||||||
tobiko_config = _config.tobiko_config
|
tobiko_config = _config.tobiko_config
|
||||||
tobiko_config_dir = _config.tobiko_config_dir
|
tobiko_config_dir = _config.tobiko_config_dir
|
||||||
tobiko_config_path = _config.tobiko_config_path
|
tobiko_config_path = _config.tobiko_config_path
|
||||||
|
140
tobiko/common/_cached.py
Normal file
140
tobiko/common/_cached.py
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
# Copyright 2020 Red Hat
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
from __future__ import absolute_import
|
||||||
|
|
||||||
|
|
||||||
|
NOT_CACHED = object()
|
||||||
|
|
||||||
|
|
||||||
|
class CachedProperty(object):
|
||||||
|
""" Property that calls getter only the first time it is required
|
||||||
|
|
||||||
|
It invokes property setter with the value returned by getter the first time
|
||||||
|
it is required so that it is not requested again until del operator is not
|
||||||
|
called again on top of target object.
|
||||||
|
|
||||||
|
Implements default setter and deleter behaving as a regular attribute would
|
||||||
|
by setting or removing named attribute of target object __dict__
|
||||||
|
|
||||||
|
The name used for storing cached property value is got from getter function
|
||||||
|
name if not passed as constructor attribute.
|
||||||
|
|
||||||
|
Examples of use:
|
||||||
|
|
||||||
|
class MyClass(object):
|
||||||
|
|
||||||
|
@cached
|
||||||
|
def my_property(self):
|
||||||
|
return object()
|
||||||
|
|
||||||
|
|
||||||
|
obj = MyClass()
|
||||||
|
# my_property method not yet called
|
||||||
|
assert 'my_property' not in obj.__dict__
|
||||||
|
|
||||||
|
# my_property method is called
|
||||||
|
first_value = obj.my_property
|
||||||
|
assert obj.__dict__['my_property'] is first_value
|
||||||
|
|
||||||
|
# my_property method is not called again
|
||||||
|
assert obj.my_property is first_value
|
||||||
|
|
||||||
|
# first value is removed from dictionary
|
||||||
|
del obj.my_property
|
||||||
|
assert 'my_property' not in obj.__dict__
|
||||||
|
|
||||||
|
# my_property method is called
|
||||||
|
second_value = obj.my_property
|
||||||
|
assert obj.__dict__['my_property'] is second_value
|
||||||
|
|
||||||
|
# value returned by second call of method can be different
|
||||||
|
second_value is not first_value
|
||||||
|
|
||||||
|
For more details about how Python properties works please refers to
|
||||||
|
language documentation [1]
|
||||||
|
|
||||||
|
[1] https://docs.python.org/3/howto/descriptor.html
|
||||||
|
"""
|
||||||
|
|
||||||
|
fget = None
|
||||||
|
fset = None
|
||||||
|
fdel = None
|
||||||
|
__doc__ = None
|
||||||
|
cached_id = None
|
||||||
|
|
||||||
|
def __init__(self, fget=None, fset=None, fdel=None, doc=None,
|
||||||
|
cached_id=None):
|
||||||
|
if fget:
|
||||||
|
self.getter(fget)
|
||||||
|
if fset:
|
||||||
|
self.setter(fset)
|
||||||
|
if fdel:
|
||||||
|
self.deleter(fdel)
|
||||||
|
if doc:
|
||||||
|
self.__doc__ = doc
|
||||||
|
if cached_id:
|
||||||
|
self.cached_id = cached_id
|
||||||
|
elif self.cached_id is None:
|
||||||
|
self.cached_id = '_cached_' + str(id(self))
|
||||||
|
|
||||||
|
def getter(self, fget):
|
||||||
|
assert callable(fget)
|
||||||
|
self.fget = fget
|
||||||
|
if self.__doc__ is None:
|
||||||
|
self.__doc__ = fget.__doc__
|
||||||
|
return fget
|
||||||
|
|
||||||
|
def setter(self, fset):
|
||||||
|
self.fset = fset
|
||||||
|
return fset
|
||||||
|
|
||||||
|
def deleter(self, fdel):
|
||||||
|
self.fdel = fdel
|
||||||
|
return fdel
|
||||||
|
|
||||||
|
def __get__(self, obj, _objtype=None):
|
||||||
|
if obj is None:
|
||||||
|
return self
|
||||||
|
|
||||||
|
value = self._get_cached(obj)
|
||||||
|
if value is NOT_CACHED:
|
||||||
|
if self.fget is None:
|
||||||
|
raise AttributeError("Cached property has no getter method")
|
||||||
|
value = self.fget(obj)
|
||||||
|
self.__set__(obj, value)
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
||||||
|
def __set__(self, obj, value):
|
||||||
|
if self.fset:
|
||||||
|
self.fset(obj, value)
|
||||||
|
self._set_cached(obj, value)
|
||||||
|
|
||||||
|
def __delete__(self, obj):
|
||||||
|
if self.fdel:
|
||||||
|
self.fdel(obj)
|
||||||
|
self._delete_cached(obj)
|
||||||
|
|
||||||
|
def _get_cached(self, obj):
|
||||||
|
return getattr(obj, self.cached_id, NOT_CACHED)
|
||||||
|
|
||||||
|
def _set_cached(self, obj, value):
|
||||||
|
setattr(obj, self.cached_id, value)
|
||||||
|
|
||||||
|
def _delete_cached(self, obj):
|
||||||
|
return obj.__dict__.pop(self.cached_id, NOT_CACHED)
|
||||||
|
|
||||||
|
|
||||||
|
def cached(*args, **kwargs):
|
||||||
|
return CachedProperty(*args, **kwargs)
|
276
tobiko/tests/unit/test_cached.py
Normal file
276
tobiko/tests/unit/test_cached.py
Normal file
@ -0,0 +1,276 @@
|
|||||||
|
# Copyright 2020 Red Hat
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
from __future__ import absolute_import
|
||||||
|
|
||||||
|
import typing # noqa
|
||||||
|
|
||||||
|
import tobiko
|
||||||
|
from tobiko.tests import unit
|
||||||
|
|
||||||
|
|
||||||
|
NOT_CALLED = object()
|
||||||
|
|
||||||
|
|
||||||
|
class TestCached(unit.TobikoUnitTest):
|
||||||
|
|
||||||
|
my_property = tobiko.cached(doc='some doc')
|
||||||
|
|
||||||
|
def test_init(self):
|
||||||
|
prop = type(self).my_property
|
||||||
|
self.assertIsNone(prop.fget)
|
||||||
|
self.assertIsNone(prop.fset)
|
||||||
|
self.assertIsNone(prop.fdel)
|
||||||
|
self.assertEqual('some doc', prop.__doc__)
|
||||||
|
self.assertTrue(prop.cached_id)
|
||||||
|
|
||||||
|
def test_get(self):
|
||||||
|
ex = self.assertRaises(AttributeError, lambda: self.my_property)
|
||||||
|
self.assertEqual("Cached property has no getter method", str(ex))
|
||||||
|
|
||||||
|
def test_set(self):
|
||||||
|
self.my_property = value = object()
|
||||||
|
self.assertIs(value, self.my_property)
|
||||||
|
|
||||||
|
def test_delete(self):
|
||||||
|
self.my_property = object()
|
||||||
|
del self.my_property
|
||||||
|
ex = self.assertRaises(AttributeError, lambda: self.my_property)
|
||||||
|
self.assertEqual("Cached property has no getter method", str(ex))
|
||||||
|
|
||||||
|
|
||||||
|
class TestCachedWithGetter(unit.TobikoUnitTest):
|
||||||
|
|
||||||
|
@tobiko.cached
|
||||||
|
def my_property(self):
|
||||||
|
return object()
|
||||||
|
|
||||||
|
def test_init(self):
|
||||||
|
# pylint: disable=no-member
|
||||||
|
prop = type(self).my_property
|
||||||
|
assert isinstance(prop, tobiko.CachedProperty)
|
||||||
|
self.assertTrue(callable(prop.fget))
|
||||||
|
self.assertIsNone(prop.fset)
|
||||||
|
self.assertIsNone(prop.fdel)
|
||||||
|
self.assertIs(prop.fget.__doc__, prop.__doc__)
|
||||||
|
self.assertTrue(prop.cached_id)
|
||||||
|
|
||||||
|
def test_get(self):
|
||||||
|
value = self.my_property
|
||||||
|
self.assertIs(value, self.my_property)
|
||||||
|
|
||||||
|
def test_set(self):
|
||||||
|
self.my_property = value = object()
|
||||||
|
self.assertIs(value, self.my_property)
|
||||||
|
|
||||||
|
def test_delete(self):
|
||||||
|
self.my_property = value = object()
|
||||||
|
del self.my_property
|
||||||
|
self.assertIsNot(value, self.my_property)
|
||||||
|
|
||||||
|
|
||||||
|
class TestCachedWithSetter(unit.TobikoUnitTest):
|
||||||
|
|
||||||
|
_set_value = None
|
||||||
|
|
||||||
|
def set_my_property(self, value):
|
||||||
|
self._set_value = value
|
||||||
|
|
||||||
|
my_property = tobiko.cached(fset=set_my_property)
|
||||||
|
|
||||||
|
def test_init(self):
|
||||||
|
prop = type(self).my_property
|
||||||
|
self.assertIsNone(prop.fget)
|
||||||
|
self.assertIs(type(self).set_my_property, prop.fset)
|
||||||
|
self.assertIsNone(prop.fdel)
|
||||||
|
self.assertIsNone(prop.__doc__)
|
||||||
|
self.assertTrue(prop.cached_id)
|
||||||
|
|
||||||
|
def test_get(self):
|
||||||
|
self.assertFalse(hasattr(self, 'my_property'))
|
||||||
|
self.assertIsNone(self._set_value)
|
||||||
|
|
||||||
|
def test_set(self):
|
||||||
|
self.my_property = value = object()
|
||||||
|
self.assertIs(value, self.my_property)
|
||||||
|
self.assertIs(value, self._set_value)
|
||||||
|
|
||||||
|
def test_delete(self):
|
||||||
|
self.my_property = object()
|
||||||
|
del self.my_property
|
||||||
|
self.assertFalse(hasattr(self, 'my_property'))
|
||||||
|
|
||||||
|
|
||||||
|
class TestCachedWithDeleter(unit.TobikoUnitTest):
|
||||||
|
|
||||||
|
_delete_value = None
|
||||||
|
|
||||||
|
def delete_my_property(self):
|
||||||
|
self._delete_value = object()
|
||||||
|
|
||||||
|
my_property = tobiko.cached(fdel=delete_my_property)
|
||||||
|
|
||||||
|
def test_init(self):
|
||||||
|
prop = type(self).my_property
|
||||||
|
self.assertIsNone(prop.fget)
|
||||||
|
self.assertIsNone(prop.fset)
|
||||||
|
self.assertIs(type(self).delete_my_property, prop.fdel)
|
||||||
|
self.assertIsNone(prop.__doc__)
|
||||||
|
self.assertTrue(prop.cached_id)
|
||||||
|
|
||||||
|
def test_get(self):
|
||||||
|
self.assertFalse(hasattr(self, 'my_property'))
|
||||||
|
self.assertIsNone(self._delete_value)
|
||||||
|
|
||||||
|
def test_set(self):
|
||||||
|
self.my_property = value = object()
|
||||||
|
self.assertIs(value, self.my_property)
|
||||||
|
self.assertIsNone(self._delete_value)
|
||||||
|
|
||||||
|
def test_delete(self):
|
||||||
|
self.my_property = object()
|
||||||
|
del self.my_property
|
||||||
|
self.assertFalse(hasattr(self, 'my_property'))
|
||||||
|
self.assertIsNotNone(self._delete_value)
|
||||||
|
|
||||||
|
|
||||||
|
class TestCachedWithDoc(unit.TobikoUnitTest):
|
||||||
|
|
||||||
|
my_doc = 'some doc'
|
||||||
|
|
||||||
|
my_property = tobiko.cached(doc=my_doc)
|
||||||
|
|
||||||
|
@my_property.getter
|
||||||
|
def get_my_property(self):
|
||||||
|
return object()
|
||||||
|
|
||||||
|
def test_init(self):
|
||||||
|
prop = type(self).my_property
|
||||||
|
self.assertTrue(callable(prop.fget))
|
||||||
|
self.assertIsNone(prop.fset)
|
||||||
|
self.assertIsNone(prop.fdel)
|
||||||
|
self.assertIs(self.my_doc, prop.__doc__)
|
||||||
|
self.assertTrue(prop.cached_id)
|
||||||
|
|
||||||
|
|
||||||
|
class TestCachedWithCachedId(unit.TobikoUnitTest):
|
||||||
|
|
||||||
|
cached_id = 'my_cached_id'
|
||||||
|
|
||||||
|
my_property = tobiko.cached(cached_id=cached_id)
|
||||||
|
|
||||||
|
@my_property.getter
|
||||||
|
def get_my_property(self):
|
||||||
|
return object()
|
||||||
|
|
||||||
|
def test_init(self):
|
||||||
|
prop = type(self).my_property
|
||||||
|
self.assertTrue(callable(prop.fget))
|
||||||
|
self.assertIsNone(prop.fset)
|
||||||
|
self.assertIsNone(prop.fdel)
|
||||||
|
self.assertIsNone(prop.__doc__)
|
||||||
|
self.assertEqual(self.cached_id, prop.cached_id)
|
||||||
|
self.assertFalse(hasattr(self, 'my_cached_id'))
|
||||||
|
|
||||||
|
def test_get(self):
|
||||||
|
# pylint: disable=no-member
|
||||||
|
value = self.my_property
|
||||||
|
self.assertIs(value, self.my_property)
|
||||||
|
self.assertIs(value, self.my_cached_id)
|
||||||
|
|
||||||
|
def test_set(self):
|
||||||
|
# pylint: disable=no-member
|
||||||
|
self.my_property = value = object()
|
||||||
|
self.assertIs(value, self.my_property)
|
||||||
|
self.assertIs(value, self.my_cached_id)
|
||||||
|
|
||||||
|
def test_delete(self):
|
||||||
|
# pylint: disable=no-member
|
||||||
|
self.my_property = value = object()
|
||||||
|
del self.my_property
|
||||||
|
self.assertIsNot(value, self.my_property)
|
||||||
|
self.assertIsNot(value, self.my_cached_id)
|
||||||
|
|
||||||
|
|
||||||
|
class TestCachedWithDecorators(unit.TobikoUnitTest):
|
||||||
|
|
||||||
|
my_property = tobiko.cached()
|
||||||
|
|
||||||
|
getter_called = 0
|
||||||
|
|
||||||
|
@my_property.getter
|
||||||
|
def getter(self):
|
||||||
|
"""Getter documentation"""
|
||||||
|
self.getter_called += 1
|
||||||
|
return object()
|
||||||
|
|
||||||
|
setter_called = tuple() # type: typing.Tuple
|
||||||
|
|
||||||
|
@my_property.setter
|
||||||
|
def setter(self, value):
|
||||||
|
self.setter_called += (value,)
|
||||||
|
|
||||||
|
deleter_called = 0
|
||||||
|
|
||||||
|
@my_property.deleter
|
||||||
|
def deleter(self):
|
||||||
|
self.deleter_called += 1
|
||||||
|
|
||||||
|
def test_init(self):
|
||||||
|
# self.my_property method not called yet
|
||||||
|
prop = type(self).my_property
|
||||||
|
self.assertIs(type(self).getter, prop.fget)
|
||||||
|
self.assertIs(type(self).setter, prop.fset)
|
||||||
|
self.assertIs(type(self).deleter, prop.fdel)
|
||||||
|
self.assertIs(self.getter.__doc__, prop.__doc__)
|
||||||
|
self.assertTrue(prop.cached_id)
|
||||||
|
|
||||||
|
def test_get(self):
|
||||||
|
# my_property method is called
|
||||||
|
prop = type(self).my_property
|
||||||
|
value = self.my_property
|
||||||
|
self.assertEqual(1, self.getter_called)
|
||||||
|
self.assertIs(value, getattr(self, prop.cached_id))
|
||||||
|
self.assertEqual((value,), self.setter_called)
|
||||||
|
return value
|
||||||
|
|
||||||
|
def test_get_twice(self):
|
||||||
|
# my_property method is not called again
|
||||||
|
value1 = self.test_get()
|
||||||
|
value2 = self.test_get()
|
||||||
|
self.assertIs(value1, value2)
|
||||||
|
|
||||||
|
def test_set(self):
|
||||||
|
# property value is stored into the object
|
||||||
|
prop = type(self).my_property
|
||||||
|
self.my_property = value = object()
|
||||||
|
self.assertIs(value, self.my_property)
|
||||||
|
self.assertEqual(0, self.getter_called)
|
||||||
|
self.assertEqual(value, self.setter_called[-1])
|
||||||
|
self.assertIs(value, getattr(self, prop.cached_id))
|
||||||
|
return value
|
||||||
|
|
||||||
|
def test_set_twice(self):
|
||||||
|
# stored value is got after setting
|
||||||
|
value1 = self.test_set()
|
||||||
|
value2 = self.test_set()
|
||||||
|
self.assertIsNot(value1, value2)
|
||||||
|
|
||||||
|
def test_delete(self):
|
||||||
|
# deleting remove stored value
|
||||||
|
prop = type(self).my_property
|
||||||
|
value = self.test_set()
|
||||||
|
del self.my_property
|
||||||
|
self.assertFalse(hasattr(self, prop.cached_id))
|
||||||
|
self.assertIsNot(value, self.my_property)
|
||||||
|
self.assertEqual(1, self.deleter_called)
|
Loading…
x
Reference in New Issue
Block a user