Add release/scripts/modules/extensions_framework (formerly known as exporter_framework).

This commit is contained in:
Doug Hammond 2010-10-22 18:55:10 +00:00
parent dad9423dd6
commit a77301839d
8 changed files with 987 additions and 0 deletions

@ -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 <http://www.gnu.org/licenses/>.
#
# ***** 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

@ -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 <http://www.gnu.org/licenses/>.
#
# ***** 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

@ -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 <http://www.gnu.org/licenses/>.
#
# ***** END GPL LICENCE BLOCK *****
#

@ -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 <http://www.gnu.org/licenses/>.
#
# ***** 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])))

@ -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 <http://www.gnu.org/licenses/>.
#
# ***** 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', <declarative_property_group type>)
]
@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__)

@ -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 <http://www.gnu.org/licenses/>.
#
# ***** 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

@ -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)

@ -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 <http://www.gnu.org/licenses/>.
#
# ***** 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<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.
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