diff --git a/tkprop/properties.py b/tkprop/properties.py index 91222a962342e879637c0baa42e0dc4eed595d3b..38919232d4ae481d6c891302eef3a8e65d0206aa 100644 --- a/tkprop/properties.py +++ b/tkprop/properties.py @@ -3,8 +3,8 @@ # properties.py - Tkinter control variables encapsulated inside Python # descriptors. # -# This module should not typically be imported directly - import the -# tkprops package. Property type definitions are in properties_types.py. +# This module should not be imported directly - import the tkprops +# package instead. Property type definitions are in properties_types.py. # # Usage: # @@ -88,7 +88,7 @@ # allowing custom validation rules to be enforced on such control # variables. # -# Class structure is organised as follows: +# The runtime structure of Tk properties is organised as follows: # # A HasProperties (sub)class contains a collection of PropertyBase # instances. When an instance of the HasProperties class is created, one @@ -121,7 +121,7 @@ # If one is interested in changes to a single TkVarProxy object # (e.g. one element of a List property), then a listener may be # registered directly with the TkVarProxy object. This listener will -# only be notified of changes to that TkVarProxy object. +# then only be notified of changes to that TkVarProxy object. # # author: Paul McCarthy <pauldmccarthy@gmail.com> # @@ -170,9 +170,9 @@ class TkVarProxy(object): def addListener(self, name, callback): """ - Adds a listener for this variable. When the variable value, the - listener callback function is called. The callback function - must accept these arguments: + Adds a listener for this variable. When the variable value + changes, the listener callback function is called. The + callback function must accept these arguments: value - The new property value valid - Whether the new value is valid or invalid @@ -229,10 +229,12 @@ class TkVarProxy(object): try: newValue = self.tkVar.get() # and if that fails, we manually look up the value - # via the current tk context. + # via the current tk context, thus avoiding the + # failing type cast. Ugly. except: newValue = self.tkVar._tk.globalgetvar(self.name) - # if the new value is valid, save it + # if the new value is valid, save it as the last + # known good value try: self.tkProp.validate(self.owner, newValue) self.lastValue = newValue @@ -253,8 +255,9 @@ class TkVarProxy(object): log.debug('Notifying listener on {}: {}'.format(self.name, name)) try: func(newValue, valid, self.owner, self.tkProp, self.name) - except: - log.debug('') + except Exception as e: + log.debug('Listener on {} ({}) raised exception: {}'.format( + self.name, name, e)) def revert(self): @@ -274,10 +277,28 @@ class TkVarProxy(object): class PropertyBase(object): """ - The base class for properties. Subclasses should override the - validate method to implement any required validation rules and, in - special cases, may override __get__, __set__, and _makeTkVar, with - care. + The base class for properties. Subclasses should: + + - Ensure that PropertyBase.__init__ is called. + + - Override the validate method to implement any built in + validation rules, ensuring that the PropertyBase.validate + method is called. + + - Override __get__ and __set__ for any required implicit + casting/data transformation rules (see + properties_types.String for an example). + + - Override _makeTkVar if creation of the TkVarProxy needs + to be controlled (see properties_types.Choice for an + example). + + - Override getTkVar for properties which consist of + more than one TkVarProxy object + (see properties_types.List for an example). + + - Override whatever you want for advanced usage (see + properties_types.List for an example). """ def __init__(self, tkVarType, default, validateFunc=None): @@ -394,7 +415,7 @@ class PropertyBase(object): """ 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 + contained in the TkVarProxy variable which is attached to the instance. """ @@ -412,8 +433,7 @@ class PropertyBase(object): the given value. """ - instval = instance.__dict__.get(self.label, None) - if instval is None: instval = self._makeTkVar(instance) + instval = getattr(self, instance) instval.tkVar.set(value) diff --git a/tkprop/properties_types.py b/tkprop/properties_types.py index 0ab785e557637d52d516f140ca553df5510e95cb..db16519e924531ed0e552c4572ac72e3b9b41d9e 100644 --- a/tkprop/properties_types.py +++ b/tkprop/properties_types.py @@ -1,21 +1,18 @@ #!/usr/bin/env python # # properties_types.py - Definitions for different property types - see -# properties.py. +# properties.py for more information. # # Author: Paul McCarthy <pauldmccarthy@gmail.com> # -import os +import os import os.path as op - import logging as log from collections import OrderedDict -import Tkinter as tk - - +import Tkinter as tk import tkprop.properties as props @@ -33,7 +30,7 @@ class Boolean(props.PropertyBase): class Number(props.PropertyBase): """ Base class for the Int and Double classes. Don't - subclass this, subclass one of Int or Double. + use/subclass this, use/subclass one of Int or Double. """ def __init__(self, tkvartype, minval=None, maxval=None, **kwargs): @@ -255,9 +252,11 @@ class Choice(String): the real control variable is set to the corresponding choice. Similarly, we add a trace to the original control variable, so that when its choice value is modified, the label variable value is - changed. Even though this circular event callback situation looks - incredibly dangerous, Tkinter (in python 2.7.6) seems to be smart - enough to inhibit an infinitely recursive event explosion. + changed. + + Even though this circular event callback situation looks incredibly + dangerous, Tkinter (in python 2.7.6) seems to be smart enough to + inhibit an infinitely recursive event explosion. The label control variable is accessible via the Choice.getLabelVar() method. @@ -319,21 +318,22 @@ class FilePath(String): self.label, value)) -class _ListWrapper(object): +class ListWrapper(object): """ Used by the List property type, defined below. An object which - acts like a list, but for which items are embedded in a - TkVarProxy object, minimum/maximum list length may be enforced, - and value/type constraints enforced on the values added to it. + acts like a list, but for which items are embedded in TkVarProxy + objects, minimum/maximum list length may be enforced, and + value/type constraints enforced on the values added to it. - Only basic list operations are supported. All list modifications - occur in the append and pop methods. + Only basic list operations are supported. List modifications + occur exclusively in the append, pop, __setitem__ and + __delitem__ methods. A TkVarProxy object is created for each item that is added to the list. When a list value is changed, instead of a new variable being created, the value of the existing variable is changed. References to the list of TkVarProxy objects may - be accessed via the _tkVars attribute of the _ListWrapper + be accessed via the _tkVars attribute of the ListWrapper object. """ @@ -347,14 +347,21 @@ class _ListWrapper(object): """ Parameters: - owner: The HasProperties object, of which the List object - which is managing this _ListWrapper object, is a + which is managing this ListWrapper object, is a property. + - listProp: The List property object which is managing this - _ListWrapper object. + ListWrapper object. Whenever the list, or a value + within the list is modified, listProp._valueChanged + is called. + - values: list of initial values. + - listType: A PropertyBase instance, specifying the type of data allowed in this list. + - minlen: minimum list length + - maxlen: maximum list length """ @@ -365,13 +372,16 @@ class _ListWrapper(object): self._minlen = minlen self._maxlen = maxlen - # This is the list that the _ListWrapper wraps. + # This is the list that the ListWrapper wraps. # It contains TkVarProxy objects. self._tkVars = [] + # Set the list to the given initial values if values is not None: self.extend(values) + # Or create a list of length at least + # minlen, containing default values elif self._minlen is not None: for i in range(self._minlen): @@ -411,7 +421,8 @@ class _ListWrapper(object): def _notify(self): """ - If a listener was passed to the constructor, it is called. + Called on list modifications. Notifies the List property via + its _varChanged method. """ self._listProp._varChanged( @@ -507,10 +518,8 @@ class _ListWrapper(object): def extend(self, iterable): """ Appends all items in the given iterable to the end of the - list. A ValueError is raised if any item does not meet the - list type/value constraints. An IndexError is raised if - an insertion would causes the list to grow beyond its - maximum length. + list. An IndexError is raised if an insertion would causes + the list to grow beyond its maximum length. """ toAdd = list(iterable) @@ -531,6 +540,7 @@ class _ListWrapper(object): tkVar = self._tkVars.pop() val = tkVar.tkVar.get() + self._notify() return val @@ -538,14 +548,12 @@ class _ListWrapper(object): def __setitem__(self, key, values): """ Sets the value(s) of the list at the specified index/slice. - A ValueError is raised if any of the values do not meet the - list type/value constraints. """ if isinstance(key, slice): if (key.step is not None) and (key.step > 1): raise ValueError( - '_ListWrapper does not support extended slices') + 'ListWrapper does not support extended slices') indices = range(*key.indices(len(self))) elif isinstance(key, int): @@ -569,10 +577,12 @@ class _ListWrapper(object): # removing items elif newLen < oldLen: self._checkMinlen(-lenDiff) + # Replace values of existing items if newLen == oldLen: for i,v in zip(indices, values): self._tkVars[i].tkVar.set(v) - + + # Replace old TkVarProxy objects with new ones. else: values = [self._makeTkVar(v) for v in values] if len(values) == 1: values = values[0] @@ -600,10 +610,10 @@ class List(props.PropertyBase): """ A property which represents a list of items, of another property type. List functionality is not complete - see the documentation for the - _ListWrapper class, defined above. + ListWrapper class, defined above. This class is a bit different from the other PropertyBase classes, in - that the validation logic is built into the _ListWrapper class, rather + that the validation logic is built into the ListWrapper class, rather than this class. """ @@ -634,7 +644,7 @@ class List(props.PropertyBase): def getTkVar(self, instance, index=None): """ - Return a list of TkVarProxy objects or, if index is specifried, + Return a list of TkVarProxy objects or, if index is specified, the TkVarProxy object at the specified index. """ if index is None: return instance.__dict__[self.label]._tkVars @@ -643,14 +653,15 @@ class List(props.PropertyBase): def _makeTkVar(self, instance): """ - _ListWrapper instead of TkVarProxy. + Creates a ListWrapper object, and attaches it to the + given instance. """ - instval = _ListWrapper(instance, - self, - values=self.default, - listType=self.listType, - minlen=self.minlen, - maxlen=self.maxlen) + instval = ListWrapper(instance, + self, + values=self.default, + listType=self.listType, + minlen=self.minlen, + maxlen=self.maxlen) instance.__dict__[self.label] = instval return instval @@ -658,7 +669,8 @@ class List(props.PropertyBase): def __get__(self, instance, owner): """ - Return _ListWrapper instead of value. + If instance is None, returns this List object. Otherwise returns + the ListWrapper instance attached to the given instance. """ if instance is None: @@ -672,10 +684,10 @@ class List(props.PropertyBase): def __set__(self, instance, value): """ - Replace contents of list. + Replaces the contents of the ListWrapper object attached + to the given instance. """ - instval = getattr(instance, self.label) - + instval = getattr(instance, self.label) instval[:] = value