diff --git a/release/scripts/modules/extensions_framework/__init__.py b/release/scripts/modules/extensions_framework/__init__.py new file mode 100644 index 00000000000..fdb7ddd706d --- /dev/null +++ b/release/scripts/modules/extensions_framework/__init__.py @@ -0,0 +1,156 @@ +# -*- coding: utf8 -*- +# +# ***** BEGIN GPL LICENSE BLOCK ***** +# +# -------------------------------------------------------------------------- +# Blender 2.5 Extensions Framework +# -------------------------------------------------------------------------- +# +# Authors: +# Doug Hammond +# +# 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, see . +# +# ***** END GPL LICENCE BLOCK ***** +# + +import os, time +import bpy + +#---------------------------------------------------------------------------------------------------------------------- + +def log(str, popup=False, module_name='EF'): + 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 + ) + +#---------------------------------------------------------------------------------------------------------------------- + +from .ui import EF_OT_msg + +bpy.types.register(EF_OT_msg) +ef_path = os.path.realpath( os.path.dirname(__file__) ) +# log('Extensions_Framework detected and loaded from %s'%ef_path) + +del EF_OT_msg, os + +#---------------------------------------------------------------------------------------------------------------------- + +class ef(object): + ''' + Extensions Framework base class + ''' + + added_property_cache = {} + + +def init_properties(obj, props, cache=True): + if not obj in ef.added_property_cache.keys(): + ef.added_property_cache[obj] = [] + + for prop in props: + if cache and prop['attr'] in ef.added_property_cache[obj]: + continue + try: + if prop['type'] == 'bool': + t = bpy.props.BoolProperty + a = {k: v for k,v in prop.items() if k in ['name','description','default']} + elif prop['type'] == 'collection': + t = bpy.props.CollectionProperty + a = {k: v for k,v in prop.items() if k in ["ptype", "name", "description"]} + 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"]} + elif prop['type'] == 'float': + t = bpy.props.FloatProperty + a = {k: v for k,v in prop.items() if k in ["name", "description", "min", "max", "soft_min", "soft_max", "default", "precision"]} + elif prop['type'] == 'float_vector': + t = bpy.props.FloatVectorProperty + a = {k: v for k,v in prop.items() if k in ["name", "description", "min", "max", "soft_min", "soft_max", "default", "precision", "size", "subtype"]} + elif prop['type'] == 'int': + t = bpy.props.IntProperty + a = {k: v for k,v in prop.items() if k in ["name", "description", "min", "max", "soft_min", "soft_max", "default"]} + elif prop['type'] == 'pointer': + t = bpy.props.PointerProperty + a = {k: v for k,v in prop.items() if k in ["ptype", "name", "description"]} + 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", "maxlen", "default", "subtype"]} + else: + #ef.log('Property type not recognised: %s' % prop['type']) + continue + + setattr(obj, prop['attr'], t(**a)) + + ef.added_property_cache[obj].append(prop['attr']) + #log('Created property %s.%s' % (obj, prop['attr'])) + except KeyError: + continue + +class declarative_property_group(bpy.types.IDPropertyGroup): + + controls = [ + # 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 + ] + + # Include some properties in display based on values of others + visibility = { + # See ef.validate for test syntax + } + + # engine-specific properties to create in the scene. + # Each item should be a dict of args to pass to a + # bpy.types.Scene.Property function, with the exception + # of 'type' which is used and stripped by ef + properties = [ + # example: + #{ + # 'type': 'int', + # 'attr': 'threads', + # 'name': 'Render Threads', + # 'description': 'Number of threads to use', + # 'default': 1, + # 'min': 1, + # 'soft_min': 1, + # 'max': 64, + # 'soft_max': 64 + #}, + ] + + def draw_callback(self, context): + ''' + Sub-classes can override this to get a callback + when rendered by a property_group_renderer class + ''' + + pass + + @classmethod + def get_exportable_properties(cls): + out = [] + for prop in cls.properties: + if 'save_in_preset' in prop.keys() and prop['save_in_preset']: + out.append(prop) + return out diff --git a/release/scripts/modules/extensions_framework/engine.py b/release/scripts/modules/extensions_framework/engine.py new file mode 100644 index 00000000000..448eb715bef --- /dev/null +++ b/release/scripts/modules/extensions_framework/engine.py @@ -0,0 +1,37 @@ +# -*- coding: utf8 -*- +# +# ***** BEGIN GPL LICENSE BLOCK ***** +# +# -------------------------------------------------------------------------- +# Blender 2.5 Extensions Framework +# -------------------------------------------------------------------------- +# +# Authors: +# Doug Hammond +# +# 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, see . +# +# ***** END GPL LICENCE BLOCK ***** +# +from .plugin import plugin + +class engine_base(plugin): + ''' + Render Engine plugin base class + ''' + + bl_label = 'Abstract Render Engine Base Class' + + def render(self, scene): + pass diff --git a/release/scripts/modules/extensions_framework/outputs/__init__.py b/release/scripts/modules/extensions_framework/outputs/__init__.py new file mode 100644 index 00000000000..f05ed25fbad --- /dev/null +++ b/release/scripts/modules/extensions_framework/outputs/__init__.py @@ -0,0 +1,26 @@ +# -*- coding: utf8 -*- +# +# ***** BEGIN GPL LICENSE BLOCK ***** +# +# -------------------------------------------------------------------------- +# Blender 2.5 Extensions Framework +# -------------------------------------------------------------------------- +# +# Authors: +# Doug Hammond +# +# 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, see . +# +# ***** END GPL LICENCE BLOCK ***** +# diff --git a/release/scripts/modules/extensions_framework/outputs/xml_output.py b/release/scripts/modules/extensions_framework/outputs/xml_output.py new file mode 100644 index 00000000000..4d1af9cc877 --- /dev/null +++ b/release/scripts/modules/extensions_framework/outputs/xml_output.py @@ -0,0 +1,96 @@ +# -*- coding: utf8 -*- +# +# ***** BEGIN GPL LICENSE BLOCK ***** +# +# -------------------------------------------------------------------------- +# Blender 2.5 Extensions Framework +# -------------------------------------------------------------------------- +# +# Authors: +# Doug Hammond +# +# 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, see . +# +# ***** END GPL LICENCE BLOCK ***** +# +import xml.etree.cElementTree as ET +import xml.dom.minidom as MD + +class xml_output(object): + + format = {} + + def __str__(self): + return ET.tostring(self.root) + + def write_pretty(self, file): + xml_dom = MD.parseString(ET.tostring(self.root, encoding='utf-8')) + xml_dom.writexml(file, addindent=' ', newl='\n', encoding='utf-8') + + def pretty(self): + xml_str = MD.parseString(ET.tostring(self.root)) + return xml_str.toprettyxml() + + # This should be overridden in classes that produce XML conditionally + def get_format(self): + return self.format + + def compute(self, context): + self.context = context + + self.root = ET.Element(self.root_element) + self.parse_dict(self.get_format(), self.root) + #ET.dump(root) + + return self.root + + def make_subelement(self, elem, name): + return ET.SubElement(elem, name) + + format_types = { + 'bool': lambda c,x: str(x).lower(), + 'collection': lambda c,x: x, + 'enum': lambda c,x: x, + 'float': lambda c,x: x, + 'int': lambda c,x: x, + 'pointer': lambda c,x: x, + 'string': lambda c,x: x, + } + + def parse_dict(self, d, elem): + for key in d.keys(): + # tuple provides multiple child elements + if type(d[key]) is tuple: + for cd in d[key]: + self.parse_dict({key:cd}, elem) + continue # don't create empty element for tuple child + + x = ET.SubElement(elem, key) + + # dictionary provides nested elements + if type(d[key]) is dict: + self.parse_dict(d[key], x) + + # list provides direct value insertion + elif type(d[key]) is list: + x.text = ' '.join([str(i) for i in d[key]]) + + # else look up property + else: + for p in self.properties: + if d[key] == p['attr']: + if 'compute' in p.keys(): + x.text = str(p['compute'](self.context, self)) + else: + x.text = str(self.format_types[p['type']](self.context, getattr(self, d[key]))) diff --git a/release/scripts/modules/extensions_framework/plugin.py b/release/scripts/modules/extensions_framework/plugin.py new file mode 100644 index 00000000000..b0042aad40d --- /dev/null +++ b/release/scripts/modules/extensions_framework/plugin.py @@ -0,0 +1,72 @@ +# -*- coding: utf8 -*- +# +# ***** BEGIN GPL LICENSE BLOCK ***** +# +# -------------------------------------------------------------------------- +# Blender 2.5 Extensions Framework +# -------------------------------------------------------------------------- +# +# Authors: +# Doug Hammond +# +# 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, see . +# +# ***** END GPL LICENCE BLOCK ***** +# +from . import init_properties +from . import log + +import bpy + +class plugin(object): + + # List of IDPropertyGroup types to create in the scene + property_groups = [ + # ('bpy.type prototype to attach to. eg. Scene', ) + ] + + @classmethod + def install(r_class): + # create custom property groups + for property_group_parent, property_group in r_class.property_groups: + call_init = False + if property_group_parent is not None: + prototype = getattr(bpy.types, property_group_parent) + if not hasattr(prototype, property_group.__name__): + init_properties(prototype, [{ + 'type': 'pointer', + 'attr': property_group.__name__, + 'ptype': property_group, + 'name': property_group.__name__, + 'description': property_group.__name__ + }]) + call_init = True + #print('Created IDPropertyGroup %s.%s' % (prototype, property_group.__name__)) + else: + call_init = True + + if call_init: + init_properties(property_group, property_group.properties) + #print('Initialised IDPropertyGroup %s' % property_group.__name__) + + log('Render Engine "%s" initialised' % r_class.bl_label) + + @classmethod + def uninstall(r_class): + # unregister property groups in reverse order + reverse_property_groups = [p for p in r_class.property_groups] + reverse_property_groups.reverse() + for property_group_parent, property_group in reverse_property_groups: + prototype = getattr(bpy.types, property_group_parent) + prototype.RemoveProperty(property_group.__name__) diff --git a/release/scripts/modules/extensions_framework/ui.py b/release/scripts/modules/extensions_framework/ui.py new file mode 100644 index 00000000000..5d29ed7245a --- /dev/null +++ b/release/scripts/modules/extensions_framework/ui.py @@ -0,0 +1,181 @@ +# -*- coding: utf8 -*- +# +# ***** BEGIN GPL LICENSE BLOCK ***** +# +# -------------------------------------------------------------------------- +# Blender 2.5 Extensions Framework +# -------------------------------------------------------------------------- +# +# Authors: +# Doug Hammond +# +# 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, see . +# +# ***** END GPL LICENCE BLOCK ***** +# +import bpy + +from .validate import Visibility + +class EF_OT_msg(bpy.types.Operator): + 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): + if context is not None: + for p in path: + context = getattr(context, p) + return context + +class property_group_renderer(object): + + # Choose which custom property groups this panel should draw, and + # where to find that property group in the active context + display_property_groups = [ + # ( ('scene',), 'declarative_property_group name') + ] + + def draw(self, context): + ''' + Sub-classes should override this if they need to display + other (object-related) property groups + ''' + 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) + + @staticmethod + def property_reload(): + ''' + override this in sub classes to force data refresh upon scene reload + ''' + pass + + def check_visibility(self, lookup_property, context, property_group): + vt = Visibility(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 draw_column(self, control_list_item, layout, context, supercontext=None, property_group=None): + if type(control_list_item) is list: + do_split = 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 control_list_item: + do_split = do_split and self.check_visibility(ssp, context, property_group) + else: + do_split = do_split or self.check_visibility(sp, context, property_group) + + if do_split: + # print('split %s'%p) + if found_percent is not None: + fp = { 'percentage': found_percent } + splt = layout.split(**fp) + else: + splt = layout.row(True) + for sp in [s for s in control_list_item if type(s) in [str, list] ]: + col2 = splt.column() + self.draw_column(sp, col2, context, supercontext, property_group) + #else: + # print('dont split %s'%p) + else: + if self.check_visibility(control_list_item, context, property_group): + + for current_property in property_group.properties: + if current_property['attr'] == control_list_item: + current_property_keys = current_property.keys() + if 'type' in current_property_keys: + + if current_property['type'] in ['int', 'float', 'float_vector', 'enum', '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 ['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']: + layout.operator(current_property['operator'], + text = current_property['text'], + icon = current_property['icon'] + ) + + elif current_property['type'] in ['text']: + layout.label( + text = current_property['name'] + ) + + elif current_property['type'] in ['template_list']: + # row.template_list(idblock, "texture_slots", idblock, "active_texture_index", rows=2) + 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']: + # split.prop_search(tex, "uv_layer", ob.data, "uv_textures", text="") + 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'], + ) + else: + layout.prop(property_group, control_list_item) + + # Fire a draw callback if specified + if 'draw' in current_property_keys: + current_property['draw'](supercontext, context) + + break diff --git a/release/scripts/modules/extensions_framework/util.py b/release/scripts/modules/extensions_framework/util.py new file mode 100644 index 00000000000..8bbeaf74ec3 --- /dev/null +++ b/release/scripts/modules/extensions_framework/util.py @@ -0,0 +1,211 @@ +# -*- coding: utf8 -*- +# +# ***** BEGIN GPL LICENSE BLOCK ***** +# +# -------------------------------------------------------------------------- +# Blender 2.5 Extensions Framework +# -------------------------------------------------------------------------- +# +# Authors: +# Doug Hammond +# +# 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# +# ***** END GPL LICENCE BLOCK ***** +# +import datetime, os, configparser, threading, tempfile + +import bpy + +config_paths = bpy.utils.script_paths() + +''' +This path is set at the start of export, so that +calls to path_relative_to_export() can make all +exported paths relative to 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) + try: + relp = os.path.relpath(p, os.path.dirname(export_path)) + except ValueError: # path on different drive on windows + relp = p + + #print('Resolving rel path %s -> %s' % (p, relp)) + + 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) + + #print('Resolving FS path %s -> %s' % (p,pout)) + + return pout.replace('\\', '/') + +# TODO: - somehow specify TYPES to get/set from config + +def find_config_value(module, section, key, 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): + 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 + ''' + 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() + +def temp_file(ext='tmp'): + ''' + Get a temporary filename with the given extension + ''' + tf, fn = tempfile.mkstemp(suffix='.%s'%ext) + os.close(tf) + return fn + +class TimerThread(threading.Thread): + ''' + Periodically call self.kick() + ''' + 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): + self.KICK_PERIOD = period + self.STARTUP_DELAY + + def stop(self): + 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): + 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) diff --git a/release/scripts/modules/extensions_framework/validate.py b/release/scripts/modules/extensions_framework/validate.py new file mode 100644 index 00000000000..b2960fcf01f --- /dev/null +++ b/release/scripts/modules/extensions_framework/validate.py @@ -0,0 +1,208 @@ +# -*- coding: utf8 -*- +# +# ***** BEGIN GPL LICENSE BLOCK ***** +# +# -------------------------------------------------------------------------- +# Blender 2.5 Extensions Framework +# -------------------------------------------------------------------------- +# +# Authors: +# Doug Hammond +# +# 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, see . +# +# ***** END GPL LICENCE BLOCK ***** +# +''' +Pure logic and validation class. + +By using a Subject object, and a dict of described logic tests, it +is possible to arrive at a True or False result for various purposes: +1. Data validation +2. UI control visibility + +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' + + +Tests are described thus: + +Use the special list types Logic_AND and Logic_OR to describe combinations +of values and other members. Use Logic_Operator for 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}]) ]) +} + +# 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}]) ]) +} + +# With regards to Subject, this test is invalid +TESTC = { + 'n': 0 +} + +# Tests are executed thus: +S = Subject() +L = Logician(S) +L.execute(TESTA) + +''' + +class Logic_AND(list): + pass +class Logic_OR(list): + pass +class Logic_Operator(dict): + 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 is 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']: + 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. + each 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 +class Visibility(Logician): + pass