EnhAtts – Enhanced Attributes

This project implements properties on steroids called “fields” for Python 2 and 3 (tested with CPython 2.7 and 3.3 as well as PyPy 2). It is based on the “Felder” (german for fields) concept of the doctoral thesis of Patrick Lay “Entwurf eines Objektmodells für semistrukturierte Daten im Kontext von XML Content Management Systemen” (Rheinische Friedrich-Wilhelms Universität Bonn, 2006) and is developed as part of the diploma thesis of Michael Pohl “Architektur und Implementierung des Objektmodells für ein Web Application Framework” (Rheinische Friedrich-Wilhelms Universität Bonn, 2013-2014).

What is a Field?

A field is an attribute on a new-style class called field container. A field has a name and a field class, which implements the access to the attribute and is instantiated once per field container instance with the field’s name. On field containers the attribute FIELDS is defined allowing access to the fields mapping – an ordered mapping which allows access to the fields by their name.

The class Field defines the field protocol and serves as a base class. DataField extends this class and adds basic capabilities for storing data in the field container as an attribute with the name _ appended by the field’s name. ValueField in contrast stores the value in its own instance.

In addition to the usual setting, deleting and getting on container instances, the field protocol also defines a default value, which is returned on getting on a class, and preparation, which is called before setting to aquire the value to set and which may raise an exception if the value is invalid.

Getting an item from the fields mapping on a field container instance returns the field instance. Setting and deleting items in this mapping is the same as the according access on the attribute. The fields mapping on a container class allows only getting, which returns the field instance. Iterating over the mapping is done in the order of the fields, the other mapping methods keys(), values() and meth:items also use this order.

Setting the FIELDS attribute on a field container instance with a dictionary allows to set multiple fields at once with the values from the dictionary. First all given values are prepared; if at least one preparation raises an exception, FieldPreparationErrors is raised containing the exceptions thrown by the preparations. If no preparation fails, all fields are set to their values.

Defining a Field

A field is defined on a field container class by using the result of calling field() as a class decorator. When the decorator is applied, it creates the FIELDS class attribute containing a descriptor for the fields mapping, if this does not yet exist. The decorator also registers the field on the mapping by its name and creates another class attribute with the name being the field’s name and containing a descriptor for access to the field. The order of the fields in the mapping is the order in which the field decorators appear in the source code (i.e. in the reverse order of definition).

If the supplied field class is not a type object, the argument is used as a string and the field class to use is determined from the module containing the implementing class. This way a class extending a field container will use another field class than the container, if there is an attribute on the containing module with that name.

field() allows to specify attributes for the field class, in this case tinkerpy.anonymous_class() is used to create an anonymous class based on the given field class. If no field class is specified, DataField is used as the base class.

To omit creating a string for the field name, you can also retrieve an attribute from field(), which is a function creating a field with the name of the attribute becoming the field’s name.

Examples

Field Definition

First we define some fields as an anonymous classes and a lazy looked up class based on DataField:

>>> class Data(DataField):
...     description='This is data.'
...
...     def show(self):
...         return str(self.get())
...
>>> @field.number(
...     prepare=lambda self, value, field_values: int(value),
...     DEFAULT=None)
... @field('data', 'Data')
... class Test(object):
...     pass

To retrieve the default values, get the attribute values on the container class:

>>> Test.number is None
True
>>> Test.data
Traceback (most recent call last):
AttributeError: type object 'Data' has no attribute 'DEFAULT'

To access the field classes, use the fields mapping:

>>> print(Test.FIELDS['data'].description)
This is data.

Field Container Instances

On a container instance you can set and get the field values:

>>> test = Test()
>>> test.number
Traceback (most recent call last):
AttributeError: 'Test' object has no attribute '_FIELD_number'
>>> test.number = '1'
>>> test.data = None
>>> test.number == 1 and test.data is None
True

If preparation fails, the value is not set:

>>> test.number = 'a'
Traceback (most recent call last):
ValueError: invalid literal for int() with base 10: 'a'
>>> test.number == 1
True

