#!/usr/bin/env python
# -*- coding: utf-8 -*-
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: © 2021 Massachusetts Institute of Technology.
# SPDX-FileCopyrightText: © 2021 Lee McCuller <mcculler@caltech.edu>
# NOTICE: authors should document their contributions in concisely in NOTICE
# with details inline in source files, comments, and docstrings.
"""
"""
# from builtins import object
from functools import partial
from ..utilities.unique import NOARG, unique_generator
from .utilities import (
raise_attrerror_from_property,
raise_msg_from_property,
try_name_file_line,
)
from .bases import (
InnerException,
PropertyTransforming,
)
_UNIQUE_local = unique_generator()
[docs]
class AccessError(Exception):
pass
[docs]
class ClassMemoizedDescriptor(object):
"""
Works like a combination of :obj:`property` and :obj:`classmethod` as well as :obj:`~.memoized_property`
"""
[docs]
def __init__(self, fget, doc=None):
self.fget = fget
self.__doc__ = doc or fget.__doc__
self.__name__ = fget.__name__
def __get__(self, obj, cls):
if obj is None:
return self
result = self.fget(cls)
setattr(cls, self.__name__, result)
return result
memoized_class_property = ClassMemoizedDescriptor
[docs]
class MemoizedDescriptor(object):
"""
wraps a member function just as :obj:`property` but saves its value after evaluation
(and is thus only evaluated once)
"""
_declarative_instantiation = False
transforming = True
simple_delete = False
[docs]
def __init__(
self,
fget,
name=None,
doc=None,
declarative=None,
transforming=None,
simple_delete=False,
original_callname=None,
):
self.fget = fget
if name is None:
self.__name__ = fget.__name__
else:
self.__name__ = name
if doc is None:
self.__doc__ = fget.__doc__
else:
self.__doc__ = doc
if declarative is not None:
self._declarative_instantiation = declarative
if transforming is not None:
self.transforming = transforming
if simple_delete:
self.simple_delete = simple_delete
if original_callname is not None:
self.original_callname = original_callname
else:
self.original_callname = self.__class__.__name__
return
def __get__(self, obj, cls):
if obj is None:
return self
result = obj.__dict__.get(self.__name__, _UNIQUE_local)
if result is _UNIQUE_local:
# bd = obj.__boot_dict__
bd = getattr(obj, "__boot_dict__", None)
if bd is not None:
result = bd.pop(self.__name__, _UNIQUE_local)
if not bd:
del obj.__boot_dict__
if result is _UNIQUE_local:
try:
result = self.fget(obj)
except TypeError as e:
raise_msg_from_property(
(
"Property attribute {name} of {orepr} accessed with no initial value. Needs an initial value, or"
" to be declared with a default argument at file:"
"\n{filename}"
"\nline {lineno}."
),
AccessError,
self,
obj,
e,
if_from_file=__file__,
**try_name_file_line(self.fget)
)
raise
except AttributeError as e:
raise_attrerror_from_property(self, obj, e)
else:
try:
result = self.fget(obj, result)
except TypeError as e:
raise_msg_from_property(
(
"Property attribute {name} of {orepr} accessed with an initial value. Needs no initial value, or"
"to be declared taking an argument at file:"
"\n{filename}"
"\nline {lineno}."
),
AccessError,
self,
obj,
e,
if_from_file=__file__,
**try_name_file_line(self.fget)
)
raise
except AttributeError as e:
raise_attrerror_from_property(self, obj, e)
if __debug__:
if result is NOARG:
raise InnerException("Return result was NOARG (usu)")
if self.transforming and isinstance(result, PropertyTransforming):
try:
result = result.construct(
parent=obj,
name=self.__name__,
)
except Exception as e:
raise
raise_msg_from_property(
(
"Property attribute {name} of {orepr} failed constructing a Transforming object"
),
RuntimeError,
self,
obj,
e,
)
# print("SET Value for attr ({0}) in {1}".format(self.__name__, id(obj)))
obj.__dict__[self.__name__] = result
return result
def __set__(self, obj, value):
oldvalue = obj.__dict__.get(self.__name__, _UNIQUE_local)
if oldvalue is _UNIQUE_local:
bd = getattr(obj, "__boot_dict__", None)
if bd is not None:
oldv = bd.setdefault(self.__name__, value)
if oldv is not value:
d = dict(
name=self.__name__,
)
try:
d["orepr"] = repr(obj)
except Exception:
d["orepr"] = "<object of {0}>".format(obj.__class__.__name__)
raise RuntimeError(
"Initial set for attribute {name} on object {orepr} must be unique".format(
**d
)
)
else:
obj.__boot_dict__ = {self.__name__: value}
else:
# the new value shows up in the object BEFORE the exchanger is called
if oldvalue is value:
return
obj.__dict__[self.__name__] = value
try:
revalue = self.fget(obj, value, oldvalue)
except TypeError as e:
raise_msg_from_property(
(
"Property attribute {name} of {orepr} set, replacing an initial value with a new. Either setting not allowed, or"
"must be declared taking 2 arguments at file:"
"\n{filename}"
"\nline {lineno}."
),
AccessError,
self,
obj,
e,
if_from_file=__file__,
**try_name_file_line(self.fget)
)
raise
if revalue is not NOARG:
obj.__dict__[self.__name__] = revalue
else:
del obj.__dict__[self.__name__]
return
def __delete__(self, obj):
oldvalue = obj.__dict__.get(self.__name__, _UNIQUE_local)
if oldvalue is _UNIQUE_local:
raise InnerException(
"Value for attr ({0}) in {1}({2}) never initialized before delete".format(
self.__name__, obj, id(obj)
)
)
else:
if self.simple_delete:
del obj.__dict__[self.__name__]
else:
# the new value shows up in the object BEFORE the exchanger is called
del obj.__dict__[self.__name__]
try:
revalue = self.fget(obj, NOARG, oldvalue)
except TypeError as e:
raise_msg_from_property(
(
"Property attribute {name} of {orepr} deleted, replacing an initial value with NOARG. Either deleting not allowed, or"
"must be declared taking 2 arguments at file:"
"\n{filename}"
"\nline {lineno}."
),
AccessError,
self,
obj,
e,
if_from_file=__file__,
**try_name_file_line(self.fget)
)
raise
if revalue is not NOARG:
obj.__dict__[self.__name__] = revalue
return
[docs]
class MemoizedDescriptorFNoSet(object):
"""
wraps a member function just as :obj:`property` but saves its value after evaluation
(and is thus only evaluated once)
"""
_declarative_instantiation = False
_force_boot_dict = True
[docs]
def __init__(
self,
fget,
name=None,
doc=None,
declarative=None,
original_callname=None,
):
self.fget = fget
if name is None:
self.__name__ = fget.__name__
else:
self.__name__ = name
if doc is None:
self.__doc__ = fget.__doc__
else:
self.__doc__ = doc
if declarative is not None:
self._declarative_instantiation = declarative
if original_callname is not None:
self.original_callname = original_callname
else:
self.original_callname = self.__class__.__name__
return
def __get__(self, obj, cls):
if obj is None:
return self
result = obj.__dict__.get(self.__name__, _UNIQUE_local)
if result is _UNIQUE_local:
# bd = obj.__boot_dict__
bd = getattr(obj, "__boot_dict__", None)
if bd is not None:
result = bd.pop(self.__name__, _UNIQUE_local)
if not bd:
del obj.__boot_dict__
if result is _UNIQUE_local:
try:
result = self.fget(obj)
except TypeError as e:
raise_msg_from_property(
(
"Property attribute {name} of {orepr} accessed with no initial value. Needs an initial value, or"
"to be declared with a default argument at file:"
"\n{filename}"
"\nline {lineno}."
),
AccessError,
self,
obj,
e,
if_from_file=__file__,
**try_name_file_line(self.fget)
)
raise
except AttributeError as e:
raise_attrerror_from_property(self, obj, e)
else:
try:
result = self.fget(obj, result)
except TypeError as e:
raise_msg_from_property(
(
"Property attribute {name} of {orepr} accessed with an initial value. Needs no initial value, or"
"to be declared taking an argument at file:"
"\n{filename}"
"\nline {lineno}."
),
AccessError,
self,
obj,
e,
if_from_file=__file__,
**try_name_file_line(self.fget)
)
raise
except AttributeError as e:
raise_attrerror_from_property(self, obj, e)
if __debug__:
if result is NOARG:
raise InnerException("Return result was NOARG")
# obj.__dict__[self.__name__] = result
if isinstance(result, PropertyTransforming):
result = result.construct(
parent=obj,
name=self.__name__,
)
# use standard (or overloaded! setter since this will assign to __dict__ by default
# when this descriptor object is missing __set__)
setattr(obj, self.__name__, result)
# in case the setattr transformed the object
result = getattr(obj, self.__name__)
return result
[docs]
def mproperty(__func=None, original_callname="mproperty", **kwargs):
def wrap(func):
desc = MemoizedDescriptor(func, original_callname=original_callname, **kwargs)
return desc
if __func is not None:
return wrap(__func)
else:
return wrap
[docs]
def dproperty(__func=None, declarative=True, original_callname="dproperty", **kwargs):
return mproperty(
__func=__func,
declarative=declarative,
original_callname=original_callname,
**kwargs
)
[docs]
def mproperty_plain(__func=None, original_callname="mproperty_plain", **kwargs):
def wrap(func):
desc = MemoizedDescriptorFNoSet(
func, original_callname=original_callname, **kwargs
)
return desc
if __func is not None:
return wrap(__func)
else:
return wrap
[docs]
def dproperty_plain(
__func=None, declarative=True, original_callname="dproperty_plain", **kwargs
):
return mproperty_plain(
__func=__func,
declarative=declarative,
original_callname=original_callname,
**kwargs
)
if __debug__:
mproperty_fns = mproperty
dproperty_fns = dproperty
else:
mproperty_fns = mproperty_plain
dproperty_fns = dproperty_plain
[docs]
class MemoizeFunction(object):
"""cache the return value of a method
This class is meant to be used as a decorator of methods. The return value
from a given method invocation will be cached on the instance whose method
was invoked. All arguments passed to a method decorated with memoize must
be hashable.
If a memoized method is invoked directly on its class the result will not
be cached. Instead the method will be invoked like a static method:
class Obj(object):
@memoize
def add_to(self, arg):
return self + arg
Obj.add_to(1) # not enough arguments
Obj.add_to(1, 2) # returns 3, result is not cached
from Daniel Miller on Active State
http://code.activestate.com/recipes/577452-a-memoize-decorator-for-instance-methods/
MIT Licenced
"""
[docs]
def __init__(self, func):
self.func = func
def __get__(self, obj, objtype=None):
if obj is None:
return self.func
return partial(self, obj)
def __call__(self, *args, **kw):
obj = args[0]
try:
cache = obj.__cache
except AttributeError:
cache = obj.__cache = {}
key = (self.func, args[1:], frozenset(list(kw.items())))
try:
res = cache[key]
except KeyError:
res = cache[key] = self.func(*args, **kw)
return res
mfunction = MemoizeFunction