Passed
Push — master ( cc7a4b...4d42d8 )
by Konstantinos
43s queued 14s
created

PhiFunctionRegistrator.get_name()   B

Complexity

Conditions 6

Size

Total Lines 21
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 11
nop 1
dl 0
loc 21
rs 8.6666
c 0
b 0
f 0
1
"""This module is responsible to provide a formal way of registering phi functions
2
at runtime. See the 'PhiFunctionRegistrator' class and its 'register' decorator method
3
"""
4
import logging
5
import inspect
6
from typing import Callable
7
from so_magic.utils import Singleton, ObjectRegistry, Subject
8
9
logger = logging.getLogger(__name__)
10
11
12
class PhiFunctionRegistry(Singleton, ObjectRegistry):
13
    """A Singleton dict-like object registry for phi functions.
14
15
    Use this class to create a singleton object (instance of this class) that
16
    acts as a storage for phi function objects.
17
    """
18
    def __new__(cls, *args, **kwargs):
19
        """Create a new (singleton) instance and initialize an empty registry.
20
21
        Returns:
22
            PhiFunctionRegistry: the reference to the singleton object (instance)
23
        """
24
        phi_function_registry = Singleton.__new__(cls, *args, **kwargs)
25
        phi_function_registry = ObjectRegistry(getattr(phi_function_registry, 'objects', {}))
26
        return phi_function_registry
27
28
    @staticmethod
29
    def get_instance():
30
        """Get the singleton object (instance).
31
32
        Returns:
33
            PhiFunctionRegistry: the reference to the singleton object (instance)
34
        """
35
        return PhiFunctionRegistry()
36
37
38
phi_registry = PhiFunctionRegistry()
39
40
41
class PhiFunctionMetaclass(type):
42
    """Class type with a single broadcasting (notifies listeners) facility.
43
44
    Classes using this class as metaclass, obtain a single broadcasting facility
45
    as a class attribute. The class attribute is called 'subject' can be referenced
46
    as any class attribute.
47
48
    Example:
49
        class MyExampleClass(metaclass=PhiFunctionMetaclass):
50
            pass
51
52
        instance_object_1 = MyExampleClass()
53
        instance_object_2 = MyExampleClass()
54
        assert id(MyExampleClass.subject) == id(instance_object_1.subject) == id(instance_object_2.subject)
55
    """
56
    def __new__(mcs, *args, **kwargs):
57
        """Create a new class type object and set the 'subject' attribute to a new Subject instance; the broadcaster.
58
59
        Returns:
60
            PhiFunctionMetaclass: the new class type object
61
        """
62
        phi_function_class = super().__new__(mcs, *args, **kwargs)
63
        phi_function_class.subject = Subject([])
64
        return phi_function_class
65
66
67
class PhiFunctionRegistrator(metaclass=PhiFunctionMetaclass):
68
    """Add phi functions to the registry and notify observers/listeners.
69
70
    This class provides the 'register' decorator, that client can use to decorate either functions (defined with the
71
    def python special word), or classes (defined with the python class special word).
72
    """
73
74
    # NICE TO HAVE: make the decorator work without parenthesis
75
    @classmethod
76
    def register(cls, phi_name=''):
77
        """Add a new phi function to phi function registry and notify listeners/observers.
78
79
        Use this decorator around either a callable function (defined with the 'def' python special word) or a class
80
        with a takes-no-arguments (or all-optional-arguments) constructor and a __call__ magic method.
81
82
        All phi functions are expected to be registered with a __name__ and a __doc__ attribute.
83
84
        You can select your custom phi_name under which to register the phi function or default to an automatic
85
        determination of the phi_name to use.
86
87
        Automatic determination of phi_name is done by examining either the __name__ attribute of the function or the
88
        class name of the class.
89
90
        Example:
91
92
            >>> from so_magic.data.features.phi import PhiFunctionRegistry, PhiFunctionRegistrator
93
94
            >>> registered_phis = PhiFunctionRegistry()
95
96
            >>> @PhiFunctionRegistrator.register()
97
            ... def f1(x):
98
            ...  return x * 2
99
            Registering input function f1 as phi function, at key f1.
100
101
            >>> input_value = 5
102
            >>> print(f"{input_value} * 2 = {registered_phis.get('f1')(input_value)}")
103
            5 * 2 = 10
104
105
            >>> @PhiFunctionRegistrator.register()
106
            ... class f2:
107
            ...  def __call__(self, data, **kwargs):
108
            ...   return data + 5
109
            Registering input class f2 instance as phi function, at key f2.
110
111
            >>> input_value = 1
112
            >>> print(f"{input_value} + 5 = {registered_phis.get('f2')(input_value)}")
113
            1 + 5 = 6
114
115
            >>> @PhiFunctionRegistrator.register('f3')
116
            ... class MyCustomClass:
117
            ...  def __call__(self, data, **kwargs):
118
            ...   return data + 1
119
            Registering input class MyCustomClass instance as phi function, at key f3.
120
121
            >>> input_value = 3
122
            >>> print(f"{input_value} + 1 = {registered_phis.get('f3')(input_value)}")
123
            3 + 1 = 4
124
125
        Args:
126
            phi_name (str, optional): custom name to register the phi function. Defaults to automatic computation.
127
        """