You can also delete field values:

>>> del test.number
>>> del test.FIELDS['data']
>>> test.number
Traceback (most recent call last):
AttributeError: 'Test' object has no attribute '_FIELD_number'
>>> test.data
Traceback (most recent call last):
AttributeError: 'Test' object has no attribute '_FIELD_data'

Setting DeleteField as the value of a field also deletes the value:

>>> test.number = 1
>>> test.data = 'data'
>>> test.number = DeleteField
>>> test.FIELDS['data'] = DeleteField
>>> test.number
Traceback (most recent call last):
AttributeError: 'Test' object has no attribute '_FIELD_number'
>>> test.data
Traceback (most recent call last):
AttributeError: 'Test' object has no attribute '_FIELD_data'

Existence of a field is different from existence of the field value:

>>> hasattr(test, 'number')
False
>>> 'number' in test.FIELDS
True
>>> test.FIELDS['number']
<UNSET enhatts.<anonymous:enhatts.DataField> object>

The field instances are available on the field container instance’s field mapping:

>>> test.data = 1
>>> test.FIELDS['data'].show()
'1'

Setting Multiple Fields

By assigning a mapping to the container instance’s fields mapping, you can set multiple fields at once if no preparation fails:

>>> test.FIELDS = dict(number='2', data=3)
>>> test.number == 2 and test.data == 3
True
>>> try:
...     test.FIELDS = dict(number='a', data=4)
... except FieldPreparationErrors as e:
...     for field_name, error in e.items():
...         print('{}: {}'.format(field_name, error))
number: invalid literal for int() with base 10: 'a'
>>> test.number == 2 and test.data == 3
True

Using DeleteField field values can also be deleted while setting multiple fields:

>>> test.FIELDS = dict(number=DeleteField, data=0)
>>> not hasattr(test, 'number') and test.data == 0
True

Field Container Callbacks

A field container may define callable attributes (e.g. methods), which are called while changing fields. FIELDS_before_prepare() is called before the fields are prepared with the mapping of field values to set. FIELDS_before_modifications() is called just before the fields are set with a mutable mapping being a view on the field values, which keeps track of the changes to apply. After the fields have been set FIELDS_after_modifications() is called with an immutable mapping being a view on the field values.

.FIELDS_before_prepare(field_values)

Called before preparing the field values.

Parameters:field_values – The mutable mapping from field name to field value containing an entry for each field to set. Field values being DeleteField denote the field to be deleted.
.FIELDS_before_modifications(fields_proxy)

Called before modifying the fields.

Parameters:fields_proxy – A mutable mapping from field name to field value for all fields of the container, but with values being as they will be after applying the modifications. Changes (setting or deleting items) are not applied to the underlying fields mapping, but are executed when the modifications are applied. The attributes changed and deleted contain iterators over the names of changed or deleted fields.
.FIELDS_after_modifications(fields_proxy)

Called after setting the fields with the prepared values.

Parameters:fields_proxy – An immutable mapping from field name to field value for all fields of the container. The attributes changed and deleted contain iterators over the names of changed or deleted fields.

Here’s an example field container which prints out information and sets the field revision on changes:

>>> @field('revision')
... class CallbackTest(Test):
...     def __init__(self, **fields):
...         self.FIELDS = fields
...
...     def FIELDS_before_prepare(self, field_values):
...         print('Before preparation of:')
...         for name in sorted(field_values.keys()):
...             print('  {} = {}'.format(name, repr(field_values[name])))
...
...     def FIELDS_before_modifications(self, fields_proxy):
...         print('Changes:')
...         for name in fields_proxy.changed:
...             print('  {} = {}'.format(name, repr(fields_proxy[name])))
...         print('To delete: {}'.format(', '.join(fields_proxy.deleted)))
...         try:
...             revision = self.revision + 1
...         except AttributeError:
...             revision = 0
...         fields_proxy['revision'] = revision
...
...     def FIELDS_after_modifications(self, fields_proxy):
...         print('Revision: {}'.format(self.revision))
>>> callback_test = CallbackTest(number='1', data=None)
Before preparation of:
  data = None
  number = '1'
