Welcome, guest | Sign In | My Account | Store | Cart

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.

Python, 361 lines
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
#! /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 )

I've been using this class for years in my own Python code, time to share with the community.

Benefits to using this class:

  • acts like how Python properties using internal variables on 'self' act.
  • provides quick and easy setup
  • usage documents exactly what the programmer intended.
  • class-wide default values are allowed.
  • custom documentation just like 'property()' allows.
  • creates a reasonable documentation string for you if you don't specify one.
  • you also (like all my other recipes) get unit tests.

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.