SubclassRegistry.__init__()   A
last analyzed

Complexity

Conditions 1

Size

Total Lines 3
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 3
dl 0
loc 3
rs 10
c 0
b 0
f 0
cc 1
nop 2
1
"""Exposes the SubclassRegistry that allows to define a single registration point of one or more subclasses of a
2
(common parent) class.
3
"""
4
5
from typing import Dict, Generic, TypeVar
6
7
T = TypeVar('T')
8
9
10
class SubclassRegistry(type, Generic[T]):
11
    """Subclass Registry
12
13
    A (parent) class using this class as metaclass gains the 'subclasses' class attribute as well as the 'create' and
14
    'register_as_subclass' class methods.
15
16
    The 'subclasses' attribute is a python dictionary having string identifiers as keys and subclasses of the (parent)
17
    class as values.
18
19
    The 'register_as_subclass' class method can be used as a decorator to indicate that a (child) class should belong in
20
    the parent's class registry. An input string argument will be used as the unique key to register the subclass.
21
22
    The 'create' class method can be invoked with a (string) key and suitable constructor arguments to later construct
23
    instances of the corresponding child class.
24
25
    Example:
26
27
        >>> from software_patterns import SubclassRegistry
28
29
        >>> class ClassRegistry(metaclass=SubclassRegistry):
30
        ...  pass
31
32
        >>> ClassRegistry.subclasses
33
        {}
34
35
        >>> @ClassRegistry.register_as_subclass('child')
36
        ... class ChildClass:
37
        ...  def __init__(self, child_attribute):
38
        ...   self.attr = child_attribute
39
40
        >>> child_instance = ClassRegistry.create('child', 'attribute-value')
41
        >>> child_instance.attr
42
        'attribute-value'
43
44
        >>> type(child_instance).__name__
45
        'ChildClass'
46
47
        >>> isinstance(child_instance, ChildClass)
48
        True
49
50
        >>> {k: v.__name__ for k, v in ClassRegistry.subclasses.items()}
51
        {'child': 'ChildClass'}
52
    """
53
54
    subclasses: Dict[str, type]
55
56
    def __init__(cls, *args):
57
        super().__init__(*args)
58
        cls.subclasses = {}
59
60
    def create(cls, subclass_identifier, *args, **kwargs) -> T:
61
        """Create an instance of a registered subclass, given its unique identifier and runtime (constructor) arguments.
62
63
        Invokes the identified subclass constructor passing any supplied arguments. The user needs to know the arguments
64
        to supply depending on the resulting constructor signature.
65
66
        Args:
67
            subclass_identifier (str): the unique identifier under which to look for the corresponding subclass
68
69
        Raises:
70
            UnknownClassError: In case the given identifier is unknown to the parent class
71
            InstantiationError: In case the runtime args and kwargs do not match the constructor signature
72
73
        Returns:
74
            object: the instance of the registered subclass
75
        """
76
        if subclass_identifier not in cls.subclasses:
77
            raise UnknownClassError(
78
                f'Bad "{str(cls.__name__)}" subclass request; requested subclass with identifier '
79
                f'{str(subclass_identifier)}, but known identifiers are '
80
                f'[{", ".join(str(subclass_id) for subclass_id in cls.subclasses.keys())}]'
81
            )
82
        try:
83
            return cls.subclasses[subclass_identifier](*args, **kwargs)
84
        except Exception as any_error:
85
            raise InstantiationError(
86
                'Error during instance object construction.'
87
                f' Failed to create instance of class {cls.subclasses[subclass_identifier]}'
88
                f' using args [{", ".join((str(_) for _ in args))}]'
89
                f' and kwargs [{", ".join(f"{k}={v}" for k, v in kwargs.items())}]'
90
            ) from any_error
91
92
    def register_as_subclass(cls, subclass_identifier):
93
        """Register a class as subclass of the parent class.
94
95
        Adds the subclass' constructor in the registry (dict) under the given (str) identifier. Overrides the registry
96
        in case of "identifier collision". Can be used as a python decorator.
97
98
        Args:
99
            subclass_identifier (str): the user-defined identifier, under which to register the subclass
100
        """
101
102
        def wrapper(subclass):
103
            """Add the (sub) class provided to the parent class registry.
104
105
            Args:
106
                subclass ([type]): the (sub) class to register
107
108
            Returns:
109
                object: the (sub) class
110
            """
111
            cls.subclasses[subclass_identifier] = subclass
112
            return subclass
113
114
        return wrapper
115
116
117
class InstantiationError(Exception):
118
    pass
119
120
121
class UnknownClassError(Exception):
122
    pass
123