diff --git a/modules/extensions_framework/__init__.py b/modules/extensions_framework/__init__.py index df815eb3cb7a2671dffd1803f8c14cc8cf154ed2..af909b45ad13ad0bee7254d52a8fea364fd2dd4e 100644 --- a/modules/extensions_framework/__init__.py +++ b/modules/extensions_framework/__init__.py @@ -33,339 +33,339 @@ bpy.utils.register_class(EF_OT_msg) del EF_OT_msg def log(str, popup=False, module_name='EF'): - """Print a message to the console, prefixed with the module_name - and the current time. If the popup flag is True, the message will - be raised in the UI as a warning using the operator bpy.ops.ef.msg. - - """ - print("[%s %s] %s" % - (module_name, time.strftime('%Y-%b-%d %H:%M:%S'), str)) - if popup: - bpy.ops.ef.msg( - msg_type='WARNING', - msg_text=str - ) + """Print a message to the console, prefixed with the module_name + and the current time. If the popup flag is True, the message will + be raised in the UI as a warning using the operator bpy.ops.ef.msg. + + """ + print("[%s %s] %s" % + (module_name, time.strftime('%Y-%b-%d %H:%M:%S'), str)) + if popup: + bpy.ops.ef.msg( + msg_type='WARNING', + msg_text=str + ) added_property_cache = {} def init_properties(obj, props, cache=True): - """Initialise custom properties in the given object or type. - The props list is described in the declarative_property_group - class definition. If the cache flag is False, this function - will attempt to redefine properties even if they have already been - added. - - """ - - if not obj in added_property_cache.keys(): - added_property_cache[obj] = [] - - for prop in props: - try: - if cache and prop['attr'] in added_property_cache[obj]: - continue - - if prop['type'] == 'bool': - t = bpy.props.BoolProperty - a = {k: v for k,v in prop.items() if k in ["name", - "description","default","options","subtype","update"]} - elif prop['type'] == 'bool_vector': - t = bpy.props.BoolVectorProperty - a = {k: v for k,v in prop.items() if k in ["name", - "description","default","options","subtype","size", - "update"]} - elif prop['type'] == 'collection': - t = bpy.props.CollectionProperty - a = {k: v for k,v in prop.items() if k in ["ptype","name", - "description","default","options"]} - a['type'] = a['ptype'] - del a['ptype'] - elif prop['type'] == 'enum': - t = bpy.props.EnumProperty - a = {k: v for k,v in prop.items() if k in ["items","name", - "description","default","options","update"]} - elif prop['type'] == 'float': - t = bpy.props.FloatProperty - a = {k: v for k,v in prop.items() if k in ["name", - "description","default","min","max","soft_min","soft_max", - "step","precision","options","subtype","unit","update"]} - elif prop['type'] == 'float_vector': - t = bpy.props.FloatVectorProperty - a = {k: v for k,v in prop.items() if k in ["name", - "description","default","min","max","soft_min","soft_max", - "step","precision","options","subtype","size","update"]} - elif prop['type'] == 'int': - t = bpy.props.IntProperty - a = {k: v for k,v in prop.items() if k in ["name", - "description","default","min","max","soft_min","soft_max", - "step","options","subtype","update"]} - elif prop['type'] == 'int_vector': - t = bpy.props.IntVectorProperty - a = {k: v for k,v in prop.items() if k in ["name", - "description","default","min","max","soft_min","soft_max", - "options","subtype","size","update"]} - elif prop['type'] == 'pointer': - t = bpy.props.PointerProperty - a = {k: v for k,v in prop.items() if k in ["ptype", "name", - "description","options","update"]} - a['type'] = a['ptype'] - del a['ptype'] - elif prop['type'] == 'string': - t = bpy.props.StringProperty - a = {k: v for k,v in prop.items() if k in ["name", - "description","default","maxlen","options","subtype", - "update"]} - else: - continue - - setattr(obj, prop['attr'], t(**a)) - - added_property_cache[obj].append(prop['attr']) - except KeyError: - # Silently skip invalid entries in props - continue + """Initialise custom properties in the given object or type. + The props list is described in the declarative_property_group + class definition. If the cache flag is False, this function + will attempt to redefine properties even if they have already been + added. + + """ + + if not obj in added_property_cache.keys(): + added_property_cache[obj] = [] + + for prop in props: + try: + if cache and prop['attr'] in added_property_cache[obj]: + continue + + if prop['type'] == 'bool': + t = bpy.props.BoolProperty + a = {k: v for k,v in prop.items() if k in ["name", + "description","default","options","subtype","update"]} + elif prop['type'] == 'bool_vector': + t = bpy.props.BoolVectorProperty + a = {k: v for k,v in prop.items() if k in ["name", + "description","default","options","subtype","size", + "update"]} + elif prop['type'] == 'collection': + t = bpy.props.CollectionProperty + a = {k: v for k,v in prop.items() if k in ["ptype","name", + "description","default","options"]} + a['type'] = a['ptype'] + del a['ptype'] + elif prop['type'] == 'enum': + t = bpy.props.EnumProperty + a = {k: v for k,v in prop.items() if k in ["items","name", + "description","default","options","update"]} + elif prop['type'] == 'float': + t = bpy.props.FloatProperty + a = {k: v for k,v in prop.items() if k in ["name", + "description","default","min","max","soft_min","soft_max", + "step","precision","options","subtype","unit","update"]} + elif prop['type'] == 'float_vector': + t = bpy.props.FloatVectorProperty + a = {k: v for k,v in prop.items() if k in ["name", + "description","default","min","max","soft_min","soft_max", + "step","precision","options","subtype","size","update"]} + elif prop['type'] == 'int': + t = bpy.props.IntProperty + a = {k: v for k,v in prop.items() if k in ["name", + "description","default","min","max","soft_min","soft_max", + "step","options","subtype","update"]} + elif prop['type'] == 'int_vector': + t = bpy.props.IntVectorProperty + a = {k: v for k,v in prop.items() if k in ["name", + "description","default","min","max","soft_min","soft_max", + "options","subtype","size","update"]} + elif prop['type'] == 'pointer': + t = bpy.props.PointerProperty + a = {k: v for k,v in prop.items() if k in ["ptype", "name", + "description","options","update"]} + a['type'] = a['ptype'] + del a['ptype'] + elif prop['type'] == 'string': + t = bpy.props.StringProperty + a = {k: v for k,v in prop.items() if k in ["name", + "description","default","maxlen","options","subtype", + "update"]} + else: + continue + + setattr(obj, prop['attr'], t(**a)) + + added_property_cache[obj].append(prop['attr']) + except KeyError: + # Silently skip invalid entries in props + continue class declarative_property_group(bpy.types.PropertyGroup): - """A declarative_property_group describes a set of logically - related properties, using a declarative style to list each - property type, name, values, and other relevant information. - The information provided for each property depends on the - property's type. - - The properties list attribute in this class describes the - properties present in this group. - - Some additional information about the properties in this group - can be specified, so that a UI can be generated to display them. - To that end, the controls list attribute and the visibility dict - attribute are present here, to be read and interpreted by a - property_group_renderer object. - See extensions_framework.ui.property_group_renderer. - - """ - - ef_initialised = False - - """This property tells extensions_framework which bpy.type(s) - to attach this PropertyGroup to. If left as an empty list, - it will not be attached to any type, but its properties will - still be initialised. The type(s) given in the list should be - a string, such as 'Scene'. - - """ - ef_attach_to = [] - - @classmethod - def initialise_properties(cls): - """This is a function that should be called on - sub-classes of declarative_property_group in order - to ensure that they are initialised when the addon - is loaded. - the init_properties is called without caching here, - as it is assumed that any addon calling this function - will also call ef_remove_properties when it is - unregistered. - - """ - - if not cls.ef_initialised: - for property_group_parent in cls.ef_attach_to: - if property_group_parent is not None: - prototype = getattr(bpy.types, property_group_parent) - if not hasattr(prototype, cls.__name__): - init_properties(prototype, [{ - 'type': 'pointer', - 'attr': cls.__name__, - 'ptype': cls, - 'name': cls.__name__, - 'description': cls.__name__ - }], cache=False) - - init_properties(cls, cls.properties, cache=False) - cls.ef_initialised = True - - return cls - - @classmethod - def register_initialise_properties(cls): - """As ef_initialise_properties, but also registers the - class with RNA. Note that this isn't a great idea - because it's non-trivial to unregister the class, unless - you keep track of it yourself. - """ - - bpy.utils.register_class(cls) - cls.initialise_properties() - return cls - - @classmethod - def remove_properties(cls): - """This is a function that should be called on - sub-classes of declarative_property_group in order - to ensure that they are un-initialised when the addon - is unloaded. - - """ - - if cls.ef_initialised: - prototype = getattr(bpy.types, cls.__name__) - for prop in cls.properties: - if hasattr(prototype, prop['attr']): - delattr(prototype, prop['attr']) - - for property_group_parent in cls.ef_attach_to: - if property_group_parent is not None: - prototype = getattr(bpy.types, property_group_parent) - if hasattr(prototype, cls.__name__): - delattr(prototype, cls.__name__) - - cls.ef_initialised = False - - return cls - - - """This list controls the order of property layout when rendered - by a property_group_renderer. This can be a nested list, where each - list becomes a row in the panel layout. Nesting may be to any depth. - - """ - controls = [] - - """The visibility dict controls the visibility of properties based on - the value of other properties. See extensions_framework.validate - for test syntax. - - """ - visibility = {} - - """The enabled dict controls the enabled state of properties based on - the value of other properties. See extensions_framework.validate - for test syntax. - - """ - enabled = {} - - """The alert dict controls the alert state of properties based on - the value of other properties. See extensions_framework.validate - for test syntax. - - """ - alert = {} - - """The properties list describes each property to be created. Each - item should be a dict of args to pass to a - bpy.props.<?>Property function, with the exception of 'type' - which is used and stripped by extensions_framework in order to - determine which Property creation function to call. - - Example item: - { - 'type': 'int', # bpy.props.IntProperty - 'attr': 'threads', # bpy.types.<type>.threads - 'name': 'Render Threads', # Rendered next to the UI - 'description': 'Number of threads to use', # Tooltip text in the UI - 'default': 1, - 'min': 1, - 'soft_min': 1, - 'max': 64, - 'soft_max': 64 - } - - """ - properties = [] - - def draw_callback(self, context): - """Sub-classes can override this to get a callback when - rendering is completed by a property_group_renderer sub-class. - - """ - - pass - - @classmethod - def get_exportable_properties(cls): - """Return a list of properties which have the 'save_in_preset' key - set to True, and hence should be saved into preset files. - - """ - - out = [] - for prop in cls.properties: - if 'save_in_preset' in prop.keys() and prop['save_in_preset']: - out.append(prop) - return out - - def reset(self): - """Reset all properties in this group to the default value, - if specified""" - for prop in self.properties: - pk = prop.keys() - if 'attr' in pk and 'default' in pk and hasattr(self, prop['attr']): - setattr(self, prop['attr'], prop['default']) + """A declarative_property_group describes a set of logically + related properties, using a declarative style to list each + property type, name, values, and other relevant information. + The information provided for each property depends on the + property's type. + + The properties list attribute in this class describes the + properties present in this group. + + Some additional information about the properties in this group + can be specified, so that a UI can be generated to display them. + To that end, the controls list attribute and the visibility dict + attribute are present here, to be read and interpreted by a + property_group_renderer object. + See extensions_framework.ui.property_group_renderer. + + """ + + ef_initialised = False + + """This property tells extensions_framework which bpy.type(s) + to attach this PropertyGroup to. If left as an empty list, + it will not be attached to any type, but its properties will + still be initialised. The type(s) given in the list should be + a string, such as 'Scene'. + + """ + ef_attach_to = [] + + @classmethod + def initialise_properties(cls): + """This is a function that should be called on + sub-classes of declarative_property_group in order + to ensure that they are initialised when the addon + is loaded. + the init_properties is called without caching here, + as it is assumed that any addon calling this function + will also call ef_remove_properties when it is + unregistered. + + """ + + if not cls.ef_initialised: + for property_group_parent in cls.ef_attach_to: + if property_group_parent is not None: + prototype = getattr(bpy.types, property_group_parent) + if not hasattr(prototype, cls.__name__): + init_properties(prototype, [{ + 'type': 'pointer', + 'attr': cls.__name__, + 'ptype': cls, + 'name': cls.__name__, + 'description': cls.__name__ + }], cache=False) + + init_properties(cls, cls.properties, cache=False) + cls.ef_initialised = True + + return cls + + @classmethod + def register_initialise_properties(cls): + """As ef_initialise_properties, but also registers the + class with RNA. Note that this isn't a great idea + because it's non-trivial to unregister the class, unless + you keep track of it yourself. + """ + + bpy.utils.register_class(cls) + cls.initialise_properties() + return cls + + @classmethod + def remove_properties(cls): + """This is a function that should be called on + sub-classes of declarative_property_group in order + to ensure that they are un-initialised when the addon + is unloaded. + + """ + + if cls.ef_initialised: + prototype = getattr(bpy.types, cls.__name__) + for prop in cls.properties: + if hasattr(prototype, prop['attr']): + delattr(prototype, prop['attr']) + + for property_group_parent in cls.ef_attach_to: + if property_group_parent is not None: + prototype = getattr(bpy.types, property_group_parent) + if hasattr(prototype, cls.__name__): + delattr(prototype, cls.__name__) + + cls.ef_initialised = False + + return cls + + + """This list controls the order of property layout when rendered + by a property_group_renderer. This can be a nested list, where each + list becomes a row in the panel layout. Nesting may be to any depth. + + """ + controls = [] + + """The visibility dict controls the visibility of properties based on + the value of other properties. See extensions_framework.validate + for test syntax. + + """ + visibility = {} + + """The enabled dict controls the enabled state of properties based on + the value of other properties. See extensions_framework.validate + for test syntax. + + """ + enabled = {} + + """The alert dict controls the alert state of properties based on + the value of other properties. See extensions_framework.validate + for test syntax. + + """ + alert = {} + + """The properties list describes each property to be created. Each + item should be a dict of args to pass to a + bpy.props.<?>Property function, with the exception of 'type' + which is used and stripped by extensions_framework in order to + determine which Property creation function to call. + + Example item: + { + 'type': 'int', # bpy.props.IntProperty + 'attr': 'threads', # bpy.types.<type>.threads + 'name': 'Render Threads', # Rendered next to the UI + 'description': 'Number of threads to use', # Tooltip text in the UI + 'default': 1, + 'min': 1, + 'soft_min': 1, + 'max': 64, + 'soft_max': 64 + } + + """ + properties = [] + + def draw_callback(self, context): + """Sub-classes can override this to get a callback when + rendering is completed by a property_group_renderer sub-class. + + """ + + pass + + @classmethod + def get_exportable_properties(cls): + """Return a list of properties which have the 'save_in_preset' key + set to True, and hence should be saved into preset files. + + """ + + out = [] + for prop in cls.properties: + if 'save_in_preset' in prop.keys() and prop['save_in_preset']: + out.append(prop) + return out + + def reset(self): + """Reset all properties in this group to the default value, + if specified""" + for prop in self.properties: + pk = prop.keys() + if 'attr' in pk and 'default' in pk and hasattr(self, prop['attr']): + setattr(self, prop['attr'], prop['default']) class Addon(object): - """A list of classes registered by this addon""" - static_addon_count = 0 - - addon_serial = 0 - addon_classes = None - bl_info = None - - BL_VERSION = None - BL_IDNAME = None - - def __init__(self, bl_info=None): - self.addon_classes = [] - self.bl_info = bl_info - - # Keep a count in case we have to give this addon an anonymous name - self.addon_serial = Addon.static_addon_count - Addon.static_addon_count += 1 - - if self.bl_info: - self.BL_VERSION = '.'.join(['%s'%v for v in self.bl_info['version']]).lower() - self.BL_IDNAME = self.bl_info['name'].lower() + '-' + self.BL_VERSION - else: - # construct anonymous name - self.BL_VERSION = '0' - self.BL_IDNAME = 'Addon-%03d'%self.addon_serial - - def addon_register_class(self, cls): - """This method is designed to be used as a decorator on RNA-registerable - classes defined by the addon. By using this decorator, this class will - keep track of classes registered by this addon so that they can be - unregistered later in the correct order. - - """ - self.addon_classes.append(cls) - return cls - - def register(self): - """This is the register function that should be exposed in the addon's - __init__. - - """ - for cls in self.addon_classes: - bpy.utils.register_class(cls) - if hasattr(cls, 'ef_attach_to'): cls.initialise_properties() - - def unregister(self): - """This is the unregister function that should be exposed in the addon's - __init__. - - """ - for cls in self.addon_classes[::-1]: # unregister in reverse order - if hasattr(cls, 'ef_attach_to'): cls.remove_properties() - bpy.utils.unregister_class(cls) - - def init_functions(self): - """Returns references to the three functions that this addon needs - for successful class registration management. In the addon's __init__ - you would use like this: - - addon_register_class, register, unregister = Addon().init_functions() - - """ - - return self.register, self.unregister + """A list of classes registered by this addon""" + static_addon_count = 0 + + addon_serial = 0 + addon_classes = None + bl_info = None + + BL_VERSION = None + BL_IDNAME = None + + def __init__(self, bl_info=None): + self.addon_classes = [] + self.bl_info = bl_info + + # Keep a count in case we have to give this addon an anonymous name + self.addon_serial = Addon.static_addon_count + Addon.static_addon_count += 1 + + if self.bl_info: + self.BL_VERSION = '.'.join(['%s'%v for v in self.bl_info['version']]).lower() + self.BL_IDNAME = self.bl_info['name'].lower() + '-' + self.BL_VERSION + else: + # construct anonymous name + self.BL_VERSION = '0' + self.BL_IDNAME = 'Addon-%03d'%self.addon_serial + + def addon_register_class(self, cls): + """This method is designed to be used as a decorator on RNA-registerable + classes defined by the addon. By using this decorator, this class will + keep track of classes registered by this addon so that they can be + unregistered later in the correct order. + + """ + self.addon_classes.append(cls) + return cls + + def register(self): + """This is the register function that should be exposed in the addon's + __init__. + + """ + for cls in self.addon_classes: + bpy.utils.register_class(cls) + if hasattr(cls, 'ef_attach_to'): cls.initialise_properties() + + def unregister(self): + """This is the unregister function that should be exposed in the addon's + __init__. + + """ + for cls in self.addon_classes[::-1]: # unregister in reverse order + if hasattr(cls, 'ef_attach_to'): cls.remove_properties() + bpy.utils.unregister_class(cls) + + def init_functions(self): + """Returns references to the three functions that this addon needs + for successful class registration management. In the addon's __init__ + you would use like this: + + addon_register_class, register, unregister = Addon().init_functions() + + """ + + return self.register, self.unregister diff --git a/modules/extensions_framework/ui.py b/modules/extensions_framework/ui.py index 4f24d873471e5d663b9c72f3cf0876171592f654..944309111ec19e0504d8fad363a4d8543362a41e 100644 --- a/modules/extensions_framework/ui.py +++ b/modules/extensions_framework/ui.py @@ -29,309 +29,309 @@ import bpy from extensions_framework.validate import Logician class EF_OT_msg(bpy.types.Operator): - """An operator to show simple messages in the UI""" - bl_idname = 'ef.msg' - bl_label = 'Show UI Message' - msg_type = bpy.props.StringProperty(default='INFO') - msg_text = bpy.props.StringProperty(default='') - def execute(self, context): - self.report({self.properties.msg_type}, self.properties.msg_text) - return {'FINISHED'} + """An operator to show simple messages in the UI""" + bl_idname = 'ef.msg' + bl_label = 'Show UI Message' + msg_type = bpy.props.StringProperty(default='INFO') + msg_text = bpy.props.StringProperty(default='') + def execute(self, context): + self.report({self.properties.msg_type}, self.properties.msg_text) + return {'FINISHED'} def _get_item_from_context(context, path): - """Utility to get an object when the path to it is known: - _get_item_from_context(context, ['a','b','c']) returns - context.a.b.c - No error checking is performed other than checking that context - is not None. Exceptions caused by invalid path should be caught in - the calling code. - - """ - - if context is not None: - for p in path: - context = getattr(context, p) - return context + """Utility to get an object when the path to it is known: + _get_item_from_context(context, ['a','b','c']) returns + context.a.b.c + No error checking is performed other than checking that context + is not None. Exceptions caused by invalid path should be caught in + the calling code. + + """ + + if context is not None: + for p in path: + context = getattr(context, p) + return context class property_group_renderer(bpy.types.Panel): - """Mix-in class for sub-classes of bpy.types.Panel. This class - will provide the draw() method which implements drawing one or - more property groups derived from - extensions_framework.declarative_propery_group. - The display_property_groups list attribute describes which - declarative_property_groups should be drawn in the Panel, and - how to extract those groups from the context passed to draw(). - - """ - - """The display_property_groups list attribute specifies which - custom declarative_property_groups this panel should draw, and - where to find that property group in the active context. - Example item: - ( ('scene',), 'myaddon_property_group') - In this case, this renderer will look for properties in - context.scene.myaddon_property_group to draw in the Panel. - - """ - display_property_groups = [] - - def draw(self, context): - """Sub-classes should override this if they need to display - other (object-related) property groups. super().draw(context) - can be a useful call in those cases. - - """ - for property_group_path, property_group_name in \ - self.display_property_groups: - ctx = _get_item_from_context(context, property_group_path) - property_group = getattr(ctx, property_group_name) - for p in property_group.controls: - self.draw_column(p, self.layout, ctx, context, - property_group=property_group) - property_group.draw_callback(context) - - def check_visibility(self, lookup_property, property_group): - """Determine if the lookup_property should be drawn in the Panel""" - vt = Logician(property_group) - if lookup_property in property_group.visibility.keys(): - if hasattr(property_group, lookup_property): - member = getattr(property_group, lookup_property) - else: - member = None - return vt.test_logic(member, - property_group.visibility[lookup_property]) - else: - return True - - def check_enabled(self, lookup_property, property_group): - """Determine if the lookup_property should be enabled in the Panel""" - et = Logician(property_group) - if lookup_property in property_group.enabled.keys(): - if hasattr(property_group, lookup_property): - member = getattr(property_group, lookup_property) - else: - member = None - return et.test_logic(member, - property_group.enabled[lookup_property]) - else: - return True - - def check_alert(self, lookup_property, property_group): - """Determine if the lookup_property should be in an alert state in the Panel""" - et = Logician(property_group) - if lookup_property in property_group.alert.keys(): - if hasattr(property_group, lookup_property): - member = getattr(property_group, lookup_property) - else: - member = None - return et.test_logic(member, - property_group.alert[lookup_property]) - else: - return False - - def is_real_property(self, lookup_property, property_group): - for prop in property_group.properties: - if prop['attr'] == lookup_property: - return prop['type'] not in ['text', 'prop_search'] - - return False - - def draw_column(self, control_list_item, layout, context, - supercontext=None, property_group=None): - """Draw a column's worth of UI controls in this Panel""" - if type(control_list_item) is list: - draw_row = False - - found_percent = None - for sp in control_list_item: - if type(sp) is float: - found_percent = sp - elif type(sp) is list: - for ssp in [s for s in sp if self.is_real_property(s, property_group)]: - draw_row = draw_row or self.check_visibility(ssp, - property_group) - else: - draw_row = draw_row or self.check_visibility(sp, - property_group) - - next_items = [s for s in control_list_item if type(s) in [str, list]] - if draw_row and len(next_items) > 0: - if found_percent is not None: - splt = layout.split(percentage=found_percent) - else: - splt = layout.row(True) - for sp in next_items: - col2 = splt.column() - self.draw_column(sp, col2, context, supercontext, - property_group) - else: - if self.check_visibility(control_list_item, property_group): - - for current_property in property_group.properties: - if current_property['attr'] == control_list_item: - current_property_keys = current_property.keys() - - sub_layout_created = False - if not self.check_enabled(control_list_item, property_group): - last_layout = layout - sub_layout_created = True - - layout = layout.row() - layout.enabled = False - - if self.check_alert(control_list_item, property_group): - if not sub_layout_created: - last_layout = layout - sub_layout_created = True - layout = layout.row() - layout.alert = True - - if 'type' in current_property_keys: - if current_property['type'] in ['int', 'float', - 'float_vector', 'string']: - layout.prop( - property_group, - control_list_item, - text = current_property['name'], - expand = current_property['expand'] \ - if 'expand' in current_property_keys \ - else False, - slider = current_property['slider'] \ - if 'slider' in current_property_keys \ - else False, - toggle = current_property['toggle'] \ - if 'toggle' in current_property_keys \ - else False, - icon_only = current_property['icon_only'] \ - if 'icon_only' in current_property_keys \ - else False, - event = current_property['event'] \ - if 'event' in current_property_keys \ - else False, - full_event = current_property['full_event'] \ - if 'full_event' in current_property_keys \ - else False, - emboss = current_property['emboss'] \ - if 'emboss' in current_property_keys \ - else True, - ) - if current_property['type'] in ['enum']: - if 'use_menu' in current_property_keys and \ - current_property['use_menu']: - layout.prop_menu_enum( - property_group, - control_list_item, - text = current_property['name'] - ) - else: - layout.prop( - property_group, - control_list_item, - text = current_property['name'], - expand = current_property['expand'] \ - if 'expand' in current_property_keys \ - else False, - slider = current_property['slider'] \ - if 'slider' in current_property_keys \ - else False, - toggle = current_property['toggle'] \ - if 'toggle' in current_property_keys \ - else False, - icon_only = current_property['icon_only'] \ - if 'icon_only' in current_property_keys \ - else False, - event = current_property['event'] \ - if 'event' in current_property_keys \ - else False, - full_event = current_property['full_event'] \ - if 'full_event' in current_property_keys \ - else False, - emboss = current_property['emboss'] \ - if 'emboss' in current_property_keys \ - else True, - ) - if current_property['type'] in ['bool']: - layout.prop( - property_group, - control_list_item, - text = current_property['name'], - toggle = current_property['toggle'] \ - if 'toggle' in current_property_keys \ - else False, - icon_only = current_property['icon_only'] \ - if 'icon_only' in current_property_keys \ - else False, - event = current_property['event'] \ - if 'event' in current_property_keys \ - else False, - full_event = current_property['full_event'] \ - if 'full_event' in current_property_keys \ - else False, - emboss = current_property['emboss'] \ - if 'emboss' in current_property_keys \ - else True, - ) - elif current_property['type'] in ['operator']: - args = {} - for optional_arg in ('text', 'icon'): - if optional_arg in current_property_keys: - args.update({ - optional_arg: current_property[optional_arg], - }) - layout.operator( current_property['operator'], **args ) - - elif current_property['type'] in ['menu']: - args = {} - for optional_arg in ('text', 'icon'): - if optional_arg in current_property_keys: - args.update({ - optional_arg: current_property[optional_arg], - }) - layout.menu(current_property['menu'], **args) - - elif current_property['type'] in ['text']: - layout.label( - text = current_property['name'] - ) - - elif current_property['type'] in ['template_list']: - layout.template_list( - current_property['src'](supercontext, context), - current_property['src_attr'], - current_property['trg'](supercontext, context), - current_property['trg_attr'], - rows = 4 \ - if not 'rows' in current_property_keys \ - else current_property['rows'], - maxrows = 4 \ - if not 'rows' in current_property_keys \ - else current_property['rows'], - type = 'DEFAULT' \ - if not 'list_type' in current_property_keys \ - else current_property['list_type'] - ) - - elif current_property['type'] in ['prop_search']: - layout.prop_search( - current_property['trg'](supercontext, - context), - current_property['trg_attr'], - current_property['src'](supercontext, - context), - current_property['src_attr'], - text = current_property['name'], - ) - - elif current_property['type'] in ['ef_callback']: - getattr(self, current_property['method'])(supercontext) - else: - layout.prop(property_group, control_list_item) - - if sub_layout_created: - layout = last_layout - - # Fire a draw callback if specified - if 'draw' in current_property_keys: - current_property['draw'](supercontext, context) - - break + """Mix-in class for sub-classes of bpy.types.Panel. This class + will provide the draw() method which implements drawing one or + more property groups derived from + extensions_framework.declarative_propery_group. + The display_property_groups list attribute describes which + declarative_property_groups should be drawn in the Panel, and + how to extract those groups from the context passed to draw(). + + """ + + """The display_property_groups list attribute specifies which + custom declarative_property_groups this panel should draw, and + where to find that property group in the active context. + Example item: + ( ('scene',), 'myaddon_property_group') + In this case, this renderer will look for properties in + context.scene.myaddon_property_group to draw in the Panel. + + """ + display_property_groups = [] + + def draw(self, context): + """Sub-classes should override this if they need to display + other (object-related) property groups. super().draw(context) + can be a useful call in those cases. + + """ + for property_group_path, property_group_name in \ + self.display_property_groups: + ctx = _get_item_from_context(context, property_group_path) + property_group = getattr(ctx, property_group_name) + for p in property_group.controls: + self.draw_column(p, self.layout, ctx, context, + property_group=property_group) + property_group.draw_callback(context) + + def check_visibility(self, lookup_property, property_group): + """Determine if the lookup_property should be drawn in the Panel""" + vt = Logician(property_group) + if lookup_property in property_group.visibility.keys(): + if hasattr(property_group, lookup_property): + member = getattr(property_group, lookup_property) + else: + member = None + return vt.test_logic(member, + property_group.visibility[lookup_property]) + else: + return True + + def check_enabled(self, lookup_property, property_group): + """Determine if the lookup_property should be enabled in the Panel""" + et = Logician(property_group) + if lookup_property in property_group.enabled.keys(): + if hasattr(property_group, lookup_property): + member = getattr(property_group, lookup_property) + else: + member = None + return et.test_logic(member, + property_group.enabled[lookup_property]) + else: + return True + + def check_alert(self, lookup_property, property_group): + """Determine if the lookup_property should be in an alert state in the Panel""" + et = Logician(property_group) + if lookup_property in property_group.alert.keys(): + if hasattr(property_group, lookup_property): + member = getattr(property_group, lookup_property) + else: + member = None + return et.test_logic(member, + property_group.alert[lookup_property]) + else: + return False + + def is_real_property(self, lookup_property, property_group): + for prop in property_group.properties: + if prop['attr'] == lookup_property: + return prop['type'] not in ['text', 'prop_search'] + + return False + + def draw_column(self, control_list_item, layout, context, + supercontext=None, property_group=None): + """Draw a column's worth of UI controls in this Panel""" + if type(control_list_item) is list: + draw_row = False + + found_percent = None + for sp in control_list_item: + if type(sp) is float: + found_percent = sp + elif type(sp) is list: + for ssp in [s for s in sp if self.is_real_property(s, property_group)]: + draw_row = draw_row or self.check_visibility(ssp, + property_group) + else: + draw_row = draw_row or self.check_visibility(sp, + property_group) + + next_items = [s for s in control_list_item if type(s) in [str, list]] + if draw_row and len(next_items) > 0: + if found_percent is not None: + splt = layout.split(percentage=found_percent) + else: + splt = layout.row(True) + for sp in next_items: + col2 = splt.column() + self.draw_column(sp, col2, context, supercontext, + property_group) + else: + if self.check_visibility(control_list_item, property_group): + + for current_property in property_group.properties: + if current_property['attr'] == control_list_item: + current_property_keys = current_property.keys() + + sub_layout_created = False + if not self.check_enabled(control_list_item, property_group): + last_layout = layout + sub_layout_created = True + + layout = layout.row() + layout.enabled = False + + if self.check_alert(control_list_item, property_group): + if not sub_layout_created: + last_layout = layout + sub_layout_created = True + layout = layout.row() + layout.alert = True + + if 'type' in current_property_keys: + if current_property['type'] in ['int', 'float', + 'float_vector', 'string']: + layout.prop( + property_group, + control_list_item, + text = current_property['name'], + expand = current_property['expand'] \ + if 'expand' in current_property_keys \ + else False, + slider = current_property['slider'] \ + if 'slider' in current_property_keys \ + else False, + toggle = current_property['toggle'] \ + if 'toggle' in current_property_keys \ + else False, + icon_only = current_property['icon_only'] \ + if 'icon_only' in current_property_keys \ + else False, + event = current_property['event'] \ + if 'event' in current_property_keys \ + else False, + full_event = current_property['full_event'] \ + if 'full_event' in current_property_keys \ + else False, + emboss = current_property['emboss'] \ + if 'emboss' in current_property_keys \ + else True, + ) + if current_property['type'] in ['enum']: + if 'use_menu' in current_property_keys and \ + current_property['use_menu']: + layout.prop_menu_enum( + property_group, + control_list_item, + text = current_property['name'] + ) + else: + layout.prop( + property_group, + control_list_item, + text = current_property['name'], + expand = current_property['expand'] \ + if 'expand' in current_property_keys \ + else False, + slider = current_property['slider'] \ + if 'slider' in current_property_keys \ + else False, + toggle = current_property['toggle'] \ + if 'toggle' in current_property_keys \ + else False, + icon_only = current_property['icon_only'] \ + if 'icon_only' in current_property_keys \ + else False, + event = current_property['event'] \ + if 'event' in current_property_keys \ + else False, + full_event = current_property['full_event'] \ + if 'full_event' in current_property_keys \ + else False, + emboss = current_property['emboss'] \ + if 'emboss' in current_property_keys \ + else True, + ) + if current_property['type'] in ['bool']: + layout.prop( + property_group, + control_list_item, + text = current_property['name'], + toggle = current_property['toggle'] \ + if 'toggle' in current_property_keys \ + else False, + icon_only = current_property['icon_only'] \ + if 'icon_only' in current_property_keys \ + else False, + event = current_property['event'] \ + if 'event' in current_property_keys \ + else False, + full_event = current_property['full_event'] \ + if 'full_event' in current_property_keys \ + else False, + emboss = current_property['emboss'] \ + if 'emboss' in current_property_keys \ + else True, + ) + elif current_property['type'] in ['operator']: + args = {} + for optional_arg in ('text', 'icon'): + if optional_arg in current_property_keys: + args.update({ + optional_arg: current_property[optional_arg], + }) + layout.operator( current_property['operator'], **args ) + + elif current_property['type'] in ['menu']: + args = {} + for optional_arg in ('text', 'icon'): + if optional_arg in current_property_keys: + args.update({ + optional_arg: current_property[optional_arg], + }) + layout.menu(current_property['menu'], **args) + + elif current_property['type'] in ['text']: + layout.label( + text = current_property['name'] + ) + + elif current_property['type'] in ['template_list']: + layout.template_list( + current_property['src'](supercontext, context), + current_property['src_attr'], + current_property['trg'](supercontext, context), + current_property['trg_attr'], + rows = 4 \ + if not 'rows' in current_property_keys \ + else current_property['rows'], + maxrows = 4 \ + if not 'rows' in current_property_keys \ + else current_property['rows'], + type = 'DEFAULT' \ + if not 'list_type' in current_property_keys \ + else current_property['list_type'] + ) + + elif current_property['type'] in ['prop_search']: + layout.prop_search( + current_property['trg'](supercontext, + context), + current_property['trg_attr'], + current_property['src'](supercontext, + context), + current_property['src_attr'], + text = current_property['name'], + ) + + elif current_property['type'] in ['ef_callback']: + getattr(self, current_property['method'])(supercontext) + else: + layout.prop(property_group, control_list_item) + + if sub_layout_created: + layout = last_layout + + # Fire a draw callback if specified + if 'draw' in current_property_keys: + current_property['draw'](supercontext, context) + + break diff --git a/modules/extensions_framework/util.py b/modules/extensions_framework/util.py index b48c702676887f2839b26f2b0c85fc78d6844ae7..dd71e55af633ad0f96e55aad1237373474c9171d 100644 --- a/modules/extensions_framework/util.py +++ b/modules/extensions_framework/util.py @@ -49,222 +49,222 @@ this one. export_path = ''; def path_relative_to_export(p): - """Return a path that is relative to the export path""" - global export_path - p = filesystem_path(p) - ep = os.path.dirname(export_path) - - if os.sys.platform[:3] == "win": - # Prevent an error whereby python thinks C: and c: are different drives - if p[1] == ':': p = p[0].lower() + p[1:] - if ep[1] == ':': ep = ep[0].lower() + ep[1:] - - try: - relp = os.path.relpath(p, ep) - except ValueError: # path on different drive on windows - relp = p - - return relp.replace('\\', '/') + """Return a path that is relative to the export path""" + global export_path + p = filesystem_path(p) + ep = os.path.dirname(export_path) + + if os.sys.platform[:3] == "win": + # Prevent an error whereby python thinks C: and c: are different drives + if p[1] == ':': p = p[0].lower() + p[1:] + if ep[1] == ':': ep = ep[0].lower() + ep[1:] + + try: + relp = os.path.relpath(p, ep) + except ValueError: # path on different drive on windows + relp = p + + return relp.replace('\\', '/') def filesystem_path(p): - """Resolve a relative Blender path to a real filesystem path""" - if p.startswith('//'): - pout = bpy.path.abspath(p) - else: - pout = os.path.realpath(p) - - return pout.replace('\\', '/') + """Resolve a relative Blender path to a real filesystem path""" + if p.startswith('//'): + pout = bpy.path.abspath(p) + else: + pout = os.path.realpath(p) + + return pout.replace('\\', '/') # TODO: - somehow specify TYPES to get/set from config def find_config_value(module, section, key, default): - """Attempt to find the configuration value specified by string key - in the specified section of module's configuration file. If it is - not found, return default. - - """ - global config_paths - fc = [] - for p in config_paths: - if os.path.exists(p) and os.path.isdir(p) and os.access(p, os.W_OK): - fc.append( '/'.join([p, '%s.cfg' % module])) - - if len(fc) < 1: - print('Cannot find %s config file path' % module) - return default - - cp = configparser.SafeConfigParser() - - cfg_files = cp.read(fc) - if len(cfg_files) > 0: - try: - val = cp.get(section, key) - if val == 'true': - return True - elif val == 'false': - return False - else: - return val - except: - return default - else: - return default + """Attempt to find the configuration value specified by string key + in the specified section of module's configuration file. If it is + not found, return default. + + """ + global config_paths + fc = [] + for p in config_paths: + if os.path.exists(p) and os.path.isdir(p) and os.access(p, os.W_OK): + fc.append( '/'.join([p, '%s.cfg' % module])) + + if len(fc) < 1: + print('Cannot find %s config file path' % module) + return default + + cp = configparser.SafeConfigParser() + + cfg_files = cp.read(fc) + if len(cfg_files) > 0: + try: + val = cp.get(section, key) + if val == 'true': + return True + elif val == 'false': + return False + else: + return val + except: + return default + else: + return default def write_config_value(module, section, key, value): - """Attempt to write the configuration value specified by string key - in the specified section of module's configuration file. - - """ - global config_paths - fc = [] - for p in config_paths: - if os.path.exists(p) and os.path.isdir(p) and os.access(p, os.W_OK): - fc.append( '/'.join([p, '%s.cfg' % module])) - - if len(fc) < 1: - raise Exception('Cannot find a writable path to store %s config file' % - module) - - cp = configparser.SafeConfigParser() - - cfg_files = cp.read(fc) - - if not cp.has_section(section): - cp.add_section(section) - - if value == True: - cp.set(section, key, 'true') - elif value == False: - cp.set(section, key, 'false') - else: - cp.set(section, key, value) - - if len(cfg_files) < 1: - cfg_files = fc - - fh=open(cfg_files[0],'w') - cp.write(fh) - fh.close() - - return True + """Attempt to write the configuration value specified by string key + in the specified section of module's configuration file. + + """ + global config_paths + fc = [] + for p in config_paths: + if os.path.exists(p) and os.path.isdir(p) and os.access(p, os.W_OK): + fc.append( '/'.join([p, '%s.cfg' % module])) + + if len(fc) < 1: + raise Exception('Cannot find a writable path to store %s config file' % + module) + + cp = configparser.SafeConfigParser() + + cfg_files = cp.read(fc) + + if not cp.has_section(section): + cp.add_section(section) + + if value == True: + cp.set(section, key, 'true') + elif value == False: + cp.set(section, key, 'false') + else: + cp.set(section, key, value) + + if len(cfg_files) < 1: + cfg_files = fc + + fh=open(cfg_files[0],'w') + cp.write(fh) + fh.close() + + return True def scene_filename(): - """Construct a safe scene filename, using 'untitled' instead of ''""" - filename = os.path.splitext(os.path.basename(bpy.data.filepath))[0] - if filename == '': - filename = 'untitled' - return bpy.path.clean_name(filename) + """Construct a safe scene filename, using 'untitled' instead of ''""" + filename = os.path.splitext(os.path.basename(bpy.data.filepath))[0] + if filename == '': + filename = 'untitled' + return bpy.path.clean_name(filename) def temp_directory(): - """Return the system temp directory""" - return tempfile.gettempdir() + """Return the system temp directory""" + return tempfile.gettempdir() def temp_file(ext='tmp'): - """Get a temporary filename with the given extension. This function - will actually attempt to create the file.""" - tf, fn = tempfile.mkstemp(suffix='.%s'%ext) - os.close(tf) - return fn + """Get a temporary filename with the given extension. This function + will actually attempt to create the file.""" + tf, fn = tempfile.mkstemp(suffix='.%s'%ext) + os.close(tf) + return fn class TimerThread(threading.Thread): - """Periodically call self.kick(). The period of time in seconds - between calling is given by self.KICK_PERIOD, and the first call - may be delayed by setting self.STARTUP_DELAY, also in seconds. - self.kick() will continue to be called at regular intervals until - self.stop() is called. Since this is a thread, calling self.join() - may be wise after calling self.stop() if self.kick() is performing - a task necessary for the continuation of the program. - The object that creates this TimerThread may pass into it data - needed during self.kick() as a dict LocalStorage in __init__(). - - """ - STARTUP_DELAY = 0 - KICK_PERIOD = 8 - - active = True - timer = None - - LocalStorage = None - - def __init__(self, LocalStorage=dict()): - threading.Thread.__init__(self) - self.LocalStorage = LocalStorage - - def set_kick_period(self, period): - """Adjust the KICK_PERIOD between __init__() and start()""" - self.KICK_PERIOD = period + self.STARTUP_DELAY - - def stop(self): - """Stop this timer. This method does not join()""" - self.active = False - if self.timer is not None: - self.timer.cancel() - - def run(self): - """Timed Thread loop""" - while self.active: - self.timer = threading.Timer(self.KICK_PERIOD, self.kick_caller) - self.timer.start() - if self.timer.isAlive(): self.timer.join() - - def kick_caller(self): - """Intermediary between the kick-wait-loop and kick to allow - adjustment of the first KICK_PERIOD by STARTUP_DELAY - - """ - if self.STARTUP_DELAY > 0: - self.KICK_PERIOD -= self.STARTUP_DELAY - self.STARTUP_DELAY = 0 - - self.kick() - - def kick(self): - """Sub-classes do their work here""" - pass + """Periodically call self.kick(). The period of time in seconds + between calling is given by self.KICK_PERIOD, and the first call + may be delayed by setting self.STARTUP_DELAY, also in seconds. + self.kick() will continue to be called at regular intervals until + self.stop() is called. Since this is a thread, calling self.join() + may be wise after calling self.stop() if self.kick() is performing + a task necessary for the continuation of the program. + The object that creates this TimerThread may pass into it data + needed during self.kick() as a dict LocalStorage in __init__(). + + """ + STARTUP_DELAY = 0 + KICK_PERIOD = 8 + + active = True + timer = None + + LocalStorage = None + + def __init__(self, LocalStorage=dict()): + threading.Thread.__init__(self) + self.LocalStorage = LocalStorage + + def set_kick_period(self, period): + """Adjust the KICK_PERIOD between __init__() and start()""" + self.KICK_PERIOD = period + self.STARTUP_DELAY + + def stop(self): + """Stop this timer. This method does not join()""" + self.active = False + if self.timer is not None: + self.timer.cancel() + + def run(self): + """Timed Thread loop""" + while self.active: + self.timer = threading.Timer(self.KICK_PERIOD, self.kick_caller) + self.timer.start() + if self.timer.isAlive(): self.timer.join() + + def kick_caller(self): + """Intermediary between the kick-wait-loop and kick to allow + adjustment of the first KICK_PERIOD by STARTUP_DELAY + + """ + if self.STARTUP_DELAY > 0: + self.KICK_PERIOD -= self.STARTUP_DELAY + self.STARTUP_DELAY = 0 + + self.kick() + + def kick(self): + """Sub-classes do their work here""" + pass def format_elapsed_time(t): - """Format a duration in seconds as an HH:MM:SS format time""" - - td = datetime.timedelta(seconds=t) - min = td.days*1440 + td.seconds/60.0 - hrs = td.days*24 + td.seconds/3600.0 - - return '%i:%02i:%02i' % (hrs, min%60, td.seconds%60) + """Format a duration in seconds as an HH:MM:SS format time""" + + td = datetime.timedelta(seconds=t) + min = td.days*1440 + td.seconds/60.0 + hrs = td.days*24 + td.seconds/3600.0 + + return '%i:%02i:%02i' % (hrs, min%60, td.seconds%60) def getSequenceTexturePath(it, f): - import bpy.path - import os.path - import string - fd = it.image_user.frame_duration - fs = it.image_user.frame_start - fo = it.image_user.frame_offset - cyclic = it.image_user.use_cyclic - ext = os.path.splitext(it.image.filepath)[-1] - fb = bpy.path.display_name_from_filepath(it.image.filepath) - dn = os.path.dirname(it.image.filepath) - rf = fb[::-1] - nl = 0 - for i in range (len(fb)): - if rf[i] in string.digits: - nl += 1 - else: - break - head = fb[:len(fb)-nl] - fnum = f - if fs != 1: - if f != fs: - fnum -= (fs-1) - elif f == fs: - fnum = 1 - if fnum <= 0: - if cyclic: - fnum = fd - abs(fnum) % fd - else: - fnum = 1 - elif fnum > fd: - if cyclic: - fnum = fnum % fd - else: - fnum = fd - fnum += fo - return dn + "/" + head + str(fnum).rjust(nl, "0") + ext + import bpy.path + import os.path + import string + fd = it.image_user.frame_duration + fs = it.image_user.frame_start + fo = it.image_user.frame_offset + cyclic = it.image_user.use_cyclic + ext = os.path.splitext(it.image.filepath)[-1] + fb = bpy.path.display_name_from_filepath(it.image.filepath) + dn = os.path.dirname(it.image.filepath) + rf = fb[::-1] + nl = 0 + for i in range (len(fb)): + if rf[i] in string.digits: + nl += 1 + else: + break + head = fb[:len(fb)-nl] + fnum = f + if fs != 1: + if f != fs: + fnum -= (fs-1) + elif f == fs: + fnum = 1 + if fnum <= 0: + if cyclic: + fnum = fd - abs(fnum) % fd + else: + fnum = 1 + elif fnum > fd: + if cyclic: + fnum = fnum % fd + else: + fnum = fd + fnum += fo + return dn + "/" + head + str(fnum).rjust(nl, "0") + ext diff --git a/modules/extensions_framework/validate.py b/modules/extensions_framework/validate.py index d9cee8fd807152860f6c01977e92c328d4621376..5b20552ba593d63ac10aa0dcef4cce25b468fcea 100644 --- a/modules/extensions_framework/validate.py +++ b/modules/extensions_framework/validate.py @@ -34,13 +34,13 @@ is possible to arrive at a True or False result for various purposes: A Subject can be any object whose members are readable with getattr() : class Subject(object): - a = 0 - b = 1 - c = 'foo' - d = True - e = False - f = 8 - g = 'bar' + a = 0 + b = 1 + c = 'foo' + d = True + e = False + f = 8 + g = 'bar' Tests are described thus: @@ -51,27 +51,27 @@ numerical comparison. With regards to Subject, each of these evaluate to True: TESTA = { - 'a': 0, - 'c': Logic_OR([ 'foo', 'bar' ]), - 'd': Logic_AND([True, True]), - 'f': Logic_AND([8, {'b': 1}]), - 'e': {'b': Logic_Operator({'gte':1, 'lt':3}) }, - 'g': Logic_OR([ 'baz', Logic_AND([{'b': 1}, {'f': 8}]) ]) + 'a': 0, + 'c': Logic_OR([ 'foo', 'bar' ]), + 'd': Logic_AND([True, True]), + 'f': Logic_AND([8, {'b': 1}]), + 'e': {'b': Logic_Operator({'gte':1, 'lt':3}) }, + 'g': Logic_OR([ 'baz', Logic_AND([{'b': 1}, {'f': 8}]) ]) } With regards to Subject, each of these evaluate to False: TESTB = { - 'a': 'foo', - 'c': Logic_OR([ 'bar', 'baz' ]), - 'd': Logic_AND([ True, 'foo' ]), - 'f': Logic_AND([9, {'b': 1}]), - 'e': {'b': Logic_Operator({'gte':-10, 'lt': 1}) }, - 'g': Logic_OR([ 'baz', Logic_AND([{'b':0}, {'f': 8}]) ]) + 'a': 'foo', + 'c': Logic_OR([ 'bar', 'baz' ]), + 'd': Logic_AND([ True, 'foo' ]), + 'f': Logic_AND([9, {'b': 1}]), + 'e': {'b': Logic_Operator({'gte':-10, 'lt': 1}) }, + 'g': Logic_OR([ 'baz', Logic_AND([{'b':0}, {'f': 8}]) ]) } With regards to Subject, this test is invalid TESTC = { - 'n': 0 + 'n': 0 } Tests are executed thus: @@ -82,132 +82,132 @@ L.execute(TESTA) """ class Logic_AND(list): - pass + pass class Logic_OR(list): - pass + pass class Logic_Operator(dict): - pass + pass class Logician(object): - """Given a subject and a dict that describes tests to perform on - its members, this class will evaluate True or False results for - each member/test pair. See the examples below for test syntax. - - """ - - subject = None - def __init__(self, subject): - self.subject = subject - - def get_member(self, member_name): - """Get a member value from the subject object. Raise exception - if subject is None or member not found. - - """ - if self.subject is None: - raise Exception('Cannot run tests on a subject which is None') - - return getattr(self.subject, member_name) - - def test_logic(self, member, logic, operator='eq'): - """Find the type of test to run on member, and perform that test""" - - if type(logic) is dict: - return self.test_dict(member, logic) - elif type(logic) is Logic_AND: - return self.test_and(member, logic) - elif type(logic) is Logic_OR: - return self.test_or(member, logic) - elif type(logic) is Logic_Operator: - return self.test_operator(member, logic) - else: - # compare the value, I think using Logic_Operator() here - # allows completeness in test_operator(), but I can't put - # my finger on why for the minute - return self.test_operator(member, - Logic_Operator({operator: logic})) - - def test_operator(self, member, value): - """Execute the operators contained within value and expect that - ALL operators are True - - """ - - # something in this method is incomplete, what if operand is - # a dict, Logic_AND, Logic_OR or another Logic_Operator ? - # Do those constructs even make any sense ? - - result = True - for operator, operand in value.items(): - operator = operator.lower().strip() - if operator in ['eq', '==']: - result &= member==operand - if operator in ['not', '!=']: - result &= member!=operand - if operator in ['lt', '<']: - result &= member<operand - if operator in ['lte', '<=']: - result &= member<=operand - if operator in ['gt', '>']: - result &= member>operand - if operator in ['gte', '>=']: - result &= member>=operand - if operator in ['and', '&']: - result &= member&operand - if operator in ['or', '|']: - result &= member|operand - if operator in ['len']: - result &= len(member)==operand - # I can think of some more, but they're probably not useful. - - return result - - def test_or(self, member, logic): - """Member is a value, logic is a set of values, ANY of which - can be True - - """ - result = False - for test in logic: - result |= self.test_logic(member, test) - - return result - - def test_and(self, member, logic): - """Member is a value, logic is a list of values, ALL of which - must be True - - """ - result = True - for test in logic: - result &= self.test_logic(member, test) - - return result - - def test_dict(self, member, logic): - """Member is a value, logic is a dict of other members to - compare to. All other member tests must be True - - """ - result = True - for other_member, test in logic.items(): - result &= self.test_logic(self.get_member(other_member), test) - - return result - - def execute(self, test): - """Subject is an object, test is a dict of {member: test} pairs - to perform on subject's members. Wach key in test is a member - of subject. - - """ - - for member_name, logic in test.items(): - result = self.test_logic(self.get_member(member_name), logic) - print('member %s is %s' % (member_name, result)) - -# A couple of name aliases + """Given a subject and a dict that describes tests to perform on + its members, this class will evaluate True or False results for + each member/test pair. See the examples below for test syntax. + + """ + + subject = None + def __init__(self, subject): + self.subject = subject + + def get_member(self, member_name): + """Get a member value from the subject object. Raise exception + if subject is None or member not found. + + """ + if self.subject is None: + raise Exception('Cannot run tests on a subject which is None') + + return getattr(self.subject, member_name) + + def test_logic(self, member, logic, operator='eq'): + """Find the type of test to run on member, and perform that test""" + + if type(logic) is dict: + return self.test_dict(member, logic) + elif type(logic) is Logic_AND: + return self.test_and(member, logic) + elif type(logic) is Logic_OR: + return self.test_or(member, logic) + elif type(logic) is Logic_Operator: + return self.test_operator(member, logic) + else: + # compare the value, I think using Logic_Operator() here + # allows completeness in test_operator(), but I can't put + # my finger on why for the minute + return self.test_operator(member, + Logic_Operator({operator: logic})) + + def test_operator(self, member, value): + """Execute the operators contained within value and expect that + ALL operators are True + + """ + + # something in this method is incomplete, what if operand is + # a dict, Logic_AND, Logic_OR or another Logic_Operator ? + # Do those constructs even make any sense ? + + result = True + for operator, operand in value.items(): + operator = operator.lower().strip() + if operator in ['eq', '==']: + result &= member==operand + if operator in ['not', '!=']: + result &= member!=operand + if operator in ['lt', '<']: + result &= member<operand + if operator in ['lte', '<=']: + result &= member<=operand + if operator in ['gt', '>']: + result &= member>operand + if operator in ['gte', '>=']: + result &= member>=operand + if operator in ['and', '&']: + result &= member&operand + if operator in ['or', '|']: + result &= member|operand + if operator in ['len']: + result &= len(member)==operand + # I can think of some more, but they're probably not useful. + + return result + + def test_or(self, member, logic): + """Member is a value, logic is a set of values, ANY of which + can be True + + """ + result = False + for test in logic: + result |= self.test_logic(member, test) + + return result + + def test_and(self, member, logic): + """Member is a value, logic is a list of values, ALL of which + must be True + + """ + result = True + for test in logic: + result &= self.test_logic(member, test) + + return result + + def test_dict(self, member, logic): + """Member is a value, logic is a dict of other members to + compare to. All other member tests must be True + + """ + result = True + for other_member, test in logic.items(): + result &= self.test_logic(self.get_member(other_member), test) + + return result + + def execute(self, test): + """Subject is an object, test is a dict of {member: test} pairs + to perform on subject's members. Wach key in test is a member + of subject. + + """ + + for member_name, logic in test.items(): + result = self.test_logic(self.get_member(member_name), logic) + print('member %s is %s' % (member_name, result)) + +# A couple of name aliases class Validation(Logician): - pass + pass class Visibility(Logician): - pass + pass