# Copyright (C) 2009, Tutorius.org # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA """ This module contains properties class that can be included in other types. TutoriusProperties have the same behaviour as python properties (assuming you also use the TPropContainer), with the added benefit of having builtin dialog prompts and constraint validation. """ import uuid from copy import copy, deepcopy from .constraints import Constraint, \ UpperLimitConstraint, LowerLimitConstraint, \ MaxSizeConstraint, MinSizeConstraint, \ ColorConstraint, FileConstraint, BooleanConstraint, EnumConstraint, \ ResourceConstraint, IntTypeConstraint, FloatTypeConstraint, \ StringTypeConstraint, ArrayTypeConstraint, BoolTypeConstraint, \ SequenceTypeConstraint, TypeConstraint, TypeConstraintError from .propwidgets import PropWidget, \ StringPropWidget, \ UAMPropWidget, \ EventTypePropWidget, \ IntPropWidget, \ FloatPropWidget, \ IntArrayPropWidget import logging LOGGER = logging.getLogger("properties") class TPropContainer(object): """ A class containing properties. This does the attribute wrapping between the container instance and the property value. As properties are on the containing classes, they allow static introspection of those types at the cost of needing a mapping between container instances, and property values. This is what TPropContainer does. """ def __init__(self, **kwargs): """ Prepares the instance for property value storage. This is done at object initialization, thus allowing initial mapping of properties declared on the class. Properties won't work correctly without this call. Keyword arguments will be evaluated as properties """ # create property value storage object.__setattr__(self, "_props", {}) for attr_name in dir(type(self)): propinstance = object.__getattribute__(self, attr_name) if isinstance(propinstance, TutoriusProperty): # only care about TutoriusProperty instances propinstance.tname = attr_name self._props[attr_name] = propinstance.validate( copy(propinstance.default)) self.__id = hash(uuid.uuid4()) # The differences dictionary. This is a structure that holds all the # modifications that were made to the properties since the action # was last installed or the last moment the notification was executed. # Every property change will be logged inside it and it will be sent # to the creator to update its action edition dialog. self._diff_dict = {} #Set attribute values that were supplied for key, value in kwargs.items(): setattr(self, key, value) def __getattribute__(self, name): """ Process the 'fake' read of properties in the appropriate instance container. Pass 'real' attributes as usual. """ try: props = object.__getattribute__(self, "_props") except AttributeError: # necessary for deepcopy as order of init can't be guaranteed object.__setattr__(self, "_props", {}) props = object.__getattribute__(self, "_props") try: # try gettin value from property storage # if it's not in the map, it's not a property or its default wasn't # set at initialization. return props[name] except KeyError: return object.__getattribute__(self, name) def __setattr__(self, name, value): """ Process the 'fake' write of properties in the appropriate instance container. Pass 'real' attributes as usual. @param name the name of the property @param value the value to assign to name @return the setted value """ props = object.__getattribute__(self, "_props") try: # We attempt to get the property object with __getattribute__ # to work through inheritance and benefit of the MRO. real_value = props.__setitem__(name, object.__getattribute__(self, name).validate(value)) LOGGER.debug("Action :: caching %s = %s in diff dict"%(name, str(value))) self._diff_dict[name] = value return real_value except AttributeError: return object.__setattr__(self, name, value) def replace_property(self, prop_name, new_prop): """ Changes the content of a property. This is done in order to support the insertion of executable properties in the place of a portable property. The typical exemple is that a resource property needs to be changed into a file property with the correct file name, since the installation location will be different on every platform. @param prop_name The name of the property to be changed @param new_prop The new property to insert @raise AttributeError of the mentionned property doesn't exist """ props = object.__getattribute__(self, "_props") props.__setitem__(prop_name, new_prop) def get_properties(self): """ Return the list of property names. """ return object.__getattribute__(self, "_props").keys() def get_properties_dict_copy(self): """ Return a deep copy of the dictionary of properties from that object. """ return deepcopy(self._props) def __hash__(self): """ Deprecated. Property containers should not be used as keys inside dictionary for the time being. Since containers are mutable, we should definitely use the addressing mechanism to refer to the containers. """ # Many places we use containers as keys to store additional data. # Since containers are mutable, there is a need for a hash function # where the result is constant, so we can still lookup old instances. return self.__id # Adding methods for pickling and unpickling an object with # properties def __getstate__(self): return dict(id=self.__id, props=self._props.copy()) def __setstate__(self, dict): self.__id = dict['id'] self._props.update(dict['props']) def __eq__(self, e2): return (isinstance(e2, type(self)) and self._props == e2._props) class TutoriusProperty(object): """ The base class for all actions' properties. The interface is the following : value : the value of the property type : the type of the property get_contraints() : the constraints inserted on this property. They define what is acceptable or not as values. """ widget_class = PropWidget def __init__(self): super(TutoriusProperty, self).__init__() self.type = None self._constraints = None self.default = None def get_constraints(self): """ Returns the list of constraints associated to this property. """ if self._constraints is None: self._constraints = [] for i in dir(self): typ = getattr(self, i) if isinstance(typ, Constraint): self._constraints.append(i) return self._constraints def validate(self, value): """ Validates the value of the property. If the value does not respect the constraints on the property, this method will raise an exception. The exception should be of the type related to the constraint that failed. E.g. When a int is to be set with a value that """ for constraint_name in self.get_constraints(): constraint = getattr(self, constraint_name) constraint.validate(value) return value class TAddonListProperty(TutoriusProperty): """ Stores an addon component list as a property. The purpose of this class is to allow correct mapping of properties through encapsulated hierarchies. """ pass class TIntProperty(TutoriusProperty): """ Represents an integer. Can have an upper value limit and/or a lower value limit. """ widget_class = IntPropWidget def __init__(self, value, lower_limit=None, upper_limit=None): TutoriusProperty.__init__(self) self.type = "int" self.upper_limit = UpperLimitConstraint(upper_limit) self.lower_limit = LowerLimitConstraint(lower_limit) self.int_type = IntTypeConstraint() self.default = self.validate(value) class TFloatProperty(TutoriusProperty): """ Represents a floating point number. Can have an upper value limit and/or a lower value limit. """ widget_class = FloatPropWidget def __init__(self, value, lower_limit=None, upper_limit=None): TutoriusProperty.__init__(self) self.type = "float" self.upper_limit = UpperLimitConstraint(upper_limit) self.lower_limit = LowerLimitConstraint(lower_limit) self.float_type = FloatTypeConstraint() self.default = self.validate(value) class TStringProperty(TutoriusProperty): """ Represents a string. Can have a maximum size limit. """ widget_class = StringPropWidget def __init__(self, value, size_limit=None, null=False): TutoriusProperty.__init__(self) self.type = "string" if size_limit: self.size_limit = MaxSizeConstraint(size_limit) if null: self.string_type = TypeConstraint((str, type(None))) else: self.string_type = StringTypeConstraint() self.default = self.validate(value) class TArrayProperty(TutoriusProperty): """ Represents an array of properties. Can have a maximum number of element limit, but there are no constraints on the content of the array. """ widget_class = IntArrayPropWidget def __init__(self, value, min_size_limit=None, max_size_limit=None): TutoriusProperty.__init__(self) self.type = "array" self.max_size_limit = MaxSizeConstraint(max_size_limit) self.min_size_limit = MinSizeConstraint(min_size_limit) self.array_type = ArrayTypeConstraint() self.default = tuple(self.validate(value)) #Make this thing hashable def __setstate__(self, state): self.max_size_limit = MaxSizeConstraint(state["max_size_limit"]) self.min_size_limit = MinSizeConstraint(state["min_size_limit"]) self.value = state["value"] def __getstate__(self): return dict( max_size_limit=self.max_size_limit.limit, min_size_limit=self.min_size_limit.limit, value=self.value, ) class TSequenceProperty(TutoriusProperty): """ Represents a data type that can be accessed with indices (via []). Those are mainly for structures that only expect a list of elements that may also be specified using a string. E.g. the strokes in a text type filter. Since it is more convenient to specify a list of keystrokes as a string rather than a list of characters, a sequence is the best option. """ def __init__(self, value, min_size_limit=None, max_size_limit=None): TutoriusProperty.__init__(self) self.type = "sequence" self.max_size_limit = MaxSizeConstraint(max_size_limit) self.min_size_limit = MinSizeConstraint(min_size_limit) self.sequence_type = SequenceTypeConstraint() self.default = tuple(self.validate(value)) class TColorProperty(TutoriusProperty): """ Represents a RGB color with 3 8-bit integer values. The value of the property is the array [R, G, B] """ def __init__(self, red=None, green=None, blue=None): TutoriusProperty.__init__(self) self.type = "color" self.array_type = ArrayTypeConstraint() self.color_constraint = ColorConstraint() self._red = red or 0 self._green = green or 0 self._blue = blue or 0 self.default = self.validate([self._red, self._green, self._blue]) class TFileProperty(TutoriusProperty): """ Represents a path to a file on the disk. """ def __init__(self, path): """ Defines the path to an existing file on disk file. For now, the path may be relative or absolute, as long as it exists on the local machine. TODO : Make sure that we have a file scheme that supports distribution on other computers (LP 355197) """ TutoriusProperty.__init__(self) self.type = "file" self.file_constraint = FileConstraint() self.default = self.validate(path) class TResourceProperty(TutoriusProperty): """ Represents a resource in the tutorial. A resource is a file with a specific name that exists under the tutorials folder. It is distributed alongside the tutorial itself. When the system encounters a resource, it knows that it refers to a file in the resource folder and that it should translate this resource name to an absolute file name before it is executed. E.g. An image is added to a tutorial in an action. On doing so, the creator adds a resource to the tutorial, then saves its name in the resource property of that action. When this tutorial is executed, the Engine replaces all the TResourceProperties inside the action by their equivalent TFileProperties with absolute paths, so that they can be used on any machine. """ def __init__(self, resource_name=""): """ Creates a new resource pointing to an existing resource. @param resource_name The file name of the resource (should be only the file name itself, no directory information) """ TutoriusProperty.__init__(self) self.type = "resource" self.resource_cons = ResourceConstraint() self.default = self.validate("") class TEnumProperty(TutoriusProperty): """ Represents a value in a given enumeration. This means that the value will always be one in the enumeration and nothing else. """ def __init__(self, value, accepted_values): """ Creates the enumeration property. @param value The initial value of the enum. Must be part of accepted_values @param accepted_values A list of values that the property can take """ TutoriusProperty.__init__(self) self.type = "enum" self.enum_constraint = EnumConstraint(accepted_values) self.default = self.validate(value) class TBooleanProperty(TutoriusProperty): """ Represents a True or False value. """ def __init__(self, value=False): TutoriusProperty.__init__(self) self.type = "boolean" self.bool_type = BoolTypeConstraint() self.boolean_constraint = BooleanConstraint() self.default = self.validate(value) class TUAMProperty(TutoriusProperty): """ Represents a widget of the interface by storing its UAM. """ widget_class = UAMPropWidget def __init__(self, value=None): TutoriusProperty.__init__(self) self.type = "uam" self.default = self.validate(value) class AddonTypeConstraint(TypeConstraint): def __init__(self): TypeConstraint.__init__(self, TPropContainer) class TAddonProperty(TutoriusProperty): """ Reprensents an embedded tutorius Addon Component (action, trigger, etc.) The purpose of this class is to flag the container for proper serialization, as the contained object can't be directly dumped to text for its attributes to be saved. """ class NullAction(TPropContainer): def do(self): pass def undo(self): pass def __init__(self): super(TAddonProperty, self).__init__() self.type = "addon" self.addon_type = AddonTypeConstraint() self.default = self.NullAction() class TEventType(TutoriusProperty): """ Represents an GUI signal for a widget. """ widget_class = EventTypePropWidget def __init__(self, value): super(TEventType, self).__init__() self.type = "gtk-signal" self.default = self.validate(value) class ListOfAddonTypeConstraint(TypeConstraint): """ Ensures that the value is a list of Addons. """ def __init__(self): TypeConstraint.__init__(self, 'list') def validate(self, value): value = TypeConstraint.validate(self, value) for component in value: if not (isinstance(component, TPropContainer)): raise TypeConstraintError("Expected a list of TPropContainer instances inside ListOfAddonTypeConstraint, got a %s" % (str(type(component)))) return value class TAddonListProperty(TutoriusProperty): """ Reprensents an embedded tutorius Addon List Component. See TAddonProperty """ def __init__(self): TutoriusProperty.__init__(self) self.type = "addonlist" self.list_of_addon_type = ListOfAddonTypeConstraint() self.default = []