BasicProperty (and BasicTypes) for Python

The BasicProperty/BasicTypes system provides a mechanism for intelligent property-based modeling of problem domains in Python 2.2+.  The idea behind property-based domain modeling is to simplify the definition of objects/data structures so that concerns such as initialization, data-type checking and coercion, run-time introspection, and domain specific storage/retrieval operations can be abstracted out from the modeling operations.

BasicProperty builds on the Python 2.2+ descriptor system to provide attribute descriptors with:

As part of the implementation of BasicProperty, the BasicTypes package provides base data types and data type annotations for use with BasicProperty properties.  BasicTypes provides:

As well as a number of utility mechanisms:

BasicProperty was originally created as part of the wxPython Properties Distribution.  It is released under a BSD-style license. BasicProperty can be downloaded from the project page on SourceForge.  The automatically generated reference documentation for basicproperty and basictypes is available online.

Usage Example

To start off, let's create a simple object that model's a task to be accomplished:

from basicproperty import propertied, common

class Todo( propertied.Propertied):
"""Simple example of a todo object"""
name = common.StringProperty(
"name", "Allows you to identify this todo item",
defaultValue="",
setDefaultOnGet = 0,
)
description = common.StringProperty(
"description", """Description of what is to be accomplished""",
defaultValue = "",
friendlyName = "Note",
)
def isOverDue( property, client ):
"""Determine whether client is overDue or not"""
if hasattr( client, 'dueDate') and client.dueDate < now():
return 1
return 0
overDue = common.BooleanProperty(
"overDue", """Whether we are currently overdue""",
setDefaultOnGet=1,
defaultFunction = isOverDue,
)
del isOverDue # just to clean up the namespace
notes = common.StringsProperty(
"notes", """List of note strings for the item""",
# list and dictionary property types default to creating
# new instances and storing them as the property-value
)

if __name__ == "__main__":
t = Todo()
print 'name: %r description: %r overDue: %r'%( t.name, t.description, t.overDue )
t.name = 23
print 'name: %r description: %r overDue: %r'%( t.name, t.description, t.overDue )
for x in range( 10 ):
t.notes.append( 'Note %s'%(x,))
print t.notes

And the results of running it:

name: u'' description: u'' overDue: False
name: u'23' description: u'' overDue: False
[u'Note 0', u'Note 1', u'Note 2', u'Note 3', u'Note 4', u'Note 5',
u'Note 6', u'Note 7', u'Note 8', u'Note 9']

The propertied.Propertied class here simply provides an initialiser that takes named arguments and calls setattr( obj, name, value) for each named argument.  This makes it very convenient to work with propertied classes during development, as the addition of new properties can be done merely by adding a new property descriptor.

The common module collects together properties for the most commonly used data-types, lists, dictionaries, ints, longs, locale-specific strings, unicode strings, floats, and mx.DateTime values.  The properties in the module are themselves propertied.Propertied objects, and are therefore initialised with named arguments, though they have two positional arguments as well; their name, and their docstring.

Default Values

There are two major ways to define default values for properties:

name = common.StringProperty(
"name", "Allows you to identify this todo item",
defaultValue="",
)

Here, we specify a simple default value for use if the name property does not currently have a default. The value given can be any data-type compatible with the property's underlying data-type (in this case, a (Unicode) string), as it will be coerced to the property's data-type before being returned or set as the value for the object.  In this case, the locale-specific string (str) object will be coerced to a Unicode string for the name property.

In the second approach:

def isOverDue( property, client ):
"""Determine whether client is overDue or not"""
if hasattr( client, 'dueDate') and client.dueDate < now():
return 1
return 0
overDue = common.BooleanProperty(
"overDue", """Whether we are currently overdue""",
setDefaultOnGet=1,
defaultFunction = isOverDue,
)
del isOverDue # just to clean up the namespace

we define a default value by creating a default function which takes as its arguments the property being set (a basicproperty.basic.BasicProperty instance) and the client object (in this case, a Todo instance).  The default function can do any arbitrary amount of processing necessary to determine the appropriate value for the property on this client.

Because the defaultFunction may involve an arbitrarily large amount of processing to calculate the value, it's useful to be able to tell the system whether, on calculating a default value, that default value should be set as the current value of the property for the client.  The setDefaultOnGet flag is used to signal this behaviour.

List and Dictionary properties from the basicproperty.common module, by default, have their setDefaultOnGet flag set, and provide a defaultFunction similar to: lambda prop,client: [] and lambda prop,client: {} respectively.  This is because, in normal use, users normally want to be able to say myTodo.notes.append( 'this' ), rather than myTodo.notes = getattr(myTodo,'notes',[]).append( 'this' ), i.e. to treat the default value as a new per-instance list/dictionary.

The example above could have been written more tersely as:

overDue = common.BooleanProperty(
"overDue", """Whether we are currently overdue""",
setDefaultOnGet=1,
defaultFunction = lambda prop,client: hasattr(client,overDue) and client.overDue < now(),
)

for those who prefer lambda functions to named functions.

Under the covers, BasicProperty instances wrap the defaultValue or defaultFunction value in a basicproperty.defaults.Default instance appropriate to the given argument.

Common Property Types

Much of the time the common module's built-in property types will be sufficient for modeling simple domain objects.  This module provides the following property types:

The last three property types will not be available if the mx.DateTime package is not available.

