141 lines
4.0 KiB
Python
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)
|