extensions_framework: lots of docs and code formatting to be more pep8-like

This commit is contained in:
Doug Hammond 2010-11-17 21:28:22 +00:00
parent b99a11bc3c
commit 9183f20fb4
7 changed files with 385 additions and 226 deletions

@ -24,131 +24,170 @@
#
# ***** END GPL LICENCE BLOCK *****
#
import time
import os, 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("[%s %s] %s" % (module_name, time.strftime('%Y-%b-%d %H:%M:%S'), str))
"""Print a message to the console, prefixed with the module_name
and the current time. If the popup flag is True, the message will
be raised in the UI as a warning using the operator bpy.ops.ef.msg.
"""
print("[%s %s] %s" %
(module_name, time.strftime('%Y-%b-%d %H:%M:%S'), str))
if popup:
bpy.ops.ef.msg(
msg_type='WARNING',
msg_text=str
)
#----------------------------------------------------------------------------------------------------------------------
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 = {}
added_property_cache = {}
def init_properties(obj, props, cache=True):
if not obj in ef.added_property_cache.keys():
ef.added_property_cache[obj] = []
"""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:
if cache and prop['attr'] in ef.added_property_cache[obj]:
continue
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']}
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 = {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"]}
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"]}
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"]}
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"]}
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 = {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"]}
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']))
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.
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
]
The properties list attribute in this class describes the
properties present in this group.
# Include some properties in display based on values of others
visibility = {
# See ef.validate for test syntax
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
}
# 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
#},
]
"""
properties = []
def draw_callback(self, context):
'''
Sub-classes can override this to get a callback
when rendered by a property_group_renderer class
'''
"""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']:

@ -24,12 +24,14 @@
#
# ***** END GPL LICENCE BLOCK *****
#
from .plugin import plugin
from extensions_framework.plugin import plugin
class engine_base(plugin):
'''
Render Engine plugin base class
'''
"""Render Engine plugin base class
TODO: Remove, this class hasn't grown to be useful
"""
bl_label = 'Abstract Render Engine Base Class'

@ -28,36 +28,49 @@ 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()
# This should be overridden in classes that produce XML conditionally
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)
#ET.dump(root)
return self.root
def make_subelement(self, elem, name):
return ET.SubElement(elem, name)
"""Formatting functions for various data types"""
format_types = {
'bool': lambda c,x: str(x).lower(),
'collection': lambda c,x: x,
@ -67,8 +80,12 @@ class xml_output(object):
'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:
@ -93,4 +110,7 @@ class xml_output(object):
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])))
x.text = str(
self.format_types[p['type']](self.context,
getattr(self, d[key]))
)

@ -24,21 +24,45 @@
#
# ***** END GPL LICENCE BLOCK *****
#
from . import init_properties
from . import log
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.
# List of IDPropertyGroup types to create in the scene
property_groups = [
# ('bpy.type prototype to attach to. eg. Scene', <declarative_property_group type>)
]
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):
# create custom property groups
"""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:
@ -52,19 +76,17 @@ class plugin(object):
'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('Extension "%s" initialised' % r_class.bl_label)
@classmethod
def uninstall(r_class):
# unregister property groups in reverse order
"""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:

@ -26,9 +26,10 @@
#
import bpy
from .validate import Visibility
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')
@ -38,50 +39,81 @@ class EF_OT_msg(bpy.types.Operator):
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().
# 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')
]
"""
"""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
'''
for property_group_path, property_group_name in self.display_property_groups:
"""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)
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
'''
"""Override this in sub classes to force data refresh upon scene reload
TODO: Remove, this is not used anywhere
"""
pass
def check_visibility(self, lookup_property, context, property_group):
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])
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):
def draw_column(self, control_list_item, layout, context,
supercontext=None, property_group=None):
"""Draw a column's worth of UI controls in this Panel"""
if type(control_list_item) is list:
do_split = False
@ -91,85 +123,117 @@ class property_group_renderer(object):
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)
do_split = do_split and self.check_visibility(ssp,
property_group)
else:
do_split = do_split or self.check_visibility(sp, context, property_group)
do_split = do_split or self.check_visibility(sp,
property_group)
if do_split:
# print('split %s'%p)
if found_percent is not None:
fp = { 'percentage': found_percent }
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] ]:
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)
self.draw_column(sp, col2, context, supercontext,
property_group)
else:
if self.check_visibility(control_list_item, context, property_group):
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']:
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,
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,
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']
text = current_property['text'],
icon = current_property['icon']
)
elif current_property['type'] in ['text']:
layout.label(
text = current_property['name']
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']
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'](supercontext,
context),
current_property['trg_attr'],
current_property['src'](supercontext, context),
current_property['src'](supercontext,
context),
current_property['src_attr'],
text = current_property['name'],
text = current_property['name'],
)
else:
layout.prop(property_group, control_list_item)

@ -25,23 +25,24 @@
#
# ***** END GPL LICENCE BLOCK *****
#
import datetime, os, configparser, threading, tempfile
import configparser
import datetime
import os
import tempfile
import threading
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
'''
"""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
'''
"""Return a path that is relative to the export path"""
global export_path
p = filesystem_path(p)
try:
@ -49,26 +50,25 @@ def path_relative_to_export(p):
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
'''
"""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):
"""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:
@ -97,6 +97,10 @@ def find_config_value(module, section, key, default):
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:
@ -104,7 +108,8 @@ def write_config_value(module, section, key, value):
fc.append( '/'.join([p, '%s.cfg' % module]))
if len(fc) < 1:
raise Exception('Cannot find a writable path to store %s config file' % module)
raise Exception('Cannot find a writable path to store %s config file' %
module)
cp = configparser.SafeConfigParser()
@ -130,32 +135,35 @@ def write_config_value(module, section, key, value):
return True
def scene_filename():
'''
Construct a safe 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 the system temp directory"""
return tempfile.gettempdir()
def temp_file(ext='tmp'):
'''
Get a temporary filename with the given extension
'''
"""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()
'''
"""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
@ -169,24 +177,27 @@ class TimerThread(threading.Thread):
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
'''
"""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
@ -194,15 +205,11 @@ class TimerThread(threading.Thread):
self.kick()
def kick(self):
'''
sub-classes do their work here
'''
"""Sub-classes do their work here"""
pass
def format_elapsed_time(t):
'''
Format a duration in seconds as an HH:MM:SS format time
'''
"""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

@ -24,7 +24,7 @@
#
# ***** END GPL LICENCE BLOCK *****
#
'''
"""
Pure logic and validation class.
By using a Subject object, and a dict of described logic tests, it
@ -45,10 +45,11 @@ class Subject(object):
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.
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:
With regards to Subject, each of these evaluate to True:
TESTA = {
'a': 0,
'c': Logic_OR([ 'foo', 'bar' ]),
@ -58,7 +59,7 @@ TESTA = {
'g': Logic_OR([ 'baz', Logic_AND([{'b': 1}, {'f': 8}]) ])
}
# With regards to Subject, each of these evaluate to False:
With regards to Subject, each of these evaluate to False:
TESTB = {
'a': 'foo',
'c': Logic_OR([ 'bar', 'baz' ]),
@ -68,17 +69,17 @@ TESTB = {
'g': Logic_OR([ 'baz', Logic_AND([{'b':0}, {'f': 8}]) ])
}
# With regards to Subject, this test is invalid
With regards to Subject, this test is invalid
TESTC = {
'n': 0
}
# Tests are executed thus:
Tests are executed thus:
S = Subject()
L = Logician(S)
L.execute(TESTA)
'''
"""
class Logic_AND(list):
pass
@ -88,31 +89,28 @@ 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.
'''
"""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.
'''
"""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
'''
"""Find the type of test to run on member, and perform that test"""
if type(logic) is dict:
return self.test_dict(member, logic)
@ -123,17 +121,21 @@ class Logician(object):
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}))
# 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
'''
"""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 ?
"""
# 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():
@ -161,9 +163,10 @@ class Logician(object):
return result
def test_or(self, member, logic):
'''
member is a value, logic is a set of values, ANY of which can be True
'''
"""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)
@ -171,9 +174,10 @@ class Logician(object):
return result
def test_and(self, member, logic):
'''
member is a value, logic is a list of values, ALL of which must be True
'''
"""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)
@ -181,9 +185,10 @@ class Logician(object):
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
'''
"""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)
@ -191,11 +196,11 @@ class Logician(object):
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.
'''
"""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)