Source code for mutaprops.mutaprops

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from enum import Enum
import logging
import types
from collections import OrderedDict
from .utils import MutaPropError, rest_to_html

logger = logging.getLogger(__name__)


[docs]class MutaTypes(Enum): """ Representation of allowed MutaProperty types. """ STRING = 0 INT = 1 REAL = 2 BOOL = 3 HTML = 4 @classmethod def typecast(cls, muta_type, string_value): """ Cast string representation of the value to the particular python type. """ if muta_type == cls.STRING: return string_value elif muta_type == cls.INT: return int(string_value) elif muta_type == cls.REAL: return float(string_value) elif muta_type == cls.BOOL: return bool(string_value.lower() == 'true') elif muta_type == cls.HTML: return string_value else: raise MutaPropError("Unknown value type {0}".format(muta_type))
[docs]class MutaProp(object): """ Abstract class defining a generic MutaProp object. Such object holds basic information about a "property" of a MutaProp-accessible class (such as ID and human-readable name) as well as position of such property in the hierarchy of all properties. Each MutaProp implementation is expected to overload following: * :meth:`~mutaprops.MutaProp._allowed_kwargs` class method, defining kwargs which are used/allowed in the constructor. * :meth:`~mutaprops.MutaProp._exported_kwargs` class method, i defining which parameters are exported during serialization of the MutaProp. * :const:`~mutaprops.MutaProp.MP_CLASS_TYPE` constant defining the MutaProp class for GUI use (the utilization of this parameter is GUI-implementation-dependent). """ __definition_counter = 0 MP_ID = 'id' # ID of the prop (usually matches the property name) MP_NAME = 'name' # Human-readable name MP_PRIORITY = 'priority' # GUI display priority MP_HIERARCHY = 'hierarchy' # Hierarchy position of a prop MP_DEFINITION_ORDER = 'deford' # Definition order for default ordering MP_DOC = 'doc' # Prop's docstring MP_VIEW = 'view' # Alternative view widget assignment where applicable MP_TYPE = 'type' # Type of the Prop (Property/Action/Source) MP_CLASS_TYPE = 'abstract' # I'm using classmethods instead of class constants because of easier # inheritance @classmethod def _allowed_kwargs(cls): """ Define kwargs which are allowed in the constructor.""" return cls.MP_PRIORITY, cls.MP_HIERARCHY, cls.MP_DEFINITION_ORDER, \ cls.MP_DOC, cls.MP_VIEW @classmethod def _exported_params(cls): """ Define which parameters are exported/serialized to the GUI""" return (cls.MP_ID, cls.MP_NAME, cls.MP_PRIORITY, cls.MP_HIERARCHY, cls.MP_DEFINITION_ORDER, cls.MP_DOC, cls.MP_VIEW, cls.MP_TYPE) def __init__(self, pid, display_name, **kwargs): """ :param pid: Mutaprop identifier :param display_name: Mutaprop name to be displayed in GUI :param kwargs: Optional attibutes * `priority` : int Display priority, higher numbers are displayed first * `hierarchy` : string Hierarchy path in the GUI * `deford` : int Modifies definition order. This is normally defined automatically based on the decorator calls. If priority is not used, the order at which MutaProps are listed in GUI is defined by deford. * `view` : string identifier of recommended GUI view type """ self._muta_id = pid self._muta_name = display_name # Check for invalid kwargs for key in kwargs.keys(): if key not in self._allowed_kwargs(): raise MutaPropError("Invalid argument {0}".format(key)) # Assign with defaults self._muta_priority = kwargs.get(self.MP_PRIORITY, None) self._muta_hierarchy = kwargs.get(self.MP_HIERARCHY, None) self._muta_view = kwargs.get(self.MP_VIEW, None) self._muta_deford = kwargs.get(self.MP_DEFINITION_ORDER, None) if self._muta_deford is None: self._muta_deford = MutaProp.__definition_counter MutaProp.__definition_counter += 1 self.__doc__ = kwargs.get(self.MP_DOC, None) def _assign_kwarg(self, kwarg_key, kwarg_value): """ Converts :param kwarg_key: :param kwarg_value: :return: """ if kwarg_key in self._allowed_kwargs(): if kwarg_key == 'doc': self.__doc__ = kwarg_value else: setattr(self, "_muta_{0}".format(kwarg_key), kwarg_value) else: raise MutaPropError("Invalid keyword {0}".format(kwarg_key)) @property def prop_id(self): return self._muta_id @property def display_name(self): return self._muta_name @property def display_priority(self): return self._muta_priority @property def hierarchy(self): return self._muta_hierarchy @property def view(self): return self._muta_view @property def definition_order(self): return self._muta_deford def __str__(self): temp = ( "ID: {pid}: {name}\n" + "order: {deford}, priority: {priority}, hierarchy: {hierarchy}\n" + "Description: {doc}").format(pid=self._muta_id, name=self._muta_name, deford=self._muta_deford, priority=self._muta_priority, hierarchy=self._muta_hierarchy, doc=self.__doc__) return temp
[docs] def to_dict(self, obj=None): temp = {} for attr in self._exported_params(): if attr == self.MP_DOC: # Docstring is here converted from reST to HTML temp[self.MP_DOC] = rest_to_html(self.__doc__) elif attr == self.MP_TYPE: temp[self.MP_TYPE] = self.MP_CLASS_TYPE else: temp[attr] = MutaSource.serialize( getattr(self, '_muta_{0}'.format(attr))) return temp
[docs]class MutaProperty(MutaProp): """Emulate PyProperty_Type() in Objects/descrobject.c""" # Value limits and step MP_MAXVAL = 'max_val' MP_MINVAL = 'min_val' MP_STEP = 'step' MP_FGET = 'fget' # Getter function MP_FSET = 'fset' # Setter function MP_FDEL = 'fdel' # Deleter function MP_CHANGE_CALLBACK = 'change_callback' # Callback called on change MP_VALUE = 'value' # Value of the MutaProperty MP_VALUE_TYPE = 'value_type' # Type of the value (INT/BOOL...) MP_READ_ONLY = 'read_only' # GUI Read only setting MP_SELECT = 'select' # Iterable set of possible values MP_TOGGLE = 'toggle' # GUI setting for toggle-switch representation MP_CLASS_TYPE = 'property' @classmethod def _allowed_kwargs(cls): return super()._allowed_kwargs() + (cls.MP_MAXVAL, cls.MP_MINVAL, cls.MP_STEP, cls.MP_FGET, cls.MP_FSET, cls.MP_FDEL, cls.MP_CHANGE_CALLBACK, cls.MP_SELECT, cls.MP_TOGGLE, cls.MP_READ_ONLY) @classmethod def _exported_params(cls): return super()._exported_params() + (cls.MP_MINVAL, cls.MP_MAXVAL, cls.MP_STEP, cls.MP_READ_ONLY, cls.MP_VALUE_TYPE, cls.MP_SELECT, cls.MP_TOGGLE) def __init__(self, pid, display_name, value_type, **kwargs): """ :param pid: Mutaprop identifier :param display_name: Mutaprop name to be displayed in GUI :param value_type: MutaType value :param kwargs: Optional attibutes * `priority` : int Display priority, higher numbers are displayed first * `hierarchy` : string Hierarchy path in the GUI * `deford` : int Modifies definition order. This is normally defined automatically based on the decorator calls. If priority is not used, the order at which MutaProps are listed in GUI is defined by deford. * `view` : string identifier of recommended GUI view type * `min_val` : int For numerical type, minimum possible value, for string type, minimal length of string * `max_val` : int For numerical type, maximum possible value, for string type, maximal length of string * `step` : int For numerical type, step interval between values, for other types ignored * `fget` : function Getter function * `fset` : function Setter function * `fdel` : function Deleter function * `read_only` : bool Sets GUI element to read-only state. Automatically set to true when setter function is not provided. * `select` : List or Dict Set of allowed values. GUI will offer just this set. * `toggle` : Dict of format {'on': 'Some-on-label', 'off': 'Some-off-label'} If set, a toggle-switch like control will be used as GUI. Valid only for BOOL types, otherwise ignored. """ doc = kwargs.get(self.MP_DOC, None) fget = kwargs.get(self.MP_FGET, None) if doc is None and fget is not None: kwargs[self.MP_DOC] = fget.__doc__ super().__init__(pid, display_name, **kwargs) self._muta_value_type = value_type self._muta_min_val = kwargs.get(self.MP_MINVAL, None) self._muta_max_val = kwargs.get(self.MP_MAXVAL, None) self._muta_step = kwargs.get(self.MP_STEP, None) self._muta_fget = kwargs.get(self.MP_FGET, None) self._muta_fset = kwargs.get(self.MP_FSET, None) self._muta_fdel = kwargs.get(self.MP_FDEL, None) self._muta_change_callback = kwargs.get(self.MP_CHANGE_CALLBACK, None) self._muta_select = kwargs.get(self.MP_SELECT, {}) self._muta_read_only = kwargs.get(self.MP_READ_ONLY, False) # logger.debug("Initializing mutaprop %s with selector %s" % (pid, temp_select)) # # TODO: rewrite to mutasource # if isinstance(temp_select, SelectSource): # self._muta_select = temp_select # else: # self._muta_select = SelectSource(temp_select) self._muta_toggle = kwargs.get(self.MP_TOGGLE, None) @property def value_type(self): return self._muta_value_type def _get_kwargs(self): temp = {} for kwarg in self._allowed_kwargs(): if kwarg == self.MP_DOC: temp[kwarg] = self.__doc__ else: temp[kwarg] = getattr(self, '_muta_{0}'.format(kwarg)) return temp def __get__(self, obj, objtype=None): if obj is None: return self if self._muta_fget is None: raise MutaPropError("No getter defined.") logger.debug("Getting value for %s", self._muta_name) return self._muta_fget(obj) def __set__(self, obj, value): if self._muta_fset is None: raise MutaPropError("No setter defined.") different = (self._muta_fget(obj) != value) self._muta_fset(obj, value) # Notify of property change try: if different and self._muta_change_callback \ and hasattr(obj, 'muta_id'): logger.debug("Notification of set call for %s on %s", self._muta_name, obj.muta_id) self._muta_change_callback(obj.muta_id, self._muta_id, value) except AttributeError: raise Warning("Property change called on unitialized object.") def __delete__(self, obj): if self._muta_fdel is None: raise MutaPropError("No deleter defined.") self._muta_fdel(obj) def __str__(self): temp = ( super().__str__() + "\nProperty {ro} [{valtyp}] ({minval}, {maxval}, {step}, {select})" .format( valtyp=self._muta_value_type, minval=self._muta_min_val, maxval=self._muta_max_val, step=self._muta_step, select=self._muta_select, ro='[Read Only]' if not self.is_writeable() else '')) return temp
[docs] def getter(self, fget): """ Decorator function for constructing MutaProperty on getter function. Takes all ``kwargs`` from :meth:`~mutaprops.MutaProperty.__init__` """ logger.debug("{0}: Getter set".format(self._muta_id)) temp_kwargs = self._get_kwargs() temp_kwargs[self.MP_FGET] = fget return type(self)(self._muta_id, self._muta_name, self._muta_value_type, **temp_kwargs)
[docs] def setter(self, func=None, min_val=None, max_val=None, step=None, select={}): """ Decorator function usable in two ways: * decorator without arguments:: @some_metaprop.setter def some_metaprop(self, value): pass * decorator with arguments:: @some_metaprop.setter(min_value=1.0, max_value=2.0) def some_metaprop(self, value): pass :param func: Is only for internal decorator use, don't use it :param min_val: Min. value for numeric types, min. lenght for Strings :param max_val: Max. value for numeric types, max. length for Strings :param step: Step increment for numeric types :param select: Selector object to provide user select. A selector can be either a dict, or list of (label, value) tuples, or another MutaProperty or MutaSource which provides dict or list of tuples. In such case, the selector list will be updated during runtime. :returns: MutaProp object """ temp_kwargs = self._get_kwargs() temp_kwargs[self.MP_MINVAL] = min_val or self._muta_min_val temp_kwargs[self.MP_MAXVAL] = max_val or self._muta_max_val temp_kwargs[self.MP_STEP] = step or self._muta_step temp_kwargs[self.MP_SELECT] = select or self._muta_select if func: logger.debug("{0}: Setter set".format(self._muta_id)) temp_kwargs[self.MP_FSET] = func return type(self)(self._muta_id, self._muta_name, self._muta_value_type, **temp_kwargs) else: def decorator(fset): logger.debug("{0}: Setter set".format(self._muta_id)) temp_kwargs[self.MP_FSET] = fset return type(self)(self._muta_id, self._muta_name, self._muta_value_type, **temp_kwargs) return decorator
[docs] def deleter(self, fdel): logger.debug("{0}: Deleter set".format(self._muta_id)) temp_kwargs = self._get_kwargs() temp_kwargs[self.MP_FDEL] = fdel return type(self)(self._muta_id, self._muta_name, self._muta_value_type, **temp_kwargs)
[docs] def register_change_callback(self, callback): self._muta_change_callback = callback
[docs] def to_dict(self, obj=None): temp = super().to_dict() # Some specific/derived properties follows # Override the user setting for read_only if setter is not provided if not self.is_writeable(): temp[self.MP_READ_ONLY] = True if obj: temp[self.MP_VALUE] = self.__get__(obj) temp[self.MP_VALUE_TYPE] = self._muta_value_type.name # Remove toggle parameter for non-bool items if self._muta_value_type != MutaTypes.BOOL: temp.pop(self.MP_TOGGLE) return temp
[docs] def muta_set(self, obj, value): # TODO: Validation! if self._muta_fget(obj) != value: logger.debug("Set remotely to %s", str(value)) self._muta_fset(obj, value)
[docs] def is_writeable(self): """ Returns true if only getter is defined. Warning: doesn't reflect the read_only kwarg! """ return self._muta_fset is not None
[docs]class MutaSource(MutaProperty): """ MutaSource is generalized MutaProperty, which is not visible in the GUI, however it's changes are reflected in the GUI. MutaSource does not need to define display name and value type - any serializable type goes. MutaSource can also be a class-property. MutaSources cannot be directly changed from the GUI layer, however they can be changed indirectly from the model/MutaObject itself. **Implementation Note** In theory :class:`~mutaprops.mutaprops.MutaProperty` should be child of :class:`~mutaprops.mutaprops.MutaSource`, in practice the differences are of such character it doesn't make it more convenient to implement :class:`~mutaprops.mutaprops.MutaSource` as child of :class:`~mutaprops.mutaprops.MutaProperty`. """ MP_CLASS_TYPE = 'source' MP_CLASS_SCOPE = 'class_scope' MP_OWNER_CLASS = 'owner_class' @classmethod def _allowed_kwargs(cls): return cls.MP_DOC, cls.MP_CLASS_SCOPE, cls.MP_FGET, cls.MP_FSET, \ cls.MP_FDEL, cls.MP_CHANGE_CALLBACK, cls.MP_OWNER_CLASS @classmethod def _exported_params(cls): return cls.MP_ID, cls.MP_DOC, cls.MP_TYPE, cls.MP_CLASS_SCOPE
[docs] @classmethod def serialize(cls, value): if isinstance(value, MutaProperty): return {cls.MP_TYPE: value.MP_CLASS_TYPE, cls.MP_ID: value._muta_id} else: return value
def __init__(self, pid, display_name, value_type, **kwargs): """ Init signature is kept for convenience, however `display_name` and `value_type` are ignored. :param kwargs: Optional attibutes * `fget` : function Getter function * `fset` : function Setter function * `fdel` : function Deleter function * `class_scope` : bool Set to true if the exposed property is a property of a class. """ doc = kwargs.get(self.MP_DOC, None) fget = kwargs.get(self.MP_FGET, None) if doc is None and fget is not None: kwargs[self.MP_DOC] = fget.__doc__ MutaProp.__init__(self, pid, pid, **kwargs) self._muta_fget = kwargs.get(self.MP_FGET, None) self._muta_fset = kwargs.get(self.MP_FSET, None) self._muta_fdel = kwargs.get(self.MP_FDEL, None) self._muta_change_callback = kwargs.get(self.MP_CHANGE_CALLBACK, None) self._muta_class_scope = kwargs.get(self.MP_CLASS_SCOPE, False) self._muta_owner_class = kwargs.get(self.MP_OWNER_CLASS, None) self._muta_value_type = None @property def class_scoped(self): """ :return: True if the MutaSource is classproperty. """ return self._muta_class_scope @property def owner_class(self): """ In case of classproperty source, returns the owner class.""" return self._muta_owner_class
[docs] def set_owner_class(self, defining_class): self._muta_owner_class = defining_class
# Overload to disable
[docs] def muta_set(self, obj, value): raise MutaPropError("Source cannot be set frou GUI layer.")
def __str__(self): return "MutaSource ID: {pid}, Description: {doc}".format( pid=self._muta_id, doc=self.__doc__ ) def __call__(self, value): if self._muta_class_scope: self.__set__(None, value) def __get__(self, obj, objtype=None): if obj is None: if self._muta_class_scope: obj = objtype else: raise MutaPropError("Object not specified.") if self._muta_fget is None: raise MutaPropError("No getter defined.") logger.debug("Getting value for %s", self._muta_name) return self._muta_fget(obj)
[docs] def setter(self, func): """ Get setter method. :param func: :return: MutaSource object. """ temp_kwargs = self._get_kwargs() logger.debug("{0}: Setter set".format(self._muta_id)) temp_kwargs[self.MP_FSET] = func return type(self)(self._muta_id, self._muta_name, self._muta_value_type, **temp_kwargs)
[docs] def setter_classproperty(self, func): if not self._muta_class_scope: raise MutaPropError("Initializing class property setter" + " for property without class scope.") def class_scoped_setter(cls, value): different = self._muta_fget(cls) != value func(cls, value) # Notify of property change if different and self._muta_change_callback: logger.debug("Notification of set on mutaselect on %s", id(cls)) self._muta_change_callback(cls._orig_cls.__name__, self._muta_id, value) return classmethod(class_scoped_setter)
[docs] def to_dict(self, obj=None): temp = MutaProp.to_dict(self) logger.debug("Serializing mutasource: {0}".format(temp)) if obj: temp[self.MP_VALUE] = self.__get__(obj) return temp
[docs]class MutaAction(MutaProp): MP_CLASS_TYPE = 'action' MP_READ_ONLY = 'read_only' # GUI Read only setting @classmethod def _allowed_kwargs(cls): return super()._allowed_kwargs() + (cls.MP_READ_ONLY,) @classmethod def _exported_params(cls): return super()._exported_params() + (cls.MP_READ_ONLY,) def __init__(self, pid, display_name, callback, **kwargs): doc = kwargs.get(self.MP_DOC, None) if doc is None: kwargs[self.MP_DOC] = callback.__doc__ super().__init__(pid, display_name, **kwargs) self._muta_read_only = kwargs.get(self.MP_READ_ONLY, False) self._callback = callback
[docs] def muta_call(self, obj): if not hasattr(obj, '_muta_obj_id'): raise MutaPropError("Executing action on uninitialized MutaObject.") logger.debug("%s: External execution call on %s", self._muta_id, obj._muta_obj_id) self.__call__(obj)
def __call__(self, obj): if self._callback is None: raise MutaPropError("No callback is defined.") self._callback(obj) # It's necessary to use non-data descriptor to make this callable class # capable of binding a method # https://docs.python.org/3.5/howto/descriptor.html#functions-and-methods # http://stackoverflow.com/questions/972/adding-a-method-to-an-existing-object-instance # http://stackoverflow.com/questions/26226604/decorating-a-class-function-with-a-callable-instance def __get__(self, obj, objtype=None): return types.MethodType(self, obj)
[docs]class MutaPropClass(object): MP_OBJ_ID = 'obj_id' MP_CLASS_ID = 'class_id' MP_PROPS = 'props' MP_NAME = 'name' MP_GUI_ID = 'gui_id' MP_GUI_MAJOR_VERSION = 'gui_major_version' MP_GUI_MINOR_VERSION = 'gui_minor_version' MP_DOC = 'doc' @classmethod def _exported_params(cls): return (cls.MP_OBJ_ID, cls.MP_NAME, cls.MP_PROPS, cls.MP_GUI_MAJOR_VERSION, cls.MP_GUI_MINOR_VERSION, cls.MP_DOC, cls.MP_CLASS_ID)
[docs] def update_props(self, change_callback=None): """Because this is potentially heavy operation and property definitions are not likely to be changed during objects lifetime, it's easier to cache it. """ temp = [] for basecls in type(self).mro(): for prop, value in basecls.__dict__.items(): if isinstance(value, MutaProp): logger.debug("Adding mutaprop: {0}".format(value.prop_id)) temp.append(value) if isinstance(value, MutaProperty): value.register_change_callback(change_callback) if isinstance(value, MutaSource): if value.class_scoped: value.set_owner_class(self.__class__) temp.sort(key=lambda x: x.definition_order) setattr(self, self.muta_attr(self.MP_PROPS), OrderedDict([(prop.prop_id, prop) for prop in temp]))
@property def props(self): return getattr(self, self.muta_attr(self.MP_PROPS)) @property def muta_id(self): return getattr(self, self.muta_attr(self.MP_OBJ_ID)) # Normal properties cannot be used on class variables, so going with get_*
[docs] @classmethod def get_class_name(cls): return getattr(cls, cls.muta_attr(cls.MP_NAME))
[docs] @classmethod def get_gui_version(cls): return (getattr(cls, cls.muta_attr(cls.MP_GUI_MAJOR_VERSION)), getattr(cls, cls.muta_attr(cls.MP_GUI_MINOR_VERSION)))
[docs] @classmethod def get_gui_id(cls): return getattr(cls, cls.muta_attr(cls.MP_GUI_ID))
[docs] @classmethod def muta_attr(cls, attr): return '_muta_{0}'.format(attr)
[docs] def muta_init(self, object_id, change_callback=None): self.update_props(change_callback) setattr(self, self.muta_attr(self.MP_OBJ_ID), object_id)
[docs] def muta_unregister(self): self.update_props(change_callback=None)
[docs] def is_muta_ready(self): if (hasattr(self, self.muta_attr(self.MP_OBJ_ID)) and (self.muta_id is not None)): return True else: return False
[docs] def to_dict(self): temp = {} for attr in self._exported_params(): if attr == self.MP_DOC: temp[self.MP_DOC] = rest_to_html(self.__doc__) elif attr == self.MP_CLASS_ID: temp[self.MP_CLASS_ID] = self._orig_cls.__name__ else: attr_value = getattr(self, self.muta_attr(attr)) if attr == self.MP_PROPS: attr_value = [prop.to_dict(obj=self) for prop in attr_value.values()] temp[attr] = attr_value return temp