Moved extensions_framework into addons/modules

This commit is contained in:
Doug Hammond 2010-12-11 16:35:11 +00:00
parent 1d1c8bb21b
commit 2b772739fb
8 changed files with 0 additions and 1159 deletions

@ -1,195 +0,0 @@
# -*- 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 time
import bpy
from extensions_framework.ui import EF_OT_msg
bpy.types.register(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
)
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']}
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:
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.IDPropertyGroup):
"""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.
"""
"""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 display of properties based on
the value of other properties. See extensions_framework.validate
for test syntax.
"""
visibility = {}
"""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

@ -1,39 +0,0 @@
# -*- 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 extensions_framework.plugin import plugin
class engine_base(plugin):
"""Render Engine plugin base class
TODO: Remove, this class hasn't grown to be useful
"""
bl_label = 'Abstract Render Engine Base Class'
def render(self, scene):
pass

@ -1,26 +0,0 @@
# -*- 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 *****
#

@ -1,116 +0,0 @@
# -*- 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):
"""This class serves to describe an XML output, it uses
cElementTree and minidom to construct and format the XML
data.
"""
"""The format dict describes the XML structure that this class
should generate, and which properties should be used to fill
the XML data structure
"""
format = {}
def __str__(self):
return ET.tostring(self.root)
def write_pretty(self, file):
"""Write a formatted XML string to 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):
"""Return a formatted XML string"""
xml_str = MD.parseString(ET.tostring(self.root))
return xml_str.toprettyxml()
def get_format(self):
"""This should be overridden in classes that produce XML
conditionally
"""
return self.format
def compute(self, context):
"""Compute the XML output from the input format"""
self.context = context
self.root = ET.Element(self.root_element)
self.parse_dict(self.get_format(), self.root)
return self.root
"""Formatting functions for various data types"""
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):
"""Parse the values in the format dict and collect the
formatted data into XML structure starting at self.root
"""
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]))
)

@ -1,94 +0,0 @@
# -*- 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 extensions_framework import init_properties
from extensions_framework import log
class plugin(object):
"""Base class for plugins which wish to make use of utilities
provided in extensions_framework. Using the property_groups
attribute and the install() and uninstall() methods, a large number
of custom scene properties can be easily defined, displayed and
managed.
TODO: Rename, 'extension' would be more appropriate than 'plugin'
"""
"""The property_groups defines a list of declarative_property_group
types to create in specified types during the initialisation of the
plugin.
Item format:
('bpy.type prototype to attach to', <declarative_property_group>)
Example item:
('Scene', myaddon_property_group)
In this example, a new property group will be attached to
bpy.types.Scene and all of the properties described in that group
will be added to it.
See extensions_framework.declarative_property_group.
"""
property_groups = []
@classmethod
def install(r_class):
"""Initialise this plugin. So far, all this does is to create
custom property groups specified in the property_groups
attribute.
"""
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
else:
call_init = True
if call_init:
init_properties(property_group, property_group.properties)
log('Extension "%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__)

@ -1,253 +0,0 @@
# -*- 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 extensions_framework.validate import Visibility
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'}
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
class property_group_renderer(object):
"""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 = 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
# tab_level = 0
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):
# self.tab_level += 1
"""Draw a column's worth of UI controls in this Panel"""
if type(control_list_item) is list:
draw_row = False
found_percent = None
# print('\t'*self.tab_level, '--', property_group, '--')
for sp in control_list_item:
# print('\t'*self.tab_level, sp)
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)
# print('\t'*self.tab_level, 'List: ', draw_row)
else:
draw_row = draw_row or self.check_visibility(sp,
property_group)
# print('\t'*self.tab_level, 'Single: ', draw_row)
# print('\t'*self.tab_level, '-->', draw_row)
# print('\t'*self.tab_level, '--', 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()
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']:
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'],
)
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
# self.tab_level -= 1

@ -1,223 +0,0 @@
# -*- 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 configparser
import datetime
import os
import tempfile
import threading
import bpy
"""List of possibly appropriate paths to load/save addon config from/to"""
config_paths = []
if bpy.utils.user_resource('CONFIG') != "": config_paths.append(bpy.utils.user_resource('CONFIG'))
if bpy.utils.user_resource('SCRIPTS') != "": config_paths.append(bpy.utils.user_resource('SCRIPTS'))
for pth in bpy.utils.script_paths():
if pth != "": config_paths.append(pth)
"""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
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('\\', '/')
# 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
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
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)
def temp_directory():
"""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
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
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)

@ -1,213 +0,0 @@
# -*- 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
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
class Visibility(Logician):
pass