Passed
Push — master ( 5f2d49...176334 )
by Konstantinos
49s queued 12s
created

so_magic.utils.subclass_registry   A

Complexity

Total Complexity 4

Size/Duplication

Total Lines 98
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 15
dl 0
loc 98
rs 10
c 0
b 0
f 0
wmc 4

3 Methods

Rating   Name   Duplication   Size   Complexity  
A SubclassRegistry.register_as_subclass() 0 21 1
A SubclassRegistry.__new__() 0 4 1
A SubclassRegistry.create() 0 20 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
class SubclassRegistry(type):
6
    """Subclass Registry
7
8
    A (parent) class using this class as metaclass gains the 'subclasses' class attribute as well as the 'create' and
9
    'register_as_subclass' class methods.
10
11
    The 'subclasses' attribute is a python dictionary having string identifiers as keys and subclasses of the (parent)
12
    class as values.
13
14
    The 'register_as_subclass' class method can be used as a decorator to indicate that a (child) class should belong in
15
    the parent's class registry. An input string argument will be used as the unique key to register the subclass.
16
17
    The 'create' class method can be invoked with a (string) key and suitable constructor arguments to later construct
18
    instances of the corresponding child class.
19
20
    Example:
21
22
        >>> from so_magic.utils import SubclassRegistry
23
24
        >>> class ParentClass(metaclass=SubclassRegistry):
25
        ...  pass
26
27
        >>> ParentClass.subclasses
28
        {}
29
30
        >>> @ParentClass.register_as_subclass('child')
31
        ... class ChildClass(ParentClass):
32
        ...  def __init__(self, child_attribute):
33
        ...   self.attr = child_attribute
34
35
        >>> child_instance = ParentClass.create('child', 'attribute-value')
36
        >>> child_instance.attr
37
        'attribute-value'
38
39
        >>> type(child_instance).__name__
40
        'ChildClass'
41
42
        >>> isinstance(child_instance, ChildClass)
43
        True
44
45
        >>> isinstance(child_instance, ParentClass)
46
        True
47
48
        >>> {k: v.__name__ for k, v in ParentClass.subclasses.items()}
49
        {'child': 'ChildClass'}
50
    """
51
    def __new__(mcs, *args, **kwargs):
52
        class_object = super().__new__(mcs, *args, **kwargs)
53
        class_object.subclasses = {}
54
        return class_object
55
56
    def create(cls, subclass_identifier, *args, **kwargs):
57
        """Create an instance of a registered subclass, given its unique identifier and runtime (constructor) arguments.
58
59
        Invokes the identified subclass constructor passing any supplied arguments. The user needs to know the arguments
60
        to supply depending on the resulting constructor signature.
61
62
        Args:
63
            subclass_identifier (str): the unique identifier under which to look for the corresponding subclass
64
65
        Raises:
66
            ValueError: In case the given identifier is unknown to the parent class
67
68
        Returns:
69
            object: the instance of the registered subclass
70
        """
71
        if subclass_identifier not in cls.subclasses:
72
            raise ValueError(f'Bad "{str(cls.__name__)}" subclass request; requested subclass with identifier '
73
                             f'{str(subclass_identifier)}, but known identifiers are '
74
                             f'[{", ".join(str(subclass_id) for subclass_id in cls.subclasses.keys())}]')
75
        return cls.subclasses[subclass_identifier](*args, **kwargs)
76
77
    def register_as_subclass(cls, subclass_identifier):
78
        """Register a class as subclass of the parent class.
79
80
        Adds the subclass' constructor in the registry (dict) under the given (str) identifier. Overrides the registry
81
        in case of "identifier collision". Can be used as a python decorator.
82
83
        Args:
84
            subclass_identifier (str): the user-defined identifier, under which to register the subclass
85
        """
86
        def wrapper(subclass):
87
            """Add the (sub) class provided to the parent class registry.
88
89
            Args:
90
                subclass ([type]): the (sub) class to register
91
92
            Returns:
93
                object: the (sub) class
94
            """
95
            cls.subclasses[subclass_identifier] = subclass
96
            return subclass
97
        return wrapper
98