Skip to main content

A debconf-like (or about:config-like) registry for storing application settings

Project description

Introduction

This package provides debconf-like (or about:config-like) settings registries for Zope applications. A registry, with a dict-like API, is used to get and set values stored in records. Each record contains the actual value, as well as a field that describes the record in more detail. At a minimum, the field contains information about the type of value allowed, as well as a short title describing the record’s purpose.

See the following doctests for more details:

  • registry.txt, which demonstrates how registries and records work

  • events.txt, which shows the events that are fired from the registry

  • field.txt, which describes the behaviour of persistent fields

Changelog

1.0b1 - 2009-08-02

  • Fix a bug in bind() for Choice fields. [optilude]

1.0a2 - 2009-07-12

  • Changed API methods and arguments to mixedCase to be more consistent with the rest of Zope. This is a non-backwards-compatible change. Our profuse apologies, but it’s now or never. :-/

    If you find that you get import errors or unknown keyword arguments in your code, please change names from foo_bar too fooBar, e.g. for_interface() becomes forInterface(). [optilude]

1.0a1 - 2009-04-17

  • Initial release

Using registries

You can create a new registry simply by instantiating the Registry class. The class and its data structures are persistent, so you can store them in the ZODB. You may want to provide the registry object as local utility for easy access as well, though we won’t do that here.

>>> from plone.registry import Registry
>>> registry = Registry()

The registry starts out empty. To access the registry’s records, you can use the records property. This exposes a dict API where keys are strings and values are objects providing IRecords.

>>> len(registry.records)
0

Simple records

Let’s now create a record. A record must have a name. This should be a dotted name, and contain ASCII characters only. By convention, it should be all lowercase and start with the name of the package that defines the record.

It is also possible to create a number of records based on a single schema interface - see below - but for now, we will focus on simple records.

Before we can create the record, we must create the field that describes it. Fields are based on the venerable zope.schema package, but plone.registry only supports certain fields, and disallows use of a few properties even of those. As a rule of thumb, so long as a field stores a Python primitive, it is supported; the same goes for attributes of fields.

Thus:

  • Fields like Object, InterfaceField and so on are not supported

  • A custom constraint method is not supported.

  • The order attribute will always be set to -1

  • For Choice fields, only named vocabularies are supported: you cannot reference a particular source or source binder

  • The key_type and value_type properties of Dict, List, Tuple, Set and Frozenset may only contain persistent fields.

See field.text for more details.

Creating a record

The supported field types are found in the module plone.registry.field. These are named the same as the equivalent field in zope.schema, and have the same constructors. You must use one of these fields when creating records directly.

>>> from plone.registry import field
>>> age_field = field.Int(title=u"Age", min=0, default=18)
>>> from plone.registry import Record
>>> age_record = Record(age_field)

Note that in this case, we did not supply a value. The value will therefore be the field default.

>>> age_record.value
18

We can set a different value, either in the Record constructor or via the value attribute:

>>> age_record.value = 2
>>> age_record.value
2

Note that the value is validated against the field:

>>> age_record.value = -1
Traceback (most recent call last):
...
TooSmall: (-1, 0)
>>> age_record.value
2

We can now add the field to the registry. This is done via the record dictionary.

>>> 'plone.records.tests.age' in registry
False
>>> registry.records['plone.records.tests.age'] = age_record

At this point, the record will gain __name__ and __parent__ attributes.

>>> age_record.__name__
'plone.records.tests.age'
>>> age_record.__parent__ is registry
True

Creating a record with an initial value

We can create records more succinctly by creating the field, setting the value and assigning it to the registry in one go, like this:

>>> registry.records['plone.records.tests.cms'] = \
...     Record(field.TextLine(title=u"CMS of choice"), u"Plone")

The record can now be obtained. Note that it has a nice __repr__ to help debugging.

>>> registry.records['plone.records.tests.cms']
<Record plone.records.tests.cms>

Accessing and manipulating record values

Once a record has been created and added to the registry, you can access its value through dict-like operations on the registry itself.

>>> 'plone.records.tests.cms' in registry
True
>>> registry['plone.records.tests.cms']
u'Plone'
>>> registry['plone.records.tests.cms'] = u"Plone 3.x"

Again, values are validated:

>>> registry['plone.records.tests.cms'] = 'Joomla'
Traceback (most recent call last):
...
WrongType: ('Joomla', <type 'unicode'>)

There is also a get() method:

>>> registry.get('plone.records.tests.cms')
u'Plone 3.x'
>>> registry.get('non-existent-key') is None
True

Deleting records

Records may be deleted from the records property:

>>> del registry.records['plone.records.tests.cms']
>>> 'plone.records.tests.cms' in registry.records
False
>>> 'plone.records.tests.cms' in registry
False

Creating records from interfaces

As an application developer, it is often desirable to define settings as traditional interfaces with zope.schema fields. plone.registry includes support for creating a set of records from a single interface.

To test this, we have created an interface, IMailSettings, with two fields, sender and smtp_host.

>>> from plone.registry.tests import IMailSettings

Note that this contains standard fields.

>>> IMailSettings['sender']
<zope.schema._bootstrapfields.TextLine object at ...>
>>> IMailSettings['smtp_host']
<zope.schema._field.URI object at ...>

We can create records from this interface like this:

>>> registry.registerInterface(IMailSettings)

One record for each field in the interface has now been created. Their names are the full dotted names to those fields:

>>> sender_record = registry.records['plone.registry.tests.IMailSettings.sender']
>>> smtp_host_record = registry.records['plone.registry.tests.IMailSettings.smtp_host']

The fields used in the records will be the equivalent persistent versions of the fields from the original interface.

>>> sender_record.field
<plone.registry.field.TextLine object at ...>
>>> smtp_host_record.field
<plone.registry.field.URI object at ...>

This feat is accomplished internally by adapting the field to the IPersistentField interface. There is a default adapter factory that works for all fields defined in plone.registry.field. You can of course define your own adapter if you have a custom field type, but bear in mind the golden rules of any persistent field:

  • The field must store only primitives or other persistent fields

  • It must not reference a function, class, interface or other method that could break if a package is uninstalled.

If we have a field for which there is no IPersistentField adapter, we will get an error:

>>> from plone.registry.tests import IMailPreferences
>>> IMailPreferences['settings']
<zope.schema._field.Object object at ...>
>>> registry.registerInterface(IMailPreferences)
Traceback (most recent call last):
...
TypeError: There is no persistent field equivalent for the field `settings` of type `Object`.

Whoops! We can, however, tell registerInterface() to ignore one or more fields.

>>> registry.registerInterface(IMailPreferences, omit=('settings',))

Once an interface’s records have been registered, we can get and set their values as normal:

>>> registry['plone.registry.tests.IMailSettings.sender']
u'root@localhost'
>>> registry['plone.registry.tests.IMailSettings.sender'] = u"webmaster@localhost"
>>> registry['plone.registry.tests.IMailSettings.sender']
u'webmaster@localhost'

If we sub-sequently re-register the same interface, the value will be retained if possible:

>>> registry.registerInterface(IMailSettings)
>>> registry['plone.registry.tests.IMailSettings.sender']
u'webmaster@localhost'

However, if the value is no longer valid, we will revert to the default. To test that, let’s sneakily modify the field for a while.

>>> old_field = IMailSettings['sender']
>>> IMailSettings._InterfaceClass__attrs['sender'] = field.Int(title=u"Definitely not a string", default=2)
>>> if hasattr(IMailSettings, '_v_attrs'):
...     del IMailSettings._v_attrs['sender']
>>> registry.registerInterface(IMailSettings)
>>> registry['plone.registry.tests.IMailSettings.sender']
2

But let’s put it back the way it was.

>>> IMailSettings._InterfaceClass__attrs['sender'] = old_field
>>> if hasattr(IMailSettings, '_v_attrs'):
...     del IMailSettings._v_attrs['sender']
>>> registry.registerInterface(IMailSettings)
>>> registry['plone.registry.tests.IMailSettings.sender']
u'root@localhost'

Accessing the original interface

Now that we have these records, we can look up the original interface. This does not break the golden rules: internally, we only store the name of the interface, and resolve it at runtime.

Records that know about interfaces are marked with IInterfaceAwareRecord and have two additional properties: interface and fieldName.

>>> from plone.registry.interfaces import IInterfaceAwareRecord
>>> IInterfaceAwareRecord.providedBy(age_record)
False
>>> IInterfaceAwareRecord.providedBy(sender_record)
True
>>> sender_record.interfaceName
'plone.registry.tests.IMailSettings'
>>> sender_record.interface is IMailSettings
True

Using the records proxy

Once the records for an interface has been created, it is possible to obtain a proxy object that provides the given interface, but reads and writes its values to the registry. This is useful, for example, to create a form using zope.formlib or z3c.form that is configured with widgets based on the interface, or simply as a more convenient API when working with multiple, related settings.

>>> proxy = registry.forInterface(IMailSettings)
>>> proxy
<RecordsProxy for plone.registry.tests.IMailSettings>

The proxy is not a persistent object on its own.

>>> from persistent.interfaces import IPersistent
>>> IPersistent.providedBy(proxy)
False

It does, however, provide the requisite interface.

>>> IMailSettings.providedBy(proxy)
True

You can distinguish between the proxy and a ‘norma’ object by checking for the IRecordsProxy marker interface:

>>> from plone.registry.interfaces import IRecordsProxy
>>> IRecordsProxy.providedBy(proxy)
True

When we set a value, it is stored in the registry:

>>> proxy.smtp_host = 'http://mail.server.com'
>>> registry['plone.registry.tests.IMailSettings.smtp_host']
'http://mail.server.com'
>>> registry['plone.registry.tests.IMailSettings.smtp_host'] = 'smtp://mail.server.com'
>>> proxy.smtp_host
'smtp://mail.server.com'

Values not in the interface will raise an AttributeError:

>>> proxy.age
Traceback (most recent call last):
...
AttributeError: age

Note that by default, the forInterface() method will check that the necessary records have been registered. For example, we cannot use any old interface:

>>> registry.forInterface(IInterfaceAwareRecord)
Traceback (most recent call last):
...
KeyError: 'Interface `plone.registry.interfaces.IInterfaceAwareRecord` defines a field `interface`, for which there is no record.'

By default, we also cannot use an interface for which only some records exist:

>>> registry.forInterface(IMailPreferences)
Traceback (most recent call last):
...
KeyError: 'Interface `plone.registry.tests.IMailPreferences` defines a field `settings`, for which there is no record.'

It is possible to disable this check, however. This will be a bit more efficient:

>>> registry.forInterface(IMailPreferences, check=False)
<RecordsProxy for plone.registry.tests.IMailPreferences>

A better way, however, is to explicitly declare that some fields are omitted:

>>> pref_proxy = registry.forInterface(IMailPreferences, omit=('settings',))

In this case, the omitted fields will default to their ‘missing’ value:

>>> pref_proxy.settings ==  IMailPreferences['settings'].missing_value
True

However, trying to set the value will result in a AttributeError:

>>> pref_proxy.settings = None
Traceback (most recent call last):
...
AttributeError: settings

Registry events

The registry fires certain events. These are:

  • plone.registry.interfaces.IRecordAddedEvent, when a record has been

    added to the registry.

  • plone.registry.interfaces.IRecordRemovedEvent, when a record has been

    removed from the registry.

  • plone.registry.interfaces.IRecordModifiedEvent, when a record’s value is

    modified.

To test these events, we will create, modify and remove a few records:

>>> from plone.registry import Registry, Record, field
>>> registry = Registry()

Adding a new record to the registry should fire IRecordAddedEvents:

>>> registry.records['plone.registry.tests.age'] = \
...     Record(field.Int(title=u"Age", min=0, default=18))
>>> registry.records['plone.registry.tests.cms'] = \
...     Record(field.TextLine(title=u"Preferred CMS"), value=u"Plone")

When creating records from an interface, one event is fired for each field in the interface.

>>> from plone.registry.tests import IMailSettings
>>> registry.registerInterface(IMailSettings)

Deleting a record should fire an IRecordRemovedEvent:

>>> del registry.records['plone.registry.tests.cms']

Changing a record should fire an IRecordModifiedEvent:

>>> registry['plone.registry.tests.age'] = 25
>>> registry.records['plone.registry.tests.age'].value = 24

Let’s take a look at the events that were just fired:

>>> from plone.registry.interfaces import IRecordEvent
>>> from zope.component.eventtesting import getEvents
>>> getEvents(IRecordEvent)
[<RecordAddedEvent for plone.registry.tests.age>,
 <RecordAddedEvent for plone.registry.tests.cms>,
 <RecordAddedEvent for plone.registry.tests.IMailSettings.sender>,
 <RecordAddedEvent for plone.registry.tests.IMailSettings.smtp_host>,
 <RecordRemovedEvent for plone.registry.tests.cms>,
 <RecordModifiedEvent for plone.registry.tests.age>,
 <RecordModifiedEvent for plone.registry.tests.age>]

For the modified events, we can also check the value before and after the change.

>>> from plone.registry.interfaces import IRecordModifiedEvent
>>> [(repr(e), e.oldValue, e.newValue,) for e in getEvents(IRecordModifiedEvent)]
[('<RecordModifiedEvent for plone.registry.tests.age>', 18, 25),
 ('<RecordModifiedEvent for plone.registry.tests.age>', 25, 24)]

IObjectEvent-style redispatchers

There is a special event handler which takes care of re-dispatching registry events based on the schema interface prescribed by the record.

Let’s re-set the event testing framework and register the re-dispatching event subscriber. Normally, this would happen automatically by including this package’s ZCML.

>>> from zope.component.eventtesting import clearEvents
>>> from zope.component import provideHandler
>>> from plone.registry.events import redispatchInterfaceAwareRecordEvents
>>> clearEvents()
>>> provideHandler(redispatchInterfaceAwareRecordEvents)

We’ll then register a schema interface.

>>> from plone.registry.tests import IMailSettings
>>> registry.registerInterface(IMailSettings)

We could now register an event handler to print any record event occurring on an IMailSettings record. More specialised event handlers for e.g. IRecordModifiedEvent or IRecordRemovedEvent are of course also possible. Note that it is not possible to re-dispatch IRecordAddedEvents, so these are never caught.

>>> from zope.component import adapter
>>> @adapter(IMailSettings, IRecordEvent)
... def print_mail_settings_events(proxy, event):
...     print "Got", event, "for", proxy
>>> provideHandler(print_mail_settings_events)

Let’s now modify one of the records for this interface. The event handler should react immediately.

>>> registry['plone.registry.tests.IMailSettings.sender'] = u"Some sender"
Got <RecordModifiedEvent for plone.registry.tests.IMailSettings.sender> for <RecordsProxy for plone.registry.tests.IMailSettings>

Let’s also modify a non-interface-aware record, for comparison’s sake. Here, there is nothing printed.

>>> registry['plone.registry.tests.age'] = 3

We can try a record-removed event as well:

>>> del registry.records['plone.registry.tests.IMailSettings.sender']
Got <RecordRemovedEvent for plone.registry.tests.IMailSettings.sender> for <RecordsProxy for plone.registry.tests.IMailSettings>

The basic events that have been dispatched are:

>>> getEvents(IRecordEvent)
[<RecordAddedEvent for plone.registry.tests.IMailSettings.sender>,
 <RecordAddedEvent for plone.registry.tests.IMailSettings.smtp_host>,
 <RecordModifiedEvent for plone.registry.tests.IMailSettings.sender>,
 <RecordModifiedEvent for plone.registry.tests.age>,
 <RecordRemovedEvent for plone.registry.tests.IMailSettings.sender>]

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

plone.registry-1.0b1.tar.gz (29.4 kB view hashes)

Uploaded Source

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page