Skip to content

Registry System

📖 This page describes the coola.registry package, which provides thread-safe registry implementations for storing and managing typed mappings.

Prerequisites: You'll need to know a bit of Python. For a refresher, see the Python tutorial.

Overview

The coola.registry package provides two types of registries for managing key-value mappings:

  • Registry: A generic key-value registry for any hashable keys
  • TypeRegistry: A specialized type-based registry with Method Resolution Order (MRO) support

Both registries are thread-safe and support standard dictionary operations through operator overloading.

Registry (Generic Key-Value)

Basic Usage

The Registry class provides a thread-safe container for storing and retrieving values by key:

>>> from coola.registry import Registry
>>> registry = Registry[str, int]()
>>> registry.register("key1", 42)
>>> registry.get("key1")
42

Dictionary-Style Operations

The registry supports standard dictionary operations:

>>> from coola.registry import Registry
>>> registry = Registry[str, int]()
>>> registry["key1"] = 100
>>> "key1" in registry
True
>>> registry["key1"]
100
>>> del registry["key1"]
>>> "key1" in registry
False

Initialization with Data

You can initialize a registry with existing data:

>>> from coola.registry import Registry
>>> registry = Registry[str, int]({"a": 1, "b": 2})
>>> len(registry)
2
>>> registry.get("a")
1

Safe Registration

By default, registering a duplicate key raises an error to prevent accidental overwriting:

>>> from coola.registry import Registry
>>> registry = Registry[str, int]()
>>> registry.register("key1", 42)
>>> registry.register("key1", 100)  # doctest: +SKIP
Traceback (most recent call last):
...
RuntimeError: A value is already registered for 'key1'...

To allow overwriting, use exist_ok=True:

>>> from coola.registry import Registry
>>> registry = Registry[str, int]()
>>> registry.register("key1", 42)
>>> registry.register("key1", 100, exist_ok=True)
>>> registry.get("key1")
100

Bulk Registration

Register multiple key-value pairs at once:

>>> from coola.registry import Registry
>>> registry = Registry[str, int]()
>>> registry.register_many({"key1": 42, "key2": 100, "key3": 7})
>>> len(registry)
3
>>> registry.get("key2")
100

Bulk updates with exist_ok:

>>> from coola.registry import Registry
>>> registry = Registry[str, int]({"key1": 1})
>>> registry.register_many({"key1": 10, "key4": 4}, exist_ok=True)
>>> registry.get("key1")
10
>>> registry.get("key4")
4

Unregistering Keys

Remove a key and get its value:

>>> from coola.registry import Registry
>>> registry = Registry[str, int]({"key1": 42, "key2": 100})
>>> registry.has("key1")
True
>>> value = registry.unregister("key1")
>>> value
42
>>> registry.has("key1")
False

Clearing Registry

Remove all entries from the registry:

>>> from coola.registry import Registry
>>> registry = Registry[str, int]({"key1": 42, "key2": 100})
>>> len(registry)
2
>>> registry.clear()
>>> len(registry)
0

Iterating Over Registry

Access keys, values, and items:

>>> from coola.registry import Registry
>>> registry = Registry[str, int]({"a": 1, "b": 2})
>>> list(registry.keys())
['a', 'b']
>>> list(registry.values())
[1, 2]
>>> list(registry.items())
[('a', 1), ('b', 2)]

Equality Checking

Compare two registries:

>>> from coola.registry import Registry
>>> registry1 = Registry[str, int]({"key1": 42, "key2": 100})
>>> registry2 = Registry[str, int]({"key1": 42, "key2": 100})
>>> registry3 = Registry[str, int]({"key1": 42})
>>> registry1.equal(registry2)
True
>>> registry1.equal(registry3)
False

TypeRegistry (Type-Based)

Basic Usage

The TypeRegistry class provides a thread-safe container for mapping Python types to values:

>>> from coola.registry import TypeRegistry
>>> registry = TypeRegistry[str]()
>>> registry.register(int, "I am an integer")
>>> registry.get(int)
'I am an integer'

Type Resolution with MRO

The key feature of TypeRegistry is its resolve() method, which uses the Method Resolution Order (MRO) to find the most appropriate value for a type:

>>> from coola.registry import TypeRegistry
>>> registry = TypeRegistry[str]()
>>> registry.register(object, "I am an object")
>>> registry.register(int, "I am an integer")
>>> # Direct match
>>> registry.resolve(int)
'I am an integer'
>>> # Falls back to parent type via MRO (bool inherits from int)
>>> registry.resolve(bool)
'I am an integer'
>>> # Falls back to object
>>> registry.resolve(str)
'I am an object'

Dictionary-Style Operations

Like Registry, TypeRegistry supports standard dictionary operations:

>>> from coola.registry import TypeRegistry
>>> registry = TypeRegistry[str]()
>>> registry[str] = "I am a string"
>>> str in registry
True
>>> registry[str]
'I am a string'
>>> del registry[str]

Initialization with Types

Initialize with type mappings:

>>> from coola.registry import TypeRegistry
>>> registry = TypeRegistry[int]({str: 100, float: 200})
>>> len(registry)
2
>>> registry.get(str)
100

