#! /usr/bin/env python
######################################################################
# Written by Kevin L. Sitze on 2008-05-03
# This code may be used pursuant to the MIT License.
######################################################################
"""
Property
========
The Property class provides basic functionality that allows class
level control over how a particular attribute is managed. In its
simplest form a Property attribute works exactly like a regular
attribute on an instance while providing documentation details
about the attribute accessible via the declaring class.
This class modifies how properties are created on a class. The Python
documentation contains the following example:
class C(object):
def __init__(self):
self._x = None
def getx(self):
return self._x
def setx(self, value):
self._x = value
def delx(self):
del self._x
x = property(getx, setx, delx, 'the "x" property.')
The equivalent using Property is as follows:
class C(object):
x = Property('x', None)
>>> x = C()
>>> repr(x.x)
'None'
>>> C.x.__doc__
'the "x" property'
Need a read-only property? Here is the Python example:
class Parrot(object):
def __init__(self):
self._voltage = 100000
@property
def voltage(self):
'Get the current voltage.'
return self._voltage
And here is the equivalent:
class Parrot(object):
voltage = Property('voltage', 100000, Property.Mode.READ_ONLY, 'Get the current voltage')
If your class needs to write to a property that is intended to be
public read-only you can use the set_property() function.
"""
__all__ = ( 'Enum', 'Property' )
def Enum(*names):
"""See immutable symbolic enumeration types by Zoran Isailovski
(see http://code.activestate.com/recipes/413486-first-class-enums-in-python/)
- Enums are immutable; attributes cannot be added, deleted or changed.
- Enums are iterable.
- Enum value access is symbolic and qualified, ex. Days.Monday (like in C#).
- Enum values are true constants.
- Enum values are comparable.
- Enum values are invertible (useful for 2-valued enums, like Enum('no', 'yes').
- Enum values are usable as truth values (in a C tradition, but this is debatable).
- Enum values are reasonably introspective (by publishing their enum type and numeric value)
Changed slightly to add '__doc__' tags to the generated
enumeration types. So to the above author's comments we add:
- Enums and Enum values are documented.
- enumeration values are type-checked during comparisons.
"""
assert names, "Empty enums are not supported" # <- Don't like empty enums? Uncomment!
class EnumClass(object):
__slots__ = names
def __contains__(self, v): return v in constants
def __getitem__(self, i): return constants[i]
def __iter__(self): return iter(constants)
def __len__(self): return len(constants)
def __repr__(self): return 'Enum' + str(names)
def __str__(self): return 'enum ' + str(constants)
class EnumValue(object):
__slots__ = ('__value')
def __init__(self, value): self.__value = value
value = property(lambda self: self.__value)
type = property(lambda self: EnumType)
def __hash__(self): return hash(self.__value)
def __cmp__(self, other):
try:
if self.type is other.type:
return cmp(self.__value, other.__value)
else:
raise TypeError, "requires a '%s' object but received a '%s'" % ( self.type.__class__.__name__, other.type.__class__.__name__ )
except AttributeError:
raise TypeError, "requires a '%s' object but received a '%s'" % ( self.type.__class__.__name__, other.__class__.__name__ )
def __invert__(self): return constants[maximum - self.__value]
def __nonzero__(self): return bool(self.__value)
def __repr__(self): return str(names[self.__value])
maximum = len(names) - 1
constants = [None] * len(names)
for i, each in enumerate(names):
val = type(EnumValue)(
'EnumValue', (EnumValue,), { '__doc__': 'Enumeration value "%s"' % each }
)(i)
setattr(EnumClass, each, val)
constants[i] = val
constants = tuple(constants)
EnumType = type(EnumClass)(
'EnumClass', (EnumClass,), { '__doc__': 'Enumeration of %s' % repr(constants) }
)()
return EnumType
class Property(object):
"""Construct a data descriptor suitable for associating
documentation with an attribute value. Attribute values are
instance specific and are stored within the instance dictionary
(so property values go away when the instance is garbage
collected). Properties have a class-wide default value used if
the property has not been specified on an instance.
The class has the ability to indicate the access mode of the
resulting attribute. The possible access modes may be specified
using exactly one of the following enumeration values:
Mode.READ_ONLY
==================
The attribute may only be read. The instance property effectively
becomes a class constant (as the attribute may not be written). A
READ_ONLY attribute must have the default value specified when the
Property is constructed.
Unlike an Enum class, the resulting Property is still accessable
through the declaring class (to provide access to the attribute
documentation). This has the side effect that the constant value
is only accessable through instances of the declaring class.
Mode.READ_WRITE
===================
The READ_WRITE mode is the default mode on Property instances and
is used to provide attributes with all the normal behaviors of
typical class attributes with supporting documentation and
optional default values.
Mode.WRITE_ONCE
===================
The WRITE_ONCE mode builds a data descriptor that allows every
instance of the declaring class to set the resulting attribute one
time. A default value may be specified that will be returned if
the attribute is accessed prior to the write; but the default does
not prevent the one-time write from occuring.
Additionally you may supply a documentation string so your class
properties may expose usage information.
"""
####
# Special value used to mark an undefined default value.
####
__NONE = object()
Mode = Enum('READ_ONLY', 'READ_WRITE', 'WRITE_ONCE')
def __init__(self, name, default = __NONE, mode = Mode.READ_WRITE, doc = None):
"""Construct a new Property data descriptor.
\var{name} the name of the attribute being created.
\var{default} the (optional) default value to use when
retrieving the attribute if it hasn't already been set.
\var{mode} the mode of the constructed Property.
\var{doc} the documentation string to use. This string is
accessed through the declaring class.
"""
self.__name = name
self.__key = '__property__' + name
if mode.__class__ not in (i.__class__ for i in self.Mode):
raise TypeError, "the mode parameter requires a member of the 'Property.Mode' enumeration but received a '%s'" % mode.__class__.__name__
self.__mode = mode
if default is not self.__NONE:
self.__default = default
elif mode is self.Mode.READ_ONLY:
raise ValueError, 'read only attributes require a default value'
if doc is None:
self.__doc__ = 'the "%s" property' % name
else:
self.__doc__ = doc
def __get__(self, obj, objType = None):
"""Get the attribute value.
"""
try: return obj.__dict__[self.__key]
except AttributeError: return self
except KeyError: pass
try: return objType.__dict__[self.__key]
except KeyError: pass
try: return self.__default
except AttributeError:
raise AttributeError, "'%s' object has no attribute '%s'" % ( obj.__class__.__name__, self.__name )
def __set__(self, obj, value):
"""Set the attribute value.
"""
if self.__mode is self.Mode.READ_ONLY:
raise AttributeError, "can't set attribute \"%s\"" % self.__name
elif self.__mode is self.Mode.WRITE_ONCE:
if self.__key in obj.__dict__:
raise AttributeError, "can't set attribute \"%s\"" % self.__name
obj.__dict__[self.__key] = value
def __delete__(self, obj):
"""Delete the attribute value.
"""
if self.__mode is not self.Mode.READ_WRITE:
raise AttributeError, "can't delete attribute \"%s\"" % self.__name
del(obj.__dict__[self.__key])
def set_property(obj, name, value):
"""Set or reset the property 'name' to 'value' on 'obj'.
This function may be used to modify the value of a WRITE_ONCE or
READ_ONLY property. Therefore use of this function should be
limited to the implementation class.
"""
obj.__dict__['__property__' + name] = value
if __name__ == '__main__':
from types import FloatType, ComplexType
def assertEquals( exp, got, msg = None ):
"""assertEquals( exp, got[, message] )
Two objects test as "equal" if:
* they are the same object as tested by the 'is' operator.
* either object is a float or complex number and the absolute
value of the difference between the two is less than 1e-8.
* applying the equals operator ('==') returns True.
"""
if exp is got:
r = True
elif ( type( exp ) in ( FloatType, ComplexType ) or
type( got ) in ( FloatType, ComplexType ) ):
r = abs( exp - got ) < 1e-8
else:
r = ( exp == got )
if not r:
print >>sys.stderr, "Error: expected <%s> but got <%s>%s" % ( repr( exp ), repr( got ), colon( msg ) )
traceback.print_stack()
def assertException( exceptionType, f, msg = None ):
"""Assert that an exception of type \var{exceptionType}
is thrown when the function \var{f} is evaluated.
"""
try:
f()
except exceptionType:
assert True
else:
print >>sys.stderr, "Error: expected <%s> to be thrown by function%s" % ( exceptionType.__name__, colon( msg ) )
traceback.print_stack()
def assertNone( x, msg = None ):
assertSame( None, x, msg )
def assertSame( exp, got, msg = None ):
if got is not exp:
print >>sys.stderr, "Error: expected <%s> to be the same object as <%s>%s" % ( repr( exp ), repr( got ), colon( msg ) )
traceback.print_stack()
def assertTrue( b, msg = None ):
if not b:
print >>sys.stderr, "Error: expected value to be True%s" % colon( msg )
traceback.print_stack()
####
# Test Property
####
class Test( object ):
ro_value = Property( 'ro_value', 'test', mode = Property.Mode.READ_ONLY )
assertException( ValueError, lambda: Property( 'ro_undef', mode = Property.Mode.READ_ONLY ),
'read-only attributes should require default' )
rw_undef = Property( 'rw_undef' )
rw_default = Property( 'rw_default', None )
rw_doc = Property( 'rw_default', doc = 'alternate documentation' )
assertException( TypeError, lambda: Property( 'bad_mode', mode = None ),
'bad Property mode should raise an exception' )
wo_undef = Property( 'wo_undef', mode = Property.Mode.WRITE_ONCE )
wo_default = Property( 'wo_default', 'test', mode = Property.Mode.WRITE_ONCE )
a = Test()
b = Test()
####
# Mode.READ_ONLY
assertEquals( 'test', a.ro_value )
assertEquals( 'test', b.ro_value )
assertException( AttributeError, lambda: setattr( a, 'ro_value', 5 ), 'unexpected write to a read-only attribute' )
# assertException( AttributeError, lambda: del( b.ro_value ), 'unexpected del() on a read-only attribute' )
set_property( a, 'ro_value', 'tset' )
assertEquals( 'tset', a.ro_value )
assertEquals( 'test', b.ro_value )
####
# Mode.READ_WRITE
assertException( AttributeError, lambda: getattr( a, 'rw_undef' ), 'unexpected read of an undefined attribute' )
assertNone( a.rw_default )
a.rw_undef = 5
assertEquals( 5, a.rw_undef )
assertTrue( '__property__rw_undef' in a.__dict__ )
assertEquals( 5, a.__dict__['__property__rw_undef'] )
assertEquals( 'the "rw_undef" property', Test.rw_undef.__doc__ )
assertSame( int, type( a.rw_undef ) )
assertSame( Property, type( Test.rw_undef ) )
assertEquals( 'alternate documentation', Test.rw_doc.__doc__ )
####
# Mode.READ_WRITE: changes to 'a' should not affect 'b'
assertException( AttributeError, lambda: getattr( b, 'rw_undef' ), 'invalid state change via a different instance' )
assertNone( b.rw_default )
####
# Mode.WRITE_ONCE
assertException( AttributeError, lambda: getattr( a, 'wo_undef' ), 'unexpected read of an undefined attribute' )
assertException( AttributeError, lambda: delattr( a, 'wo_undef' ), 'unexpected del() on a write-once attribute' )
a.wo_undef = 'write_once'
assertEquals( 'write_once', a.wo_undef )
assertException( AttributeError, lambda: setattr( a, 'wo_undef', 'write_twice' ), 'unexpected secondary write on a write-once attribute' )
assertEquals( 'write_once', a.wo_undef )
assertException( AttributeError, lambda: delattr( a, 'wo_value' ), 'unexpected del() on a write-once attribute' )
assertEquals( 'test', a.wo_default )
a.wo_default = 'write_once'
assertEquals( 'write_once', a.wo_default )
assertEquals( 'test', b.wo_default )