# -*- coding: utf-8 -*-
# --------------------------------------------------------------------
# The MIT License (MIT)
#
# Copyright (c) 2014 Jonathan Labéjof <jonathan.labejof@gmail.com>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# --------------------------------------------------------------------
"""Library which aims to bind named properties on any element at runtime.
This module can bind a named property on any element but None methods.
In preserving binding from inheritance and automatical mechanisms which prevent
to set any attribute on any elements.
When you are looking for an element bound property, the result is a dictionary
of the shape {element, {property name, property}}.
.. warning:
- It is adviced to delete properties from cache after deleting them at
runtime in order to avoid memory leak.
"""
from __future__ import unicode_literals, absolute_import
__all__ = [
'get_properties',
'get_first_property', 'get_first_properties',
'get_local_property', 'get_local_properties',
'put_properties', 'del_properties',
'firsts', 'remove_ctx',
'setdefault', 'free_cache',
'find_ctx', 'addproperties'
]
from six import PY2, string_types, get_method_self
from .iterable import ensureiterable
from inspect import ismethod
from collections import Hashable
from types import MethodType
try:
from threading import Timer
except ImportError:
from dummy_threading import Timer
__B3J0F__PROPERTIES__ = '__b3j0f_props' #: __dict__ properties key
__DICT__ = '__dict__' #: __dict__ elt attribute name
__CLASS__ = '__class__' #: __class__ elt attribute name
__SELF__ = '__self__' #: __self__ class instance attribute name
__BASES__ = '__bases__' # __bases__ class attribute name
__STATIC_ELEMENTS_CACHE__ = {} #: dictionary of properties for static objects
__UNHASHABLE_ELTS_CACHE__ = {} #: dictionary of properties for unhashable obj
[docs]def find_ctx(elt):
"""Get the right ctx related to input elt.
In order to keep safe memory as much as possible, it is important to find
the right context element. For example, instead of putting properties on
a function at the level of an instance, it is important to save such
property on the instance because the function.__dict__ is shared with
instance class function, and so, if the instance is deleted from memory,
the property is still present in the class memory. And so on, it is
impossible to identify the right context in such case if all properties
are saved with the same key in the same function which is the function.
"""
result = elt # by default, result is ctx
# if elt is ctx and elt is a method, it is possible to find the best ctx
if ismethod(elt):
# get instance and class of the elt
instance = get_method_self(elt)
# if instance is not None, the right context is the instance
if instance is not None:
result = instance
elif PY2:
result = elt.im_class
return result
[docs]def free_cache(ctx, *elts):
"""Free properties bound to input cached elts. If empty, free the whole
cache.
"""
for elt in elts:
if isinstance(elt, Hashable):
cache = __STATIC_ELEMENTS_CACHE__
else:
cache = __UNHASHABLE_ELTS_CACHE__
elt = id(elt)
if elt in cache:
del cache[elt]
if not elts:
__STATIC_ELEMENTS_CACHE__.clear()
__UNHASHABLE_ELTS_CACHE__.clear()
def _ctx_elt_properties(elt, ctx=None, create=False):
"""Get elt properties related to a ctx.
:param elt: property component elt. Not None methods or unhashable types.
:param ctx: elt ctx from where get properties. Equals elt if None. It
allows to get function properties related to a class or instance if
related function is defined in base class.
:param bool create: create ctx elt properties if not exist.
:return: dictionary of property by name embedded into ctx __dict__ or in
shared __STATIC_ELEMENTS_CACHE__ or else in shared
__UNHASHABLE_ELTS_CACHE__. None if no properties exists.
:rtype: dict
"""
result = None
if ctx is None:
ctx = find_ctx(elt=elt)
ctx_properties = None
# in case of dynamic object, parse the ctx __dict__
ctx__dict__ = getattr(ctx, __DICT__, None)
if ctx__dict__ is not None and isinstance(ctx__dict__, dict):
# if properties exist in ctx__dict__
if __B3J0F__PROPERTIES__ in ctx__dict__:
ctx_properties = ctx__dict__[__B3J0F__PROPERTIES__]
elif create: # if create in worst case
ctx_properties = ctx__dict__[__B3J0F__PROPERTIES__] = {}
else: # in case of static object
if isinstance(ctx, Hashable): # search among static elements
cache = __STATIC_ELEMENTS_CACHE__
else: # or unhashable elements
cache = __UNHASHABLE_ELTS_CACHE__
ctx = id(ctx)
if not isinstance(elt, Hashable):
elt = id(elt)
# if ctx is in cache
if ctx in cache:
# get its properties
ctx_properties = cache[ctx]
elif create: # elif create, get an empty dict
ctx_properties = cache[ctx] = {}
# if ctx_properties is not None
if ctx_properties is not None:
# check if elt exist in ctx_properties
if elt in ctx_properties:
result = ctx_properties[elt]
elif create: # create the right data if create
if create:
result = ctx_properties[elt] = {}
return result
[docs]def get_properties(elt, keys=None, ctx=None):
"""Get elt properties.
:param elt: properties elt. Not None methods or unhashable types.
:param keys: key(s) of properties to get from elt.
If None, get all properties.
:type keys: list or str
:param ctx: elt ctx from where get properties. Equals elt if None. It
allows to get function properties related to a class or instance if
related function is defined in base class.
:return: list of properties by elt and name.
:rtype: list
"""
# initialize keys if str
if isinstance(keys, string_types):
keys = (keys,)
result = _get_properties(elt, keys=keys, local=False, ctx=ctx)
return result
def get_property(elt, key, ctx=None):
"""Get elt key property.
:param elt: property elt. Not None methods.
:param key: property key to get from elt.
:param ctx: elt ctx from where get properties. Equals elt if None. It
allows to get function properties related to a class or instance if
related function is defined in base class.
:return: list of property values by elt.
:rtype: list
"""
result = []
properties = get_properties(elt=elt, ctx=ctx, keys=key)
if key in properties:
result = properties[key]
return result
[docs]def get_first_properties(elt, keys=None, ctx=None):
"""Get first properties related to one input key.
:param elt: first property elt. Not None methods.
:param list keys: property keys to get.
:param ctx: elt ctx from where get properties. Equals elt if None. It
allows to get function properties related to a class or instance if
related function is defined in base class.
:return: dict of first values of elt properties.
"""
# ensure keys is an iterable if not None
if isinstance(keys, string_types):
keys = (keys,)
result = _get_properties(elt, keys=keys, first=True, ctx=ctx)
return result
[docs]def get_first_property(elt, key, default=None, ctx=None):
"""Get first property related to one input key.
:param elt: first property elt. Not None methods.
:param str key: property key to get.
:param default: default value to return if key does not exist in elt.
properties
:param ctx: elt ctx from where get properties. Equals elt if None. It
allows to get function properties related to a class or instance if
related function is defined in base class.
"""
result = default
properties = _get_properties(elt, keys=(key,), ctx=ctx, first=True)
# set value if key exists in properties
if key in properties:
result = properties[key]
return result
[docs]def get_local_properties(elt, keys=None, ctx=None):
"""Get local elt properties (not defined in elt type or base classes).
:param elt: local properties elt. Not None methods.
:param keys: keys of properties to get from elt.
:param ctx: elt ctx from where get properties. Equals elt if None. It
allows to get function properties related to a class or instance if
related function is defined in base class.
:return: dict of properties by name.
:rtype: dict
"""
if isinstance(keys, string_types):
keys = (keys,)
result = _get_properties(elt, keys=keys, local=True, ctx=ctx)
return result
[docs]def get_local_property(elt, key, default=None, ctx=None):
"""Get one local property related to one input key or default value if key
is not found.
:param elt: local property elt. Not None methods.
:param str key: property key to get.
:param default: default value to return if key does not exist in elt
properties.
:param ctx: elt ctx from where get properties. Equals elt if None. It
allows to get function properties related to a class or instance if
related function is defined in base class.
:return: dict of properties by name.
:rtype: dict
"""
result = default
local_properties = get_local_properties(elt=elt, keys=(key,), ctx=ctx)
if key in local_properties:
result = local_properties[key]
return result
def _get_properties(
elt, keys=None, local=False, exclude=None, first=False, ctx=None,
_found_keys=None
):
"""Get a dictionary of elt properties.
Such dictionary is filled related to the elt properties, and from all base
elts properties if not local.
:param elt: properties elt. Not None methods.
:param ctx: elt ctx from where get properties. Equals elt if None. It
allows to get function properties related to a class or instance if
related function is defined in base class.
:param keys: keys of properties to get from elt. If None, get all
properties.
:param bool local: if False, get properties from bases classes and type
as well.
:param set exclude: elts from where not get properties.
:param bool first: get only first property values.
:param _found_keys: used such as a cache value for result.
:return: dict of properties by name and ...:
- if local or first: value.
- other cases: [(elt, value)] in order of property definition even if
multi inheritance do not ensure to respect inheritance order in a
list.
:rtype: dict
"""
result = {}
# get the best context
if ctx is None:
ctx = find_ctx(elt=elt)
# initialize found keys
if first and _found_keys is None:
_found_keys = set()
# get elt properties if exist
elt_properties = _ctx_elt_properties(elt=elt, ctx=ctx, create=False)
# if elt_properties exists
if elt_properties is not None:
# if elt properties is not empty
# try to add property values in result related to input keys
properties_to_keep = elt_properties.copy() if keys is None else {}
# get key values
if keys is not None:
for key in keys:
if key in elt_properties:
if not first or key not in _found_keys:
properties_to_keep[key] = elt_properties[key]
if first and key not in _found_keys:
_found_keys.add(key)
if local: # in case of local, use properties_to_keep
result = properties_to_keep
# if not local and not first and properties to keep
elif not first and properties_to_keep:
for name in properties_to_keep:
property_to_keep = properties_to_keep[name]
# add (elt, properties_to_keep) in result
if name not in result:
result[name] = [(elt, property_to_keep)]
else:
result[name].append((elt, property_to_keep))
# in case of first, result is properties_to_keep
elif properties_to_keep:
result = properties_to_keep
# check if all keys have been found if first
keys_left = not first or keys is None or len(keys) != len(_found_keys)
# if not local or keys left
if not local and keys_left:
# initialize exclude
if exclude is None:
exclude = set()
# add elt in exclude in order to avoid to get elt properties twice
if isinstance(elt, Hashable):
exclude.add(elt)
# and search among bases ctx elements
if ctx is not elt:
# go to base elts
try:
elt_name = elt.__name__
except AttributeError:
pass
else:
# if elt is a sub attr of ctx
if hasattr(ctx, elt_name):
if hasattr(ctx, __BASES__):
ctx_bases = ctx.__bases__
elif hasattr(ctx, __CLASS__):
ctx_bases = (ctx.__class__,)
else:
ctx_bases = ()
for base_ctx in ctx_bases:
# get base_elt properties
base_elt = getattr(base_ctx, elt_name, None)
if base_elt is not None:
base_properties = _get_properties(
elt=base_elt,
keys=keys,
local=local,
exclude=exclude,
ctx=base_ctx,
_found_keys=_found_keys
)
# update result with base_properties
for name in base_properties:
properties = base_properties[name]
if name not in result:
result[name] = properties
else:
result[name] += properties
else:
# bases classes
if hasattr(elt, __BASES__):
for base in elt.__bases__:
if base not in exclude:
base_result = _get_properties(
elt=base,
keys=keys,
local=local,
exclude=exclude,
ctx=base,
_found_keys=_found_keys
)
# update result with base_result
for name in base_result:
properties = base_result[name]
if name not in result:
result[name] = properties
else:
result[name] += properties
# search among type definition
# class
if hasattr(elt, __CLASS__):
elt_class = elt.__class__
# get class properties only if elt is not its own class
if elt_class is not elt and elt_class not in exclude:
elt_class_properties = _get_properties(
elt=elt_class,
keys=keys,
local=local,
exclude=exclude,
ctx=elt_class,
_found_keys=_found_keys
)
# update result with class result
for name in elt_class_properties:
properties = elt_class_properties[name]
if name not in result:
result[name] = properties
else:
result[name] += properties
# type
elt_type = type(elt)
# get elt type properties only if elt is not its type
if elt_type is not elt and elt_type not in exclude:
elt_type_properties = _get_properties(
elt=elt_type,
keys=keys,
local=local,
exclude=exclude,
ctx=elt_type,
_found_keys=_found_keys
)
# update result with type properties
for name in elt_type_properties:
properties = elt_type_properties[name]
if name not in result:
result[name] = properties
else:
result[name] += properties
return result
def put_property(elt, key, value, ttl=None, ctx=None):
"""Put properties in elt.
:param elt: properties elt to put. Not None methods.
:param number ttl: If not None, property time to leave.
:param ctx: elt ctx from where put properties. Equals elt if None. It
allows to get function properties related to a class or instance if
related function is defined in base class.
:param dict properties: properties to put in elt. elt and ttl are exclude.
:return: Timer if ttl is not None.
:rtype: Timer
"""
return put_properties(elt=elt, properties={key: value}, ttl=ttl, ctx=ctx)
[docs]def put_properties(elt, properties, ttl=None, ctx=None):
"""Put properties in elt.
:param elt: properties elt to put. Not None methods.
:param number ttl: If not None, property time to leave.
:param ctx: elt ctx from where put properties. Equals elt if None. It
allows to get function properties related to a class or instance if
related function is defined in base class.
:param dict properties: properties to put in elt. elt and ttl are exclude.
:return: Timer if ttl is not None.
:rtype: Timer
"""
result = None
if properties:
# get the best context
if ctx is None:
ctx = find_ctx(elt=elt)
# get elt properties
elt_properties = _ctx_elt_properties(elt=elt, ctx=ctx, create=True)
# set properties
elt_properties.update(properties)
if ttl is not None:
kwargs = {
'elt': elt,
'ctx': ctx,
'keys': tuple(properties.keys())
}
result = Timer(ttl, del_properties, kwargs=kwargs)
result.start()
return result
def put(properties, ttl=None, ctx=None):
"""Decorator dedicated to put properties on an element.
"""
def put_on(elt):
return put_properties(elt=elt, properties=properties, ttl=ttl, ctx=ctx)
return put_on
[docs]def del_properties(elt, keys=None, ctx=None):
"""Delete elt property.
:param elt: properties elt to del. Not None methods.
:param keys: property keys to delete from elt. If empty, delete all
properties.
"""
# get the best context
if ctx is None:
ctx = find_ctx(elt=elt)
elt_properties = _ctx_elt_properties(elt=elt, ctx=ctx, create=False)
# if elt properties exist
if elt_properties is not None:
if keys is None:
keys = list(elt_properties.keys())
else:
keys = ensureiterable(keys, iterable=tuple, exclude=str)
for key in keys:
if key in elt_properties:
del elt_properties[key]
# delete property component if empty
if not elt_properties:
# case of dynamic object
if isinstance(getattr(ctx, '__dict__', None), dict):
try:
if elt in ctx.__dict__[__B3J0F__PROPERTIES__]:
del ctx.__dict__[__B3J0F__PROPERTIES__][elt]
except TypeError: # if elt is unhashable
elt = id(elt)
if elt in ctx.__dict__[__B3J0F__PROPERTIES__]:
del ctx.__dict__[__B3J0F__PROPERTIES__][elt]
# if ctx_properties is empty, delete it
if not ctx.__dict__[__B3J0F__PROPERTIES__]:
del ctx.__dict__[__B3J0F__PROPERTIES__]
# case of static object and hashable
else:
if isinstance(ctx, Hashable):
cache = __STATIC_ELEMENTS_CACHE__
else: # case of static and unhashable object
cache = __UNHASHABLE_ELTS_CACHE__
ctx = id(ctx)
if not isinstance(elt, Hashable):
elt = id(elt)
# in case of static object
if ctx in cache:
del cache[ctx][elt]
if not cache[ctx]:
del cache[ctx]
[docs]def firsts(properties):
"""
Transform a dictionary of {name: [(elt, value)+]} (resulting from
get_properties) to a dictionary of {name, value} where names are first
encountered in input properties.
:param dict properties: properties to firsts.
:return: dictionary of parameter values by names.
:rtype: dict
"""
result = {}
# parse elts
for name in properties:
elt_properties = properties[name]
# add property values in result[name]
result[name] = elt_properties[0][1]
return result
[docs]def remove_ctx(properties):
"""
Transform a dictionary of {name: [(elt, value)+]} into a dictionary of
{name: [value+]}.
:param dict properties: properties from where get only values.
:return: dictionary of parameter values by names.
:rtype: dict
"""
result = {}
for name in properties:
elt_properties = properties[name]
result[name] = []
for _, value in elt_properties:
result[name].append(value)
return result
[docs]def setdefault(elt, key, default, ctx=None):
"""
Get a local property and create default value if local property does not
exist.
:param elt: local proprety elt to get/create. Not None methods.
:param str key: proprety name.
:param default: property value to set if key no in local properties.
:return: property value or default if property does not exist.
"""
result = default
# get the best context
if ctx is None:
ctx = find_ctx(elt=elt)
# get elt properties
elt_properties = _ctx_elt_properties(elt=elt, ctx=ctx, create=True)
# if key exists in elt properties
if key in elt_properties:
# result is elt_properties[key]
result = elt_properties[key]
else: # set default property value
elt_properties[key] = default
return result
def _protectedattrname(name):
"""Get protected attribute name from input public attribute name.
:param str name: public attribute name.
:return: protected attribute name.
:rtype: str
"""
return '_{0}'.format(name)
[docs]def addproperties(
names,
bfget=None, afget=None, enableget=True,
bfset=None, afset=None, enableset=True,
bfdel=None, afdel=None, enabledel=True
):
"""Decorator in charge of adding python properties to cls.
{a/b}fget, {a/b}fset and {a/b}fdel are applied to all properties matching
names in taking care to not forget default/existing properties. The
prefixes *a* and *b* are respectively for after and before default/existing
property getter/setter/deleter execution.
These getter, setter and deleter functions are called before existing or
default getter, setter and deleters. Default getter, setter and deleters
are functions which uses an attribute with a name starting with '_' and
finishing with the property name (like the python language convention).
.. seealso::
_protectedattrname(name)
:param str(s) names: property name(s) to add.
:param bfget: getter function to apply to all properties before
default/existing getter execution. Parameters are a decorated cls
instance and a property name.
:param afget: fget function to apply to all properties after
default/existing getter execution. Parameters are a decorated cls
instance and a property name.
:param bool enableget: if True (default), enable existing or default getter
. Otherwise, use only fget if given.
:param bfset: fset function to apply to all properties before
default/existing setter execution. Parameters are a decorated cls
instance and a property name.
:param afset: fset function to apply to all properties after
default/existing setter execution. Parameters are a decorated cls
instance and a property name.
:param bool enableset: if True (default), enable existing or default setter
. Otherwise, use only fset if given.
:param bfdel: fdel function to apply to all properties before
default/existing deleter execution. Parameters are a decorated cls
instance and a property name.
:param bfdel: fdel function to apply to all properties after
default/existing deleter execution. Parameters are a decorated cls
instance and a property name.
:param bool enabledel: if True (default), enable existing or default
deleter. Otherwise, use only fdel if given.
:return: cls decorator.
"""
# ensure names is a list
names = ensureiterable(names, exclude=string_types)
if isinstance(bfget, MethodType):
finalbfget = lambda self, name: bfget(name)
else:
finalbfget = bfget
if isinstance(afget, MethodType):
finalafget = lambda self, name: afget(name)
else:
finalafget = afget
if isinstance(bfset, MethodType):
finalbfset = lambda self, value, name: bfset(value, name)
else:
finalbfset = bfset
if isinstance(afset, MethodType):
finalafset = lambda self, value, name: afset(value, name)
else:
finalafset = afset
if isinstance(bfdel, MethodType):
finalbfdel = lambda self, name: bfdel(name)
else:
finalbfdel = bfdel
if isinstance(afdel, MethodType):
finalafdel = lambda self, name: afdel(name)
else:
finalafdel = afdel
def _addproperties(cls):
"""Add properties to cls.
:param type cls: cls on adding properties.
:return: cls
"""
for name in names:
protectedattrname = _protectedattrname(name)
# try to find an existing property
existingproperty = getattr(cls, name, None)
if isinstance(existingproperty, property):
_fget = existingproperty.fget
_fset = existingproperty.fset
_fdel = existingproperty.fdel
else:
_fget, _fset, _fdel = None, None, None
# construct existing/default getter
if _fget is None:
def _fget(protectedattrname):
"""Simple getter wrapper."""
def _fget(self):
"""Simple getter."""
return getattr(self, protectedattrname, None)
return _fget
_fget = _fget(protectedattrname)
_fget.__doc__ = 'Get this {0}.\n:return: this {0}.'.format(
name
)
# transform method to function in order to add self in parameters
if isinstance(_fget, MethodType):
final_fget = lambda self: _fget()
else:
final_fget = _fget
# construct existing/default setter
if _fset is None:
def _fset(protectedattrname):
"""Simple setter wrapper."""
def _fset(self, value):
"""Simple setter."""
setattr(self, protectedattrname, value)
return _fset
_fset = _fset(protectedattrname)
_fset.__doc__ = (
'Change of {0}.\n:param {0}: {0} to use.'.format(name)
)
# transform method to function in order to add self in parameters
if isinstance(_fset, MethodType):
final_fset = lambda self, value: _fset(value)
else:
final_fset = _fset
# construct existing/default deleter
if _fdel is None:
def _fdel(protectedattrname):
"""Simple deleter wrapper."""
def _fdel(self):
"""Simple deleter."""
if hasattr(self, protectedattrname):
delattr(self, protectedattrname)
return _fdel
_fdel = _fdel(protectedattrname)
_fdel.__doc__ = 'Delete this {0}.'.format(name)
# transform method to function in order to add self in parameters
if isinstance(_fdel, MethodType):
final_fdel = lambda self: _fdel()
else:
final_fdel = _fdel
def _getter(final_fget, name):
"""Property getter wrapper."""
def _getter(self):
"""Property getter."""
result = None
# start to process input bfget
if finalbfget is not None:
result = finalbfget(self, name)
# process cls getter
if enableget:
result = final_fget(self)
# finish to process afget
if finalafget is not None:
result = finalafget(self, name)
return result
return _getter
_getter = _getter(final_fget, name)
_getter.__doc__ = final_fget.__doc__ # update doc
def _setter(final_fset, name):
"""Property setter wrapper."""
def _setter(self, value):
"""Property setter."""
# start to process input bfset
if finalbfset is not None:
finalbfset(self, value, name)
# finish to process cls setter
if enableset:
final_fset(self, value)
# finish to process afset
if finalafset is not None:
finalafset(self, value, name)
return _setter
_setter = _setter(final_fset, name)
_setter.__doc__ = final_fset.__doc__ # update doc
def _deleter(final_fdel, name):
"""Property deleter wrapper."""
def _deleter(self):
"""Property deleter."""
# start to process input fdel
if finalbfdel is not None:
finalbfdel(self, name)
# finish to process cls deleter
if enabledel:
final_fdel(self)
# finish to process afget
if finalafdel is not None:
finalafdel(self, name)
return _deleter
_deleter = _deleter(final_fdel, name)
_deleter.__doc__ = final_fdel.__doc__ # update doc
# get property name
doc = '{0} property.'.format(name)
propertyfield = property(
fget=_getter, fset=_setter, fdel=_deleter, doc=doc
)
# put property name in cls
setattr(cls, name, propertyfield)
return cls # finish to return the cls
return _addproperties