tobiko/tobiko/common/_cached.py

141 lines
4.0 KiB
Python

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