Changes:
  number = 1
  data = None
To delete: 
Revision: 0
>>> callback_test.FIELDS = dict(number=DeleteField, data='data')
Before preparation of:
  data = 'data'
  number = <enhatts.DeleteField>
Changes:
  data = 'data'
To delete: number
Revision: 1

The callbacks are also executed if only a single field is modified:

>>> try:
...     callback_test.number = None
... except TypeError as e:
...     print('ERROR: Value cannot be converted by int().')
Before preparation of:
  number = None
ERROR: Value cannot be converted by int().
>>> callback_test.number = '2'
Before preparation of:
  number = '2'
Changes:
  number = 2
To delete: 
Revision: 2
>>> del callback_test.number
Before preparation of:
  number = <enhatts.DeleteField>
Changes:
To delete: number
Revision: 3

Inheritance

The fields on classes extending field containers are appended to the existing fields. Fields can also be redefined, which doesn’t change the position:

>>> class Data(Data):
...     DEFAULT = False
...
>>> @field('attributes', ValueField, 'data')
... @field('number', DEFAULT=True)
... class Extending(Test):
...     pass
...
>>> len(Extending.FIELDS)
3
>>> for name, field_obj in Extending.FIELDS.items():
...     print('{}: {}'.format(name, field_obj))
...
number: <class 'enhatts.<anonymous:enhatts.DataField>'>
attributes: <class 'enhatts.ValueField'>
data: <class 'enhatts.Data'>
>>> print(repr(Extending.FIELDS))
FIELDS on <class 'enhatts.Extending'>: {number: <class 'enhatts.<anonymous:enhatts.DataField>'>, attributes: <class 'enhatts.ValueField'>, data: <class 'enhatts.Data'>}
>>> Extending.data is False
True
>>> Extending.number is True
True
>>> extending = Extending()
>>> extending.FIELDS = {'attributes': 2, 'data': 3}
>>> print(repr(extending.FIELDS)) 
FIELDS on <enhatts.Extending object at 0x...>: {number: <UNSET enhatts.<anonymous:enhatts.DataField> object>, attributes: <enhatts.ValueField object: 2>, data: <enhatts.Data object: 3>}

Multiple inheritance works the same. We define a diamond inheritance:

>>> @field('a')
... @field('b')
... class A(object):
...     pass
...
>>> @field('a')
... @field('c')
... class B(A): pass
...
>>> @field('d')
... @field('b')
... class C(A): pass
...
>>> @field('e')
... class D(B, C): pass

This leads to the following field orders:

>>> list(A.FIELDS.keys())
['a', 'b']
>>> list(B.FIELDS.keys())
['a', 'b', 'c']
>>> list(C.FIELDS.keys())
['a', 'b', 'd']
>>> list(D.FIELDS.keys())
['a', 'b', 'c', 'd', 'e']

API

enhatts.field(name, _field_class=DataField, _before=None, **attributes)

Creates a class decorator, which does the following on the class it is applied to:

  1. If it does not yet exist, it creates the attribute FIELDS on the class containing a descriptor for access to the fields mapping.
  1. It registers a field with name name on the fields mapping. If attributes are given an tinkerpy.anonymous_class() based on _field_class is used as the field class, otherwise _field_class is used as the field class.

If _field_class is not a class object (i.e. not of type type), it is interpreted as a string. This triggers lazy field class lookup, meaning the class to use is taken from the module the field container class is defined in.

Parameters:
  • name (str) – The name of the field.
  • _field_class – The class to use as a field class or as the base of an anonmous field class. If this is not a new-style class, it is used as a string value, this triggers lazy field class lookup.
  • _before (str) – The field the newly defined field should be inserted before. If this is None, the field will be inserted as the first.
  • attributes – If values are given, an tinkerpy.anonymous_class() is created with these attributes and _field_class as the base and used as the field class.
Returns:

