Advanced Topics

This section covers more advanced or esoteric uses of ClassRegistry features.

Registering Classes Automatically

Tired of having to add the register decorator to every class that you want to add to a class registry? Surely there’s a better way!

The answer is class_registry.base.AutoRegister()!

Call AutoRegister() and pass in a registry, and it returns a base class. Any non-abstract class that extends from that base class automatically gets added to the registry.

Here’s an example:

from abc import ABC, abstractmethod
from class_registry import ClassRegistry
from class_registry.base import AutoRegister

pokedex = ClassRegistry('element')

# Note ``AutoRegister(pokedex)`` used as a base class here, as well as ``ABC``.
class Pokemon(AutoRegister(pokedex), ABC):
     @abstractmethod
     def get_abilities(self):
         raise NotImplementedError()

# Define some non-abstract subclasses.
class Butterfree(Pokemon):
    element = 'bug'

    def get_abilities(self):
        return ['compound_eyes']

class Spearow(Pokemon):
    element = 'flying'

    def get_abilities(self):
        return ['keen_eye']

# Any non-abstract class that extends ``Pokemon`` will automatically get registered
# in our Pokédex!
assert list(pokedex.keys()) == ['bug', 'flying']
assert list(pokedex.classes()) == [Butterfree, Spearow]

In the above example, note that Butterfree and Spearow were added to pokedex automatically. However, the Pokemon base class was not added, because it is abstract.

Important

Python defines an abstract class as a class with at least one unimplemented abstract method. You can’t just add ABC!

from abc import ABC

# Declare an "abstract" class.
class ElectricPokemon(Pokemon, ABC):
    element = 'electric'

    def get_abilities(self):
        return ['shock']

assert list(pokedex.keys()) == ['bug', 'flying', 'electric']
assert list(pokedex.classes()) == [Butterfree, Spearow, ElectricPokemon]

Note in the above example that ElectricPokemon was added to pokedex, even though it extends abc.ABC.

Because ElectricPokemon doesn’t have any unimplemented abstract methods, Python does not consider it to be abstract.

We can verify this by using inspect.isabstract():

from inspect import isabstract
assert not isabstract(ElectricPokemon)

Note

In previous versions of ClassRegistry, AutoRegister returned a metaclass instead of a base class. The metaclass version of the function still exists at class_registry.auto_register.AutoRegister(), but it is deprecated and will be removed in a future version of ClassRegistry <https://github.com/todofixthis/class-registry/issues/14>.

If your code is still using the old AutoRegister function, you can change it like this:

# Deprecated:
from class_registry.auto_register import AutoRegister

class MyBaseClass(metaclass=AutoRegister(my_registry)):
    ...

# Update to this:
from abc import ABC
from class_registry.base import AutoRegister

class MyBaseClass(AutoRegister(my_registry), ABC):
    ...

Patching

From time to time, you might need to register classes temporarily. For example, you might need to patch a global class registry in a unit test, ensuring that the extra classes are removed when the test finishes.

ClassRegistry provides a RegistryPatcher that you can use for just such a purpose:

from class_registry import ClassRegistry, RegistryKeyError
from class_registry.patcher import RegistryPatcher

pokedex = ClassRegistry('element')

# Create a couple of new classes, but don't register them yet!
class Oddish:
    element = 'grass'

class Meowth:
    element = 'normal'

# As expected, neither of these classes are registered.
try:
    pokedex['grass']
except RegistryKeyError:
    pass

# Use a patcher to temporarily register these classes.
with RegistryPatcher(pokedex, Oddish, Meowth):
    abbot = pokedex['grass']
    assert isinstance(abbot, Oddish)

    costello = pokedex['normal']
    assert isinstance(costello, Meowth)

# Outside the context, the classes are no longer registered!
try:
    pokedex['grass']
except RegistryKeyError:
    pass

If desired, you can also change existing registry keys, or even replace a class that is already registered.

@pokedex.register
class Squirtle:
    element = 'water'

# Get your diving suit Meowth; we're going to Atlantis!
with RegistryPatcher(pokedex, water=Meowth):
    nemo = pokedex['water']
    assert isinstance(nemo, Meowth)

# After the context exits, the previously-registered class is restored.
ponsonby = pokedex['water']
assert isinstance(ponsonby, Squirtle)

Important

Only mutable registries can be patched (any class that extends BaseMutableRegistry).

In particular, this means that EntryPointClassRegistry can not be patched using RegistryPatcher.

Overriding Lookup Keys

In some cases, you may want to customise the way a ClassRegistry looks up which class to use. For example, you may need to change the registry key for a particular class, but you want to maintain backwards-compatibility for existing code that references the old key.

To customise this, create a subclass of ClassRegistry and override its gen_lookup_key method:

import typing
from class_registry import ClassRegistry

class FacadeRegistry(ClassRegistry):
    @staticmethod
    def gen_lookup_key(key: typing.Hashable) -> typing.Hashable:
        """
        In a previous version of the codebase, some pokémon had the 'bird'
        type, but this was later dropped in favour of 'flying'.
        """
        if key == 'bird':
            return 'flying'

        return key

pokedex = FacadeRegistry('element')

@pokedex.register
class MissingNo:
    element = 'flying'

@pokedex.register
class Meowth:
    element = 'normal'

# MissingNo can be accessed by either key.
assert isinstance(pokedex['bird'], MissingNo)
assert isinstance(pokedex['flying'], MissingNo)

# Other pokémon work as you'd expect.
assert isinstance(pokedex['normal'], Meowth)