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.
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.
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.
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.
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:
Range( minimum = NULL, maximum = NULL )
-- constrain value so that minimum <= value <= maximum
,
note that None is a perfectly valid minimum or maximum, only the
special object NULL (the default) eliminates the given constraint.Length( minimum = NULL, maximum = NULL )
-- as with Range, save that it is the length of the value (i.e. len(value)) which is constrained.NotNull( )
-- constrain value so that not valueType( typeDescriptor )
-- constrain the value to
objects of a given type, can be specified as a dotted string name to
allow for late binding (note, normally you would use baseType for this
kind of thing, Type is largely a holdover from an earlier codebase).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.
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.
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.
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:
def coerce(cls, value)
-- if provided, coerce the
given value to an instance of the base-type. After coercion, the
value must pass the check( value ) test. Failed coercion should
raise TypeError or ValueError when an invalid value is
encountered. It is often useful to do cls.check( value ) before
any involved processing to catch the common case where the value is
already acceptable. coerce is only called by BasicProperty if the
value is not already an instance of the baseType, but the coerce method
should be robust enough to handle the case where the value is of the
baseType.def check( cls, value )
-- if provided, check that
the given value is of the correct type and conforms to all type-based
restrictions (such as being properly formatted). If not provided,
BasicProperty uses isinstance( value, baseType) to do the test.factories
(or def factories( cls )
) --
if provided, should be a sequence of callable objects which construct
new instances suitable for storage in the property, or a callable
object returning such a sequence. The only use of this feature in
BasicProperty/BasicTypes is to provide a method getFactories() on
properties which returns either this value, or a sequence with just the
baseType itself, or [] if there is no baseType. This is used by
the wxPython Properties Distribution to provide GUI menus for editing
modeled objects.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.
Version 0.6.3a
Version 0.6.2a
Version 0.6.1a
Version 0.6.0a
Version 0.5.12a
Version 0.5.9a
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.