In case you are wondering where all of this interesting functionality comes from (as there is almost no code in the common module itself); BasicProperty objects are designed to delegate most of their operations to their "base type" (stored as property.baseType). 

Each property type in the common module stores a reference to either a class which implements the BasicTypes protocols for coercian and type-checking, or a base-type which has had annotations registered to provide those services.  In the next section we'll discuss how to write your classes to act as baseTypes for BasicProperty objects.

Boundaries/Constraints

You often want to constrain the value of a given property in some way, such as constraining it to a given range of values, or to values which are "not null", or by constraining each element in a sequence value in such a way.  BasicProperty allows you to add such constraints to the property object.  For instance:

from basicproperty import propertied, common
from basictypes import boundary

class TestData( propertied.Propertied ):
str2 = common.StringProperty(
"str2", """Test string property""",
boundaries = (
boundary.Range( minimum= "a", maximum="z" ),
boundary.Length( maximum = 10 ),
)
)

Creates a string/unicode property where the value must be between the characters "a" and "z" (inclusively) in Python's comparison order (i.e. 'A', '' or '\000' would raise errors, but 'a', 'abc' or 'z' would not).  Boundary objects have a fairly simple API.  The must be callable objects with a signature like so:

def boundary( value, property, client ):
"""Check value against some condition, raise errors on failure"""

boundaries should raise (subclasses of) TypeError, ValueError, KeyError or AttributeError if a boundary condition is violated.

The basictypes.boundary module defines a number of useful Boundary types:

In order to allow for checking each element of a sequence, the module also defines:

Boundaries are checked during every __set__ and getDefault operation, after coercian and checking have completed.  As a result, it is best to keep them to a minimum (or code them efficiently) for frequently used classes.

The examples directory has a contrived example for restricting a string to an email-like format with a boundary.




Writing Property Base Types

BasicProperty objects are designed to delegate much of their operation to their "base type".  The idea of a base type is that, for the most part, the most common interest in property modeling is in representing the data types which can be stored in a particular attribute, which factory functions should be used to create new instances for storage in the attribute, and how to coerce a given value to an appropriate value for storage.

To specify the base data type for a new BasicProperty, we specify a baseType argument in the property constructor:

myInteger = basic.BasicProperty(
"myInteger", """Demonstration of specifying a base-type""",
baseType = int,
)

In this particular instance, we specify that the base type for the property is to be the built-in type int.  We could just as easily specify a user-defined class, or even the dotted-name string specifier for a class/type conforming to the baseType protocol.

Base-type Stand-ins (Annotating Types)

Because there is no way to alter the definition of an int or other built-in type, BasicTypes allows for registering another object to serve as the base-type for a property whose base-type is considered unalterable.  In this case, the int type has had the basictypes.basic_types.Int_DT class registered to provide the baseType protocol implementation.

You can register a baseType protocol stand in for a given class object like so:

basictypes.registry.registerDT( classObject, standIn )

Although originally created to support built-in types, this mechanism is not restricted to built-in types.  Any class which you would rather not alter for use with BasicProperty can be annotated in this way.  Keep in mind, however, that every class for use with the stand-in must be registered, subclasses are not annotated when a superclass is annotated.

Writing Base-type Protocol Objects

The protocol used for communicating between a BasicProperty instance and its associated baseType object is fairly minimal.  All aspects of the protocol are optional, with fairly reasonable default operations defined assuming that the base-type is a class defining an object type which is to be stored in the associated property.

The following elements may be defined:

The special case of sequence of type classes should be examined.  BasicTypes provides a meta-class which can be used to construct baseTypes for use as list-of-somethings.  Here's an example of use:

from basictypes import list_types
ConstraintSchemas = list_types.listof(
ConstraintSchema,
name = "ConstraintSchemas",
dataType = "list.ConstraintSchemas",
factories = classmethod( constraint_factories ),
)

The first argument is the base type for individual elements within the list of elements.  Interactions between the list-property and this baseType are approximately equivalent to those for individual BasicProperties, save that they are applied to members of the list, rather than the whole list.

The name becomes the new class's name, with a reasonable default determined if it is missing.  Any named arguments are passed to the class constructor, so in the above example, the new list base-type will have a classmethod named factories.  The dataType may be used in creating the name if not provided.

There's actually some dark magic going on underneath the covers which will attempt to find the module from which list_types.list_of is called in order to set the __module__ attribute of the new class so that the class will work properly with Python's pickle module.  For this to work properly, however, the class needs to be available at the top level of the module in which it is defined.  i.e.  mymodule.ConstraintSchemas needs to point to the newly created class in order for the pickling system to work with it.

Changes

Version 0.6.3a

Version 0.6.2a

Version 0.6.1a

Version 0.6.0a

Version 0.5.12a

Version 0.5.9a

License

BasicProperty and BasicTypes
Copyright (c) 2002-2003, Michael C. Fletcher
All rights reserved.

THIS SOFTWARE IS NOT FAULT TOLERANT AND SHOULD NOT BE USED IN ANY
SITUATION ENDANGERING HUMAN LIFE OR PROPERTY.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:

Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.

Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials
provided with the distribution.

The name of Michael C. Fletcher may not be used to endorse or
promote products derived from this software without specific
prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
COPYRIGHT HOLDERS AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
OF THE POSSIBILITY OF SUCH DAMAGE.