Implement cached property tool

Change-Id: I169265771f018d56a518357615afafbe3bf4885a
This commit is contained in:
Federico Ressi 2020-03-10 17:50:38 +01:00
parent 48795f0a47
commit 5f143a6e69
3 changed files with 420 additions and 0 deletions

View File

@ -14,6 +14,7 @@
from __future__ import absolute_import
from tobiko.common import _asserts
from tobiko.common import _cached
from tobiko.common import _config
from tobiko.common import _detail
from tobiko.common import _exception
@ -33,6 +34,9 @@ details_content = _detail.details_content
FailureException = _asserts.FailureException
fail = _asserts.fail
cached = _cached.cached
CachedProperty = _cached.CachedProperty
tobiko_config = _config.tobiko_config
tobiko_config_dir = _config.tobiko_config_dir
tobiko_config_path = _config.tobiko_config_path

140
tobiko/common/_cached.py Normal file
View 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)

View 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)