128
        def wrapper(a_callable):
129
            """Add a callable object to the phi function registry and preserve info for __name__ and __doc__ attributes.
130
131
            The callable object should either be function (defined with def) or a class (defined with class). In case of
132
            a class the class must have a constructor that takes no arguments (or all arguments are optional) and a
133
            __call__ magic method.
134
135
            Registers the callable as a phi function under the given or automatically computed name, makes sure the
136
            __name__ and __doc__ attributes preserve information and notifies potential listeners/observers.
137
138
            Args:
139
                a_callable (Callable): the object (function or class) to register as phi function
140
            """
141
            if hasattr(a_callable, '__code__'):  # it is a function (def func_name ..)
142
                logging.info("Registering input function %s as phi function.", a_callable.__code__.co_name)
143
                key = phi_name if phi_name else cls.get_name(a_callable)
144
                print(f"Registering input function {a_callable.__code__.co_name} as phi function, at key {key}.")
145
                cls._register(a_callable, key)
146
            else:
147
                if not hasattr(a_callable, '__call__'):
148
                    raise RuntimeError("Expected an class definition with a '__call__' instance method defined 1."
149
                                       f" Got {type(a_callable)}")
150
                members = inspect.getmembers(a_callable)
151
                if ('__call__', a_callable.__call__) not in members:
152
                    raise RuntimeError("Expected an class definition with a '__call__' instance method defined 2."
153
                                       f" Got {type(a_callable)}")
154
                instance = a_callable()
155
                instance.__name__ = a_callable.__name__
156
                instance.__doc__ = a_callable.__call__.__doc__
157
                key = phi_name if phi_name else cls.get_name(instance)
158
                print(f"Registering input class {a_callable.__name__} instance as phi function, at key {key}.")
159
                cls._register(instance, key)
160
            return a_callable
161
        return wrapper
162
163
    @classmethod
164
    def _register(cls, a_callable, key_name):
165
        """Register a callable as phi function and notify potential listeners/observers.
166
167
        The phi function is registered under the given key_name or in case of None the name is automatically computed
168
        based on the input callable.
169
170
        Args:
171
            a_callable (Callable): the callable that holds the business logic of the phi function
172
            key_name (str, optional): custom phi name. Defaults to None, which means automatic determination of the name
173
        """
174
        phi_registry.add(key_name, a_callable)
175
        cls.subject.name = key_name
176
        cls.subject.state = a_callable
177
        cls.subject.notify()
178
179
    @staticmethod
180
    def get_name(a_callable: Callable):
181
        """Get the 'name' of the input callable object
182
183
        Args:
184
            a_callable (Callable): a callable object to get its name
185
186
        Returns:
187
            str: the name of the callable object
188
        """
189
        if hasattr(a_callable, 'name'):
190
            return a_callable.name
191
        if hasattr(a_callable, '__code__') and hasattr(a_callable.__code__, 'co_name'):
192
            return a_callable.__code__.co_name
193
        if hasattr(type(a_callable), 'name'):
194
            return type(a_callable).name
195
        if hasattr(type(a_callable), '__name__'):
196
            return type(a_callable).__name__
197
        # TODO replace below line with a raise Exception
198
        # we want to cause an error when we fail to get a sensible string name
199
        return ''
200
201
202
if __name__ == '__main__':
203
    reg1 = PhiFunctionRegistry()
204
    reg2 = PhiFunctionRegistry()
205
    reg3 = PhiFunctionRegistry.get_instance()
206
207
    assert id(reg1) == id(reg2) == id(reg3)
208
209
    @PhiFunctionRegistrator.register
210
    def example():
211
        """Inherited Docstring"""
212
        print('Called example function')
213
214
    example()
215
216
    print(example.__name__)
217
    print('--')
218
    print(example.__doc__)
219
220
    reg1.get('example')()
221