Safe Type Registration

Prevent accidental overwriting of type mappings:

>>> from coola.registry import TypeRegistry
>>> registry = TypeRegistry[int]()
>>> registry.register(str, 42)
>>> registry.register(str, 100)  # doctest: +SKIP
Traceback (most recent call last):
...
RuntimeError: A value is already registered for <class 'str'>...

Allow overwriting with exist_ok=True:

>>> from coola.registry import TypeRegistry
>>> registry = TypeRegistry[int]()
>>> registry.register(str, 42)
>>> registry.register(str, 100, exist_ok=True)
>>> registry.get(str)
100

Bulk Type Registration

Register multiple types at once:

>>> from coola.registry import TypeRegistry
>>> registry = TypeRegistry[str]()
>>> registry.register_many({int: "integer", float: "float", str: "string"})
>>> len(registry)
3
>>> registry.get(float)
'float'

Inheritance-Based Lookup

The resolve() method walks the MRO to find the most specific registered type:

>>> from coola.registry import TypeRegistry
>>> class Animal:
...     pass
>>> class Dog(Animal):
...     pass
>>> class Poodle(Dog):
...     pass
>>> registry = TypeRegistry[str]()
>>> registry.register(Animal, "animal")
>>> registry.register(Dog, "dog")
>>> registry.resolve(Dog)
'dog'
>>> registry.resolve(Poodle)  # Resolves to parent Dog
'dog'

Performance Optimization

The TypeRegistry uses an internal cache for type resolution to optimize performance:

>>> from coola.registry import TypeRegistry
>>> registry = TypeRegistry[str]()
>>> registry.register(object, "base")
>>> registry.register(int, "integer")
>>> # First resolve() populates cache
>>> registry.resolve(bool)
'integer'
>>> # Subsequent resolves use cached result
>>> registry.resolve(bool)
'integer'

The cache is automatically cleared when you register or unregister types to ensure consistency.

Thread Safety

Both Registry and TypeRegistry are thread-safe, using reentrant locks (threading.RLock) to protect concurrent access:

>>> from coola.registry import Registry
>>> import threading
>>> registry = Registry[str, int]()
>>> def worker(key, value):
...     registry.register(key, value, exist_ok=True)
>>> threads = [threading.Thread(target=worker, args=(f"key{i}", i)) for i in range(10)]
>>> for t in threads:
...     t.start()
>>> for t in threads:
...     t.join()
>>> len(registry) == 10
True

Common Use Cases

Plugin System

Use a registry to manage plugins:

>>> from coola.registry import Registry
>>> plugin_registry = Registry[str, type]()
>>> class JSONParser:
...     pass
>>> class XMLParser:
...     pass
>>> plugin_registry.register("json", JSONParser)
>>> plugin_registry.register("xml", XMLParser)
>>> parser_class = plugin_registry.get("json")
>>> parser_class.__name__
'JSONParser'

Type Dispatch

Use a type registry for dispatching based on type:

>>> from coola.registry import TypeRegistry
>>> handlers = TypeRegistry[str]()
>>> handlers.register(int, "handle_int")
>>> handlers.register(str, "handle_str")
>>> handlers.register(list, "handle_list")
>>> handlers.resolve(int)
'handle_int'
>>> handlers.resolve(bool)  # bool inherits from int
'handle_int'

Factory Pattern

Use a registry to implement a factory pattern:

>>> from coola.registry import Registry
>>> factory = Registry[str, type]()
>>> class Circle:
...     pass
>>> class Square:
...     pass
>>> factory.register("circle", Circle)
>>> factory.register("square", Square)
>>> shape_class = factory.get("circle")
>>> shape_class.__name__
'Circle'

Configuration Management

Store configuration handlers:

>>> from coola.registry import Registry
>>> config_handlers = Registry[str, callable]()
>>> config_handlers.register("database", lambda: {"host": "localhost"})
>>> config_handlers.register("cache", lambda: {"enabled": True})
>>> db_config = config_handlers.get("database")()
>>> db_config
{'host': 'localhost'}

Design Principles

The coola.registry package design provides:

  1. Thread safety: All operations are protected by locks for concurrent access
  2. Type safety: Generic typing support for type hints and IDE support
  3. Flexibility: Support for both generic key-value and type-based registries
  4. Performance: Caching in TypeRegistry for efficient type resolution
  5. Standard interface: Dictionary-like operations for familiar API

Properties

Property Statement
Thread-safe Reads and writes are protected by a reentrant lock (threading.RLock).
No silent overwrite Registering an existing key raises AlreadyRegisteredError, unless exist_ok=True.
Backward-compatible errors A missing key raises KeyNotRegisteredError, and a missing type raises TypeNotRegisteredError. Both are KeyError.
Most specific type wins TypeRegistry.resolve follows the Method Resolution Order.
Cached resolution TypeRegistry caches the resolution of types for speed.
Generic typing Both registries are generic in their key and value types.

Comparison: Registry vs TypeRegistry

Feature Registry TypeRegistry
Key type Any hashable Python types only
MRO lookup No Yes (via resolve())
Caching No Yes (for type resolution)
Use case Generic key-value storage Type-based dispatch

See Also