# 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. """ from sugar.tutorius.constraints import Constraint, \ UpperLimitConstraint, LowerLimitConstraint, \ MaxSizeConstraint, MinSizeConstraint, \ ColorConstraint, FileConstraint, BooleanConstraint, EnumConstraint from copy import copy 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): """ 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. """ # 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)) 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. return props.__setitem__(name, object.__getattribute__(self, name).validate(value)) except AttributeError: return object.__setattr__(self, name, value) def get_properties(self): """ Return the list of property names. """ return object.__getattribute__(self, "_props").keys() def __eq__(self, otherContainer): """ Compare this property container to the other one and returns True only if the every property of the first one can be found in the other container, with the same name and the same value. @param otherContainer The other container that we wish to test for equality. @returns True if every property in the first container can be found with the same value and the same name in the second container. """ # Make sure both have the same number of properties if len(self._props) != len(otherContainer._props): return False if not(type(self) == type(otherContainer)): return False # For every property in this container for prop in self._props.keys(): found = False # Try to match it with another property present in the other container for otherProp in otherContainer._props.keys(): # If we were able to match the name, then we look up the value if prop == otherProp: this_type = getattr(type(self), prop).type other_type = getattr(type(otherContainer), prop).type if this_type != other_type: return False # If this is an addon list, then we need to make sure that # every element of the list is also present in the other list if this_type == "addonlist": if not self._are_lists_identical(self._props[prop], otherContainer._props[prop]): return False found = True break # If this is just an embedded / decorated container, then we want to # make sure the sub component are identical. elif this_type == "addon": if not (self._props[prop] == otherContainer._props[prop]): return False found = True break else: if self._props[prop] == otherContainer._props[prop]: found = True break # If we arrive here, then we couldn't find any property in the second # container that matched the current one. We know that the two containers are # not equal. if found == False: return False return True def _are_lists_identical(self, myList, otherList): """ Compares two lists of property containers to see if they are identical ( they have the same properties @param myList The first list of properties containers @param otherList The second list of properties containers @return True if all of the properties inside the list are identical. False otherwise. """ # For each property in the first list, for container in myList: found = False # Attempt to match it with every property in the other list for other_container in otherList: # If the containers are identical, if container == other_container: # We found a matching container. We don't need to search in the # second list anymore, so we break found = True break # In the case the property was not found inside the second list if found == False: # We know right away that the two lists are not identical return False # If we were able to match each property in the first list, then we # can say the lists are equal. return True 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. """ 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. """ 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.default = self.validate(value) class TFloatProperty(TutoriusProperty): """ Represents a floating point number. Can have an upper value limit and/or a lower value limit. """ 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.default = self.validate(value) class TStringProperty(TutoriusProperty): """ Represents a string. Can have a maximum size limit. """ def __init__(self, value, size_limit=None): TutoriusProperty.__init__(self) self.type = "string" self.size_limit = MaxSizeConstraint(size_limit) 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. """ 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.default = 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.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 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.boolean_constraint = BooleanConstraint() self.default = self.validate(value) class TUAMProperty(TutoriusProperty): """ Represents a widget of the interface by storing its UAM. """ # TODO : Pending UAM check-in (LP 355199) pass 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.default = self.NullAction() def validate(self, value): if isinstance(value, TPropContainer): return super(TAddonProperty, self).validate(value) raise ValueError("Expected TPropContainer instance as TaddonProperty value") class TAddonListProperty(TutoriusProperty): """ Reprensents an embedded tutorius Addon List Component. See TAddonProperty """ def __init__(self): TutoriusProperty.__init__(self) self.type = "addonlist" self.default = [] def validate(self, value): if isinstance(value, list): for component in value: if not (isinstance(component, TPropContainer)): raise ValueError("Expected a list of TPropContainer instances inside TAddonListProperty value, got a %s" % (str(type(component)))) return value raise ValueError("Value proposed to TAddonListProperty is not a list")