A class decorator which creates a field on the class it is applied to.

You can also retrieve attributes from this object (except those from object) which returns functions calling field() with name being the retrieved attribute name.

.name(_field_class=DataField, _before=None, **attributes)

Calls field() with the function name as the first argument name and the function arguments as the appropriate arguments to field().

class enhatts.Field(container, name)

This class defines the field protocol.

Parameters:
  • container – The field container, this becomes the container value.
  • name – The name of the field, it becomes the name value.

A field class is instantiated once for each field container instance with the field’s name and the container as the argument. On access to the field the methods defined here are called.

Getting

The return value of calling default() is returned on getting a field’s value on a field container class.

On reading a field’s value on a field container instance, the result of calling get() is returned.

Setting

Before setting a field’s value, prepare() is called. If this does not raise an exception, set() is called to write the field’s value.

Deleting

delete() is called on deletion of a field’s value.

The byte an Unicode string values returned by instances of this class return the respective string values of the field value.

Comparisons compare field values. If there is no field value set, all comparisons except != return False. If the compared value also has a get() method, the return value is used for comparison, otherwise the compared value itself.

container

The field container instance.

name

The name of the field. The field is accessible through an attribute of this name and under this name in the FIELDS mapping on a field container class and instance.

classmethod default(container_cls, name)

The default value of the field. This implementation returns the value of the attribute DEFAULT on cls and thus will raise an AttributeError if this does not exist.

Parameters:
  • container_cls – The field container class.
  • name – The field name.
Raises AttributeError:
 

if there is no attribute DEFAULT on cls.

Returns:

the value of the attribute DEFAULT on cls.

prepare(value, field_values)

Prepares the value to set on the field container instance and should raise an exception, if value is not valid.

Parameters:
  • value – The value to prepare.
  • field_values – A read-only proxy mapping to the field values, returning the current field values shadowed by all yet prepared field values. The attributes changed and deleted contain iterators over the names of changed or deleted fields.
Raises Exception:
 

if value is not valid.

Returns:

The prepared value, this implementation returns value unchanged.

set(value)

Should set the field’s value to value on the field container instance or throw an exception, if the field should not be writeable.

This implmentation raises an AttributeError and does nothing else.

Parameters:value – The value to write.
Raises AttributeError:
 in this implementation.
get()

Should return the field’s value on the field container instance or throw an exception, if the field should not be readable.

This implmentation raises an AttributeError and does nothing else.

Raises AttributeError:
 in this implementation.
Returns:should return the field’s value.
delete()

Should delete the field’s value on the field container instance or throw an exception, if the field should not be deleteable.

This implmentation raises an AttributeError and does nothing else.

Raises AttributeError:
 in this implementation.
class enhatts.DataField(container, name)

A readable, writeable and deleteable Field implementation, using an attribute with name _FIELD_ appended by the field’s name on the field container instance to store the field’s value.

set(value)

Sets the field’s value to value on the field container instance.

Parameters:value – The value to write.
get()

Returns the field’s value on the field container instance.

Raises AttributeError:
 if the field’s value is not set.
Returns:the field’s value.
delete()

Deletes the field’s value on the field container instance.

Raises AttributeError:
 if the field’s value is not set.
class enhatts.ValueField(container, name)

A readable, writable and deletable Field implementation, which stores its data in an instance attribute accessible through the property value.

value

The value stored in the field instance.

set(value)

Sets the field’s value to value.

Parameters:value – The value to write.
get()

Returns the field’s value.

Raises AttributeError:
 if the field’s value is not set.
Returns:the field’s value.
delete()

Deletes the field’s value.

Raises AttributeError:
 if the field’s value is not set.
class enhatts.FieldPreparationErrors(exceptions)

This exception is thrown if preparation of at least one field fails, when setting multiple field at once by assigning a mapping to the FIELDS attribute on a field container instance.

This exception is a mapping from field names to the appropriate exception objects thrown by the Field.prepare() calls.

enhatts.DeleteField

A static value indicating to delete a field value when setting a single or multiple fields.