Skip to content
Snippets Groups Projects
Commit e33f3c42 authored by Paul McCarthy's avatar Paul McCarthy
Browse files

Constraints can be set for individual HasProperties instances, for...

Constraints can be set for individual HasProperties instances, for PropertyBase validation. The List property is now probably broken because of this.
parent c839b459
Branches
Tags
No related merge requests found
......@@ -44,7 +44,7 @@
#
#
# # Remove a previously added listener
# myPropObj.removeListener('myListener')
# >>> myPropObj.removeListener('myListener')
#
#
# Lots of the code in this file is probably very confusing. First of
......@@ -69,7 +69,7 @@
# as class attributes. When an instance of the HasProperties class is created,
# one or more PropertyValue objects are created for each of the PropertyBase
# instances. For most properties, there is a one-to-one mapping between
# ProperyyValue instances and PropertyBase instances (for each HasProperties
# PropertyValue instances and PropertyBase instances (for each HasProperties
# instance), however this is not mandatory. For example, the List property
# manages multiple PropertyValue objects for each HasProperties instance.
......@@ -81,6 +81,14 @@
# value to be set to something invalid, but it will tell registered listeners
# whether the new value is valid or invalid.
#
# The default validation logic of most PropertyBase objects can be configured
# via 'constraints'. For example, the Number property allows 'minval' and
# 'maxval' constraints to be set. These may be set via PropertyBase
# constructors, (i.e. when it is defined as a class attribute of a
# HasProperties definition), and may be queried and changed on individual
# HasProperties instances via the getConstraint/setConstraint methods,
# which are available on both PropertyBase and HasProperties objects.
#
# Application code may be notified of property changes in two ways. First, a
# listener may be registered with a PropertyBase object, either via the
# HasProperties.addListener instance method, or the PropertyBase.addListener
......@@ -96,10 +104,10 @@
# method. This listener will then only be notified of changes to that
# PropertyValue object.
#
#
# author: Paul McCarthy <pauldmccarthy@gmail.com>
#
import types
import logging
from collections import OrderedDict
......@@ -114,17 +122,24 @@ class PropertyValue(object):
of a HasProperties instance.
"""
def __init__(self, prop, owner, value, name=None):
def __init__(self, prop, owner, value, name=None, allowInvalid=True):
"""
Parameters:
- prop: The PropertyBase object which manages this
PropertyValue.
- owner: The HasProperties object, the owner of the
prop property.
- value: Initial value.
- name: Variable name - if not provided, a default,
unique name is created.
- prop: The PropertyBase object which manages this
PropertyValue.
- owner: The HasProperties object, the owner of the
prop property.
- value: Initial value.
- name: Variable name - if not provided, a default,
unique name is created.
- allowInvalid: If False, any attempt to set the value to
something invalid will result in a ValueError.
TODO - not supported yet.
"""
if name is None: name = '{}_{}'.format(prop.label, id(self))
......@@ -136,6 +151,7 @@ class PropertyValue(object):
self._value = value
self._lastValue = value
self._lastValid = self.isValid()
self._allowInvalid = allowInvalid
def addListener(self, name, callback):
......@@ -221,7 +237,10 @@ class PropertyValue(object):
"""
try: self.prop.validate(self.owner, self.get())
except: return False
except ValueError as e:
log.debug('Value for {} ({}) is invalid: {}'.format(
self.name, self.get(), e))
return False
return True
......@@ -250,7 +269,7 @@ class PropertyValue(object):
# Notify all listeners, ignoring any errors -
# it is up to the listeners to ensure that
# they handle invalid values
for (name,func) in listeners:
for (name, func) in listeners:
log.debug('Notifying listener on {}: {}'.format(self.name, name))
......@@ -260,6 +279,20 @@ class PropertyValue(object):
self.name, name, e))
class InstanceData(object):
"""
An InstanceData object is created for every instance which has
one of these PropertyBase objects as a class attribute. It stores
listener callback functions, which are notified whenever the
property value changes, and the property constraints used to
test validity.
"""
def __init__(self, instance, **constraints):
self.instance = instance
self.changeListeners = OrderedDict()
self.constraints = constraints.copy()
class PropertyBase(object):
"""
The base class for properties. For every object which has this
......@@ -286,14 +319,15 @@ class PropertyBase(object):
- Override whatever you want for advanced usage (see
properties_types.List for an example).
"""
def __init__(self,
default=None,
required=False,
validateFunc=None,
preNotifyFunc=None):
preNotifyFunc=None,
allowInvalid=True,
**constraints):
"""
Parameters:
......@@ -316,13 +350,21 @@ class PropertyBase(object):
value(s) changes. This function is called
by the PropertyValue object(s) before any
listeners are notified.
- allowInvalid: If False, a ValueError will be raised on
all attempts to set this property to an
invalid value. TODO - not supported yet.
- constraints:
"""
self.label = None
self.default = default
self.required = required
self.validateFunc = validateFunc
self.preNotifyFunc = preNotifyFunc
self.changeListeners = OrderedDict()
self.label = None
self.default = default
self.required = required
self.validateFunc = validateFunc
self.preNotifyFunc = preNotifyFunc
self.allowInvalid = allowInvalid
self.instanceData = {}
self.defaultConstraints = constraints
def addListener(self, instance, name, callback):
......@@ -332,15 +374,12 @@ class PropertyBase(object):
for required callback function signature.
"""
if instance not in self.changeListeners:
self.changeListeners[instance] = {}
log.debug('Adding listener on {}: {}'.format(self.label, name))
fullname = 'PropertyBase_{}_{}'.format(self.label, name)
self.changeListeners[instance][fullname] = callback
self.instanceData[instance].changeListeners[fullname] = callback
def removeListener(self, instance, name):
"""
......@@ -349,12 +388,9 @@ class PropertyBase(object):
fullname = 'PropertyBase_{}_{}'.format(self.label, name)
if instance not in self.changeListeners: return
if fullname not in self.changeListeners[instance]: return
log.debug('Removing listener on {}: {}'.format(self.label, name))
self.changeListeners[instance].pop(fullname)
self.instanceData[instance].changeListeners.pop(fullname)
def forceValidation(self, instance):
......@@ -382,15 +418,37 @@ class PropertyBase(object):
this property, (and to the associated HasProperties instance).
"""
if instance not in self.changeListeners: return
listeners = self.changeListeners[instance].items()
listeners = self.instanceData[instance].changeListeners.items()
for (lName, func) in listeners:
log.debug('Notifying listener on {}: {}'.format(self.label, lName))
func(value, valid, instance, prop, name)
def _newInstance(self, instance):
"""
Creates an InstanceData object for the new instance, and stores it
in self.instanceData.
"""
instanceData = InstanceData(instance, **self.defaultConstraints)
self.instanceData[instance] = instanceData
def getConstraint(self, instance, constraint):
"""
Returns the value of the named constraint for the specified instance.
"""
return self.instanceData[instance].constraints[constraint]
def setConstraint(self, instance, constraint, value):
"""
Sets the value of the named constraint for the specified instance.
"""
self.instanceData[instance].constraints[constraint] = value
def _makePropVal(self, instance):
"""
......@@ -399,7 +457,8 @@ class PropertyBase(object):
listener on the PropertyValue object.
"""
instval = PropertyValue(self, instance, self.default, self.label)
instval = PropertyValue(
self, instance, self.default, self.label, self.allowInvalid)
instance.__dict__[self.label] = instval
instval.addListener('internal', self._varChanged)
......@@ -459,8 +518,14 @@ class PropertyBase(object):
return self
instval = self.getPropVal(instance)
if instval is None: instval = self._makePropVal(instance)
# new instance - create an InstanceData object
# to store the instance metadata and a
# PropertyValue object to store the property
# value for the instance
if instval is None:
self._newInstance(instance)
instval = self._makePropVal(instance)
return instval.get()
......@@ -529,6 +594,22 @@ class HasProperties(object):
return self.getProp(propName).getPropVal(self)
def getConstraint(self, propName, constraint):
"""
Convenience method, returns the value of the named constraint for the
named property. See PropertyBase.setConstraint.
"""
return self.getProp(propName).getConstraint(self, constraint)
def setConstraint(self, propName, constraint, value):
"""
Conventience method, sets the value of the named constraint for the
named property. See PropertyBase.setConstraint.
"""
return self.getProp(propName).setConstraint(self, constraint, value)
def addListener(self, propName, listenerName, callback):
"""
Convenience method, adds the specified listener to the specified
......@@ -553,7 +634,7 @@ class HasProperties(object):
"""
props = filter(
lambda (name,prop): isinstance(prop, PropertyBase),
lambda (name, prop): isinstance(prop, PropertyBase),
self.__class__.__dict__.items())
propNames, props = zip(*props)
......@@ -583,7 +664,7 @@ class HasProperties(object):
errors = []
for name, prop in zip(names,props):
for name, prop in zip(names, props):
try:
val = getattr(self, name)
......@@ -621,7 +702,7 @@ class HasProperties(object):
clsname = self.__class__.__name__
propNames,props = self.getAllProperties()
propNames, props = self.getAllProperties()
propVals = ['{}'.format(getattr(self, propName))
for propName in propNames]
......@@ -630,7 +711,7 @@ class HasProperties(object):
lines = [clsname]
for propName,propVal in zip(propNames,propVals):
for propName, propVal in zip(propNames, propVals):
fmtStr = ' {:>' + str(maxNameLength) + '} = {}'
lines.append(fmtStr.format(propName, propVal))
......
......@@ -41,35 +41,42 @@ class Number(props.PropertyBase):
"""
def __init__(self, minval=None, maxval=None, **kwargs):
self.minval = minval
self.maxval = maxval
"""
Optional parameters:
- minval
- maxval
"""
default = kwargs.get('default', None)
if default is None:
if self.minval is not None and self.maxval is not None:
default = (self.minval + self.maxval) / 2
elif self.minval is not None:
default = self.minval
elif self.maxval is not None:
default = self.maxval
if minval is not None and maxval is not None:
default = (minval + maxval) / 2
elif minval is not None:
default = minval
elif maxval is not None:
default = maxval
else:
default = 0
kwargs['default'] = default
kwargs['minval'] = minval
kwargs['maxval'] = maxval
props.PropertyBase.__init__(self, **kwargs)
def validate(self, instance, value):
props.PropertyBase.validate(self, instance, value)
minval = self.getConstraint(instance, 'minval')
maxval = self.getConstraint(instance, 'maxval')
if self.minval is not None and value < self.minval:
raise ValueError('Must be at least {}'.format(self.minval))
if minval is not None and value < minval:
raise ValueError('Must be at least {}'.format(minval))
if self.maxval is not None and value > self.maxval:
raise ValueError('Must be at most {}'.format(self.maxval))
if maxval is not None and value > maxval:
raise ValueError('Must be at most {}'.format(maxval))
class Int(Number):
......@@ -137,10 +144,10 @@ class String(props.PropertyBase):
- minlen
- maxlen
"""
self.minlen = minlen
self.maxlen = maxlen
kwargs['default'] = kwargs.get('default', None)
kwargs['minlen'] = minlen
kwargs['maxlen'] = maxlen
props.PropertyBase.__init__(self, **kwargs)
......@@ -164,13 +171,16 @@ class String(props.PropertyBase):
if value is None: return
minlen = self.getConstraint(instance, 'minlen')
maxlen = self.getConstraint(instance, 'maxlen')
value = str(value)
if self.minlen is not None and len(value) < self.minlen:
raise ValueError('Must have length at least {}'.format(self.minlen))
if minlen is not None and len(value) < minlen:
raise ValueError('Must have length at least {}'.format(minlen))
if self.maxlen is not None and len(value) > self.maxlen:
raise ValueError('Must have length at most {}'.format(self.maxlen))
if maxlen is not None and len(value) > maxlen:
raise ValueError('Must have length at most {}'.format(maxlen))
class Choice(String):
......@@ -247,9 +257,9 @@ class FilePath(String):
if isFile is True).
"""
self.exists = exists
self.isFile = isFile
self.suffixes = suffixes
kwargs['exists'] = exists
kwargs['isFile'] = isFile
kwargs['suffixes'] = suffixes
String.__init__(self, **kwargs)
......@@ -258,29 +268,31 @@ class FilePath(String):
String.validate(self, instance, value)
if value is None: return
if value == '': return
if not self.exists: return
exists = self.getConstraint(instance, 'exists')
isFile = self.getConstraint(instance, 'isFile')
suffixes = self.getConstraint(instance, 'suffixes')
if value is None: return
if value == '': return
if not exists: return
if self.isFile:
if isFile:
exists = op.isfile(value)
matchesSuffix = any(map(lambda s: value.endswith(s),
self.suffixes))
matchesSuffix = any(map(lambda s: value.endswith(s), suffixes))
# If the file doesn't exist, it's bad
if not exists:
if not op.isfile(value):
raise ValueError('Must be a file ({})'.format(value))
# if the file exists, and matches one of
# the specified suffixes, then it's good
if len(self.suffixes) == 0 or matchesSuffix: return
if len(suffixes) == 0 or matchesSuffix: return
# Otherwise it's bad
else:
raise ValueError(
'Must be a file ending in [{}] ({})'.format(
','.join(self.suffixes), value))
','.join(suffixes), value))
elif not op.isdir(value):
raise ValueError('Must be a directory ({})'.format(value))
......@@ -428,7 +440,7 @@ class ListWrapper(object):
items = self._propVals.__getitem__(key)
if isinstance(key,slice):
if isinstance(key, slice):
return [i.get() for i in items]
else:
return items.get()
......@@ -463,7 +475,7 @@ class ListWrapper(object):
for i in self._propVals:
if i.get() == item:
c = c + 1
c = c + 1
return c
......@@ -520,8 +532,8 @@ class ListWrapper(object):
if isinstance(key, slice):
if (key.step is not None) and (key.step > 1):
raise ValueError(
'ListWrapper does not support extended slices')
raise ValueError(
'ListWrapper does not support extended slices')
indices = range(*key.indices(len(self)))
elif isinstance(key, int):
......@@ -547,7 +559,7 @@ class ListWrapper(object):
# Replace values of existing items
if newLen == oldLen:
for i,v in zip(indices, values):
for i, v in zip(indices, values):
self._propVals[i].set(v)
# Replace old PropertyValue objects with new ones.
......@@ -648,10 +660,12 @@ class List(props.PropertyBase):
return
if (self.minlen is not None) and (len(values) < self.minlen):
raise ValueError('Must have length at least {}'.format(self.minlen))
raise ValueError('Must have length at least {}'.format(
self.minlen))
if (self.maxlen is not None) and (len(values) > self.maxlen):
raise ValueError('Must have length at most {}'.format(self.maxlen))
raise ValueError('Must have length at most {}'.format(
self.maxlen))
for v in values:
self.listType.validate(instance, v)
......@@ -684,9 +698,9 @@ class List(props.PropertyBase):
instval[:] = value
# TODO This would probably be better off as a subclass of Choice. Choice
# would need to be modified to allow for values of any type, not just
# Strings. Shouldn't be a major issue.
# TODO This might be better off as a subclass of Choice. Choice
# would need to be modified to allow for values of any type, not
# just Strings. Shouldn't be a major issue.
class ColourMap(props.PropertyBase):
"""
A property which encapsulates a matplotlib.colors.Colormap.
......
......@@ -21,8 +21,6 @@ import wx
import wx.combo
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.colors as mplcolors
import matplotlib.cm as mplcm
# the List property is complex enough to get its own module.
......@@ -91,7 +89,7 @@ def _propBind(hasProps, propObj, propVal, guiObj, evType, labelMap=None):
valMap = None
if labelMap is not None:
valMap = dict([(lbl,val) for (val,lbl) in labelMap.items()])
valMap = dict([(lbl, val) for (val, lbl) in labelMap.items()])
def _guiUpdate(value, *a):
"""
......@@ -206,18 +204,21 @@ def _FilePath(parent, hasProps, propObj, propVal):
panel.SetSizer(sizer)
panel.SetAutoLayout(1)
sizer.Fit(panel)
exists = propObj.getConstraint(hasProps, 'exists')
isFile = propObj.getConstraint(hasProps, 'isFile')
def _choosePath(ev):
global _lastFilePathDir
if propObj.exists and propObj.isFile:
if exists and isFile:
dlg = wx.FileDialog(parent,
message='Choose file',
defaultDir=_lastFilePathDir,
defaultFile=value,
style=wx.FD_OPEN)
elif propObj.exists and (not propObj.isFile):
elif exists and (not isFile):
dlg = wx.DirDialog(parent,
message='Choose directory',
defaultPath=_lastFilePathDir)
......@@ -286,8 +287,8 @@ def _Number(parent, hasProps, propObj, propVal):
"""
value = propVal.get()
minval = propObj.minval
maxval = propObj.maxval
minval = propObj.getConstraint(hasProps, 'minval')
maxval = propObj.getConstraint(hasProps, 'maxval')
makeSlider = (minval is not None) and (maxval is not None)
params = {}
......@@ -308,7 +309,7 @@ def _Number(parent, hasProps, propObj, propVal):
if minval is None: minval = -sys.float_info.max
if maxval is None: maxval = sys.float_info.max
if makeSlider: increment = (maxval-minval)/20.0
if makeSlider: increment = (maxval - minval) / 20.0
else: increment = 0.5
params['inc'] = increment
......@@ -417,7 +418,7 @@ def _makeColourMapComboBox(parent, cmapDict, selected=None):
colours = cmap(np.linspace(0.0, 1.0, width))
# discard alpha values
colours = colours[:,:3]
colours = colours[:, :3]
# repeat each horizontal pixel (height) times
colours = np.tile(colours, (height, 1, 1))
......@@ -436,7 +437,7 @@ def _makeColourMapComboBox(parent, cmapDict, selected=None):
cbox = wx.combo.BitmapComboBox(
parent, style=wx.CB_READONLY | wx.CB_DROPDOWN)
for name,bitmap in zip(cmapNames, bitmaps):
for name, bitmap in zip(cmapNames, bitmaps):
cbox.Append(name, bitmap)
cbox.SetSelection(selected)
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment