Writing Registries¶
Overview¶
Registries are utilities for keeping track of objects. They guarantee that each element registered with them is unique and do not share any attributes with any other registered elements. That is, for each attribute the registry defines, no two registered elements will have the same value for that attribute.
Subclassing Registries¶
Registries are intended to be subclassed.
In Djblets 3.1 and higher, subclasses can specify the type of item managed by the registry when subclassing. If not specified, any values are allowed. This is specified by doing:
class MyRegistry(Registry[Item]):
...
Registries have attributes that subclasses should override to customize the behaviour:
lookup_attrs
, which determine which attributes on the elements will be usable as lookup attributes. This should be either atuple
or alist
containing strings of attribute names.For example:
class MyRegistry(Registry[Item]): lookup_attrs = ['id', 'name'] registry = MyRegistry() # Register a new item. registry.register(Item(id=0, name='bar')) # Look up the item by its attributes. assert registry.get('id', 0) is registry.get('name', 'bar') # Unregister the item. registry.unregister_by('id', 0)
errors
, which determines the error interpolation strings for exceptions raised by the registry. This allows registry subclasses to customized and contextualized error messages about the type of item in the registry, instead of referring to “item”s.These messages override the default error messages, which are defined in the
DEFAULT_ERRORS
dictionary.lookup_error_class
, which determines the exception class for item lookup errors (i.e., when an item cannot be found in the registry). This should be a subclass ofItemLookupError
.
Overriding Error Messages¶
The error messages provided by registries are intentionally vague. To give more
specific error messages, the errors
attribute can be
overridden. This attribute provides the error interpolation strings for errors
in the registry.
For example:
from django.utils.translation import gettext as _
from djblets.registries.registry import ALREADY_REGISTERED, Registry
class FooRegistry(Registry[Item]):
errors = {
ALREADY_REGISTERED: _(
'Could not register the foo "%(attr_name)": it is already '
'registered.',
),
}
If a subclass wishes to provide more default errors, the
default_errors
attribute can be overridden for this
purpose.
For example:
from django.utils.translation import gettext as _
from djblets.registries.errors import DEFAULT_ERRORS
HTTP_ERROR = 'http_error'
_DEFAULT_ERRORS = DEFAULT_ERRORS.copy()
_DEFAULT_ERRORS.update({
HTTP_ERROR: _(
'There was an HTTP error: %(error)s.',
),
})
class ApiRegistry(Registry[Item]):
"""A registry that persists itself to an API."""
default_errors = _DEFAULT_ERRORS
api_url = "http://example.com"
def save(self) -> None:
try:
update(api_url, list(self))
except HttpError as e:
raise Exception(self.format_error(HTTP_ERROR,
error=e))
Default Item Registration¶
Registries can provide default items to be registered when they are first accessed. These default items will populate the registry whenever one of the following methods is called:
The registry will not be populated more than once. They are lazily populated and will never be populated until one of the above methods is called.
For example:
from typing import Iterable
from djblets.registries.registry import Registry
class DefaultItemsRegistry(Registry[int]):
"""A registry that provides default items."""
def get_defaults(self) -> Iterable[int]:
return [1, 2, 3]
The get_defaults()
method can either return an iterable
(such as a list
) or yield
its items, as the result will only
ever be consumed once
Example Registries¶
The following examples are practical uses of registries that may be useful beyond the default definition.
Ordered Registries¶
Suppose we wanted to retrieve each item from the registry in the order it was
registered in. We can do that by keeping a list that contains the id()
of each registered item. Then, instead of iterating through the registry in the
default order, we can iterate through in the order the items were registered.
class OrderedRegistry(Registry[Item]):
"""A registry which maintains the order of its items."""
def __init__(self) -> None:
self._key_order = []
self._by_id = {}
super().__init__()
def register(
self,
item: Item,
) -> None:
"""Register an item and keep track of its insertion order."""
super(OrderedRegistry, self).register(item)
self._key_order.append(id(item))
self._by_id[id(item)] = item
def unregister(
self,
item: Item,
) -> None:
"""Unregister an item and remove it from the insertion order."""
super(OrderedRegistry, self).unregister(item)
key = id(item)
del self._by_id[key]
self._key_order.remove(key)
def __iter__(self) -> Iterator[Item]:
"""Yield each registered item in insertion order."""
for key in self._key_order:
yield self._by_id[key]
This behavior is available in the OrderedRegistry
class.
Exception-less Registries¶
Deprecated since version 3.1: While still usable, it’s recommended that callers call
get_or_none()
instead. This works better with type
annotations and helps with consistency and readability.
If get()
raising an exception is not useful and instead you
would prefer a sentinel value (e.g., None
) to be returned instead, the
get()
method could be overridden as in the following
example.
class SafeRegistry(Registry[Item]):
"""A registry that does not throw exceptions on item lookup failure."""
def get(
self,
attr_name: str,
attr_value: object,
) -> Optional[Item]:
"""Return the item if it is registered; otherwise, return None."""
try:
return super(SafeRegistry, self).get(attr_name, attr_value)
except ItemLookupError:
return None
This behavior is also available as a mixin, as
ExceptionFreeGetterMixin
. It can be used
as follows and is equivalent to the above code example.
from djblets.registries.mixins import ExceptionFreeGetterMixin
class SafeRegistry(ExceptionFreeGetterMixin[Item], Registry[Item]):
pass