Skip to content
Snippets Groups Projects
Commit 6d759619 authored by Paul McCarthy's avatar Paul McCarthy :mountain_bicyclist:
Browse files

Changed the way that validation works. PropertyBase objects now have a...

Changed the way that validation works. PropertyBase objects now have a validate() method which, when called, will raise a ValueError for invalid values.  The tkprop module now defines custom Tkinter.*Var classes which, when their set method is called, will call the validate() method  of their containing PropertyBase object. Validation is set up in the tkpropwidget module, and works nicely for all property types (so far).
parent 196e3c40
No related branches found
No related tags found
No related merge requests found
......@@ -88,23 +88,21 @@ class BetFrame(tk.Frame):
def __init__(self, parent, betopts):
tk.Frame.__init__(self, parent)
self.pack(fill=tk.X, expand=1)
self.pack(fill=tk.BOTH, expand=1)
self.betopts = betopts
self.notebook = ttk.Notebook(self)
self.notebook.pack(fill=tk.X, expand=1)
self.notebook.pack(fill=tk.BOTH, expand=1)
self.stdOptFrame = tk.Frame(self.notebook)
self.advOptFrame = tk.Frame(self.notebook)
self.stdOptFrame.pack(fill=tk.X, expand=1)
self.advOptFrame.pack(fill=tk.X, expand=1)
self.stdOptFrame.pack(fill=tk.BOTH, expand=1)
self.advOptFrame.pack(fill=tk.BOTH, expand=1)
self.stdOptFrame.columnconfigure(0, weight=1)
self.stdOptFrame.columnconfigure(1, weight=1)
self.advOptFrame.columnconfigure(0, weight=1)
self.advOptFrame.columnconfigure(1, weight=1)
self.advOptFrame.columnconfigure(1, weight=1)
self.notebook.add(self.stdOptFrame, text='BET options')
self.notebook.add(self.advOptFrame, text='Advanced options')
......@@ -121,6 +119,8 @@ class BetFrame(tk.Frame):
label .grid(row=idx, column=0, sticky=tk.N+tk.E+tk.S+tk.W)
widget.grid(row=idx, column=1, sticky=tk.N+tk.E+tk.S+tk.W)
self.stdOptFrame.rowconfigure(idx, weight=1)
setattr(self, '{}Widget'.format(option), widget)
setattr(self, '{}Label' .format(option), label)
......@@ -141,7 +141,9 @@ class BetFrame(tk.Frame):
widget.grid(row=idx, column=1, sticky=tk.N+tk.E+tk.S+tk.W)
setattr(self, '{}Widget'.format(option), widget)
setattr(self, '{}Label' .format(option), label)
setattr(self, '{}Label' .format(option), label)
self.advOptFrame.rowconfigure(idx, weight=1)
self.buttonFrame = tk.Frame(self)
......@@ -154,7 +156,7 @@ class BetFrame(tk.Frame):
self.runButton .pack(fill=tk.X, expand=1, side=tk.LEFT)
self.quitButton .pack(fill=tk.X, expand=1, side=tk.RIGHT)
self.buttonFrame.pack(fill=tk.X, expand=1)
self.buttonFrame.pack(fill=tk.X)
if __name__ == '__main__':
......@@ -165,6 +167,10 @@ if __name__ == '__main__':
print('Before')
print(betopts)
# stupid hack for testing under OS X - forces the TK
# window to be displayed above all other windows
os.system('''/usr/bin/osascript -e 'tell app "Finder" to set frontmost of process "Python" to true' ''')
app.mainloop()
print('After')
......
......@@ -30,7 +30,39 @@
# # access the underlying Tkinter object:
# myPropObj.myProperty_tkVar
#
# # >>> <Tkinter.BooleanVar instance at 0x1047ef518>
# # >>> <_tkprops.tkprop._BooleanVar instance at 0x1047ef518>
#
#
# Lots of the code in this class is probably very confusing. First
# of all, you will need to understand python descriptors.
# Descriptors are a way of adding properties to python objects,
# and allowing them to be accessed as if they were just simple
# attributes of the object, but controlling the way that the
# attributes are accessed and assigned.
#
# The following link provides a good overview, and contains the
# ideas which form the basis for the implementation in this module:
#
# - http://nbviewer.ipython.org/urls/gist.github.com/\
# ChrisBeaumont/5758381/raw/descriptor_writeup.ipynb
#
# And if you've got 30 minutes, this video gives a very good
# introduction to descriptors:
#
# - http://pyvideo.org/video/1760/encapsulation-with-descriptors
#
# Once you know how Python descriptors work, you then need to know
# how Tk control variables work. These are simple objects which
# may be passed to a Tkinter widget object when it is created. When
# a user modifies the widget value, the Tk control variable is
# modified. Conversely, if the value of a Tk control variable object
# is modified, any widgets which are bound to the variable are
# updated to reflect the new value.
#
# This module, and the associated tkpropwidget module, uses magic to
# encapsulate Tkinter control variables within python descriptors,
# thus allowing custom validation rules to be enforced on such
# control variables.
#
# author: Paul McCarthy <pauldmccarthy@gmail.com>
#
......@@ -39,24 +71,102 @@ import os.path as op
import Tkinter as tk
# The classes below are used in place of the Tkinter.*Var classes.
# They are identical to the Tkinter versions, with the following
# exceptions:
#
# 1. A reference to a PropertyBase object is saved when the
# Var object is created.
#
# 2. When the set() method is called on the Var object, the
# PropertyBase.validate method is called, to test that
# the new value is valid. If not, a ValueError is raised.
class _StringVar(tk.StringVar):
def __init__(self, tkProp, **kwargs):
self.tkProp = tkProp
tk.StringVar.__init__(self, **kwargs)
def set(self, value):
self.tkProp.validate(value)
tk.StringVar.set(self, value)
class _DoubleVar(tk.DoubleVar):
def __init__(self, tkProp, **kwargs):
self.tkProp = tkProp
tk.DoubleVar.__init__(self, **kwargs)
def set(self, value):
self.tkProp.validate(value)
tk.DoubleVar.set(self, value)
class _IntVar(tk.IntVar):
def __init__(self, tkProp, **kwargs):
self.tkProp = tkProp
tk.IntVar.__init__(self, **kwargs)
def set(self, value):
self.tkProp.validate(value)
tk.IntVar.set(self, value)
class _BooleanVar(tk.BooleanVar):
def __init__(self, tkProp, **kwargs):
self.tkProp = tkProp
tk.BooleanVar.__init__(self, **kwargs)
def get(self):
# For some reason, tk.BooleanVar.get() returns an int,
# 0 or 1, so here we're casting it to a python bool
return bool(tk.BooleanVar.get(self))
def set(self, value):
self.tkProp.validate(value)
tk.BooleanVar.set(self, value)
class PropertyBase(object):
"""
The base class for objects which represent a property. Provides default
getter/setter methods.
The base class for descriptor objects. Provides default getter/setter
methods. Subclasses should override the validate method to implement
any required validation rules.
"""
def __init__(self, tkvartype, default):
"""
The tkvartype parameter should be one of the Tkinter.*Var
class replacements, defined above (e.g. _BooleanVar, _IntVar,
etc). For every object which has this PropertyBase object
as a property, an instance of the tkvartype is created and
attached to the instance.
"""
self.label = None
self._tkvartype = tkvartype
self.default = default
def _make_instval(self, instance):
"""
Creates a Tkinter control variable of the appropriate
type, and attaches it to the given instance.
"""
instval = self._tkvartype(value=self.default, name=self.label)
instval = self._tkvartype(self, value=self.default, name=self.label)
instance.__dict__[self.label] = instval
return instval
def validate(self, value):
"""
Called when an attempt is made to set the property value.
If the given value is invalid, subclass implementations
shouldd raise an Error. Otherwise, they should not return
any value. The default implementation does nothing.
"""
pass
def __get__(self, instance, owner):
"""
If called on the HasProperties class, and not on an instance,
returns this PropertyBase object. Otherwise, returns the value
contained in the Tk control variable which is attached to the
instance.
"""
if instance is None:
return self
......@@ -66,6 +176,12 @@ class PropertyBase(object):
return instval.get()
def __set__(self, instance, value):
"""
Attempts to set the Tk variable, attached to the given instance,
to the given value. The set() method of the tk variable will
call the validate() method of this PropertyBase object, which
will raise an Error if the value is not valid.
"""
instval = instance.__dict__.get(self.label, None)
if instval is None: instval = self._make_instval(instance)
......@@ -95,7 +211,7 @@ class HasProperties(object):
def __new__(cls, *args, **kwargs):
"""
Here, we add some extra fields to a newly created HssProperties
Here, we add some extra fields to a newly created HasProperties
instance. These fields provided direct access to the Tkinter.*Var
objects, and the tkprop objects. This overcomes the need for the
slightly ugly default methods of access, i.e.:
......@@ -140,19 +256,7 @@ class Boolean(PropertyBase):
"""
def __init__(self, default=False):
super(Boolean, self).__init__(tk.BooleanVar, default)
def __get__(self, instance, owner):
result = super(Boolean, self).__get__(instance, owner)
# tk.BooleanVar.get() returns an int, 0 or 1, so
# here we're casting it to a python bool, unless
# this was a class level attribute access.
if instance is None: return result
else: return bool(result)
def __set__(self, instance, value):
super(Boolean, self).__set__(instance, bool(value))
super(Boolean, self).__init__(_BooleanVar, default)
class Number(PropertyBase):
......@@ -168,7 +272,7 @@ class Number(PropertyBase):
super(Number, self).__init__(tkvartype, default)
def __set__(self, instance, value):
def validate(self, value):
if self.minval is not None and value < self.minval:
raise ValueError('{} must be at least {}'.format(
......@@ -178,8 +282,6 @@ class Number(PropertyBase):
raise ValueError('{} must be at most {}'.format(
self.label, self.maxval))
super(Number, self).__set__(instance, value)
class Int(Number):
"""
......@@ -193,12 +295,12 @@ class Int(Number):
- minval
- maxval
"""
super(Int, self).__init__(tk.IntVar, **kwargs)
super(Int, self).__init__(_IntVar, **kwargs)
def __set__(self, instance, value):
def validate(self, value):
value = int(value)
super(Int, self).__set__(instance, value)
Number.validate(self, value)
class Double(Number):
"""
......@@ -212,11 +314,11 @@ class Double(Number):
- minval
- maxval
"""
super(Double, self).__init__(tk.DoubleVar, **kwargs)
super(Double, self).__init__(_DoubleVar, **kwargs)
def __set__(self, instance, value):
def validate(self, value):
value = float(value)
super(Double, self).__set__(instance, value)
Number.validate(self, value)
class String(PropertyBase):
......@@ -237,9 +339,10 @@ class String(PropertyBase):
self.minlen = minlen
self.maxlen = maxlen
self.filterFunc = filterFunc
super(String, self).__init__(tk.StringVar, default)
super(String, self).__init__(_StringVar, default)
def __set__(self, instance, value):
def validate(self, value):
value = str(value)
if self.minlen is not None and len(value) < self.minlen:
......@@ -254,10 +357,8 @@ class String(PropertyBase):
raise ValueError('Invalid value for {}: {}'.format(
self.label, value))
super(String, self).__set__(instance, value)
class Choice(String): # why only string?
class Choice(String):
"""
A property which may only be set to one of a set of predefined values.
"""
......@@ -277,34 +378,36 @@ class Choice(String): # why only string?
super(Choice, self).__init__(default=default)
def __set__(self, instance, value):
def validate(self, value):
value = str(value)
if value not in self.choices:
raise ValueError('Invalid choice for {}: {}'.format(
self.label, value))
super(Choice, self).__set__(instance, value)
class FilePath(String):
"""
A property which represents a file path.
A property which represents a file or directory path.
"""
def __init__(self, default=None, exists=False):
def __init__(self, exists=False, isFile=True):
self.exists = exists
super(FilePath, self).__init__(default=default)
self.isFile = isFile
super(FilePath, self).__init__()
def __set__(self, instance, value):
value = str(value)
def validate(self, value):
if self.exists and (not op.exists(value)):
raise ValueError('{} must be a file that exists ({})'.format(
self.label, value))
if value is None: value = ''
if (value != '') and self.exists:
super(FilePath, self).__set__(instance, value)
if self.isFile and (not op.isfile(value)):
raise ValueError('{} must be a file ({})'.format(
self.label, value))
if (not self.isFile) and (not op.isdir(value)):
raise ValueError('{} must be a directory ({})'.format(
self.label, value))
......@@ -7,39 +7,130 @@
import sys
import os
import os.path as op
import tkprops.tkprop as tkp
import Tkinter as tk
import ttk
import Tkinter as tk
import tkFileDialog as tkfile
import ttk
def _setupValidation(widget, propObj, tkProp):
"""
Configures input validation for the given widget, which is assumed
to be managing the given tkProp (tkprop.PropertyBase) object, which
is further assumed to be a property of the given propObj
(tkprop.HasProperties) object. When a new value is entered into
the widget, as soon as the widget loses focus, the new value is
passed to the validate() method of the property object. If the
validate method raises an error, the property (and widget) value is
reverted to its previous value (which was previously stored when
the widget gained focus).
"""
# Container used for storing the previous value of the property,
# and sharing this value amongst the inner functions below.
oldValue = [0]
def _validate(oldValue, newValue):
# When the widget receives focus, save the current
# property value, so we can roll back to it if
# necessary
def _focused(event):
oldValue[0] = getattr(propObj, tkProp.label)
# When the widget loses focus, pass the entered
# value to the property validate() method.
def _validate(newValue):
valid = False
try:
setattr(propObj, tkProp.label, newValue)
valid = True
tkProp.validate(newValue)
return True
except ValueError as e:
setattr(propObj, tkProp.label, tkProp.default)
return False
widget.after_idle(lambda: widget.config(validate='focusout'))
widget.icursor(tk.END)
return valid
# If the new value is invalid, revert
# the property to its former value
def _invalid():
setattr(propObj, tkProp.label, oldValue[0])
vcmd = (widget.register(_validate), '%s', '%P')
# The tk validation type is reset on some (not all)
# widgets, if the invalidcommand (this function)
# modifies the tk control variable. So here we
# just re-initialise validation on the widget.
widget.after_idle(lambda: widget.config(validate='focusout'))
widget.config(validate='focusout', validatecommand=vcmd)
# Set up all of the validation and event callbacks
vcmd = (widget.register(_validate), '%P')
invcmd = (widget.register(_invalid),)
widget.bind('<FocusIn>', _focused)
widget.config(
validate='focusout',
validatecommand=vcmd,
invalidcommand=invcmd)
# This variable is used to retain the most recently
# visited directory in file dialogs. New file dialogs
# are initialised to display this directory.
_lastFilePathDir = None
def _FilePath(parent, propObj, tkProp, tkVar):
return ttk.Entry(parent, textvariable=tkVar)
"""
Creates and returns a tk Frame containing an Entry and a
Button. The button, when clicked, opens a file dialog
allowing the user to choose a file/directory to open, or
a location to save (this depends upon how the tkprop
[tkprop.FilePath] object was configured).
"""
global _lastFilePathDir
if _lastFilePathDir is None:
_lastFilePathDir = os.getcwd()
frame = tk.Frame(parent)
textbox = ttk.Entry(frame, textvariable=tkVar)
_setupValidation(textbox, propObj, tkProp)
def chooseFile():
global _lastFilePathDir
if tkProp.exists:
if tkProp.isFile:
path = tkfile.askopenfilename(
initialdir=_lastFilePathDir,
title='Open file')
else:
path = tkfile.askdirectory(
initialdir=_lastFilePathDir,
title='Open directory')
else:
path = tkfile.asksaveasfilename(
initialdir=_lastFilePathDir,
title='Save file')
if path is not None:
_lastFilePathDir = op.dirname(path)
setattr(propObj, tkProp.label, path)
button = ttk.Button(frame, text='Choose', command=chooseFile)
textbox.pack(fill=tk.BOTH, side=tk.LEFT, expand=True)
button .pack(fill=tk.Y, side=tk.RIGHT)
return frame
def _Choice(parent, propObj, tkProp, tkVar):
"""
Creates and returns a ttk Combobox allowing the
user to set the given tkProp (tkprop.Choice) object.
"""
choices = tkProp.choices
widget = ttk.Combobox(parent, textvariable=tkVar, values=choices)
......@@ -48,6 +139,11 @@ def _Choice(parent, propObj, tkProp, tkVar):
def _String(parent, propObj, tkProp, tkVar):
"""
Creates and returns a ttk Entry object, allowing
the user to set the given tkPRop (tkprop.String)
object.
"""
widget = ttk.Entry(parent, textvariable=tkVar)
_setupValidation(widget, propObj, tkProp)
......@@ -56,6 +152,11 @@ def _String(parent, propObj, tkProp, tkVar):
def _Number(parent, propObj, tkProp, tkVar):
"""
Creates and returns a tk widget, either a ttk.Scale,
or a tk.Spinbox, allowing the user to set the given
tkProp object (either a tkprop.Int or tkprop.Double).
"""
value = tkVar.get()
minval = tkProp.minval
......@@ -63,11 +164,16 @@ def _Number(parent, propObj, tkProp, tkVar):
makeScale = True
# if both minval and maxval have been set, we
# can use a Scale. Otherwise we use a spinbox.
if any((minval is None, maxval is None)):
makeScale = False
if makeScale:
# Embed the Scale inside a Frame, along
# with labels which display the minimum,
# maximum, and current value.
scaleFrame = ttk.Frame(parent)
scaleFrame.columnconfigure(0, weight=1)
scaleFrame.columnconfigure(1, weight=1)
......@@ -77,22 +183,30 @@ def _Number(parent, propObj, tkProp, tkVar):
from_=minval, to=maxval,
variable=tkVar)
minLabel = ttk.Label(scaleFrame, text='{}'.format(minval), anchor=tk.W)
curLabel = ttk.Label(scaleFrame, text='{}'.format(value), anchor=tk.CENTER)
maxLabel = ttk.Label(scaleFrame, text='{}'.format(maxval), anchor=tk.E)
minLabel = ttk.Label(scaleFrame, text='{}'.format(minval),
anchor=tk.W)
curLabel = ttk.Label(scaleFrame, text='{}'.format(value),
anchor=tk.CENTER)
maxLabel = ttk.Label(scaleFrame, text='{}'.format(maxval),
anchor=tk.E)
widget .grid(row=0, column=0, sticky=tk.N+tk.S+tk.E+tk.W, columnspan=3)
widget .grid(row=0, column=0, sticky=tk.N+tk.S+tk.E+tk.W,
columnspan=3)
minLabel.grid(row=1, column=0, sticky=tk.W)
curLabel.grid(row=1, column=1)
maxLabel.grid(row=1, column=2, sticky=tk.E)
# update the current value Label when
# the underlying variable changes
def updateLabel(*args):
curLabel.config(text='{:0.6}'.format(tkVar.get()))
tkVar.trace("w", updateLabel)
widget = scaleFrame
# The minval and maxval attributes have not both
# been set, so we create a Spinbox instead of a Scale.
else:
# Tkinter spinboxes don't behave well if you
......@@ -123,6 +237,10 @@ def _Int(parent, propObj, tkProp, tkVar):
def _Boolean(parent, propObj, tkProp, tkVar):
"""
Creates and returns a ttk Checkbutton, allowing the
user to set the given tkProp (tkprop.Boolean) object.
"""
value = bool(tkVar.get())
return ttk.Checkbutton(parent, variable=tkVar)
......@@ -130,9 +248,10 @@ def _Boolean(parent, propObj, tkProp, tkVar):
def makeWidget(parent, propObj, propName):
"""
Given a tkprop.HasProperties object, the name of a property, and a Tkinter
parent object, creates and returns a Tkinter widget, or a frame containing
widgets, which may be used to edit the property.
Given propObj (a tkprop.HasProperties object), propName (the name of a
property of propObj), and parent (a Tkinter object), creates and returns a
Tkinter widget, or a frame containing widgets, which may be used to edit
the property.
"""
tkProp = getattr(propObj, '{}_tkProp'.format(propName), None)
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment