Passed
Pull Request — dev (#32)
by Konstantinos
03:59 queued 02:15
created

so_magic.data.features.phi.example()   A

Complexity

Conditions 1

Size

Total Lines 4
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
nop 0
dl 0
loc 4
rs 10
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
        r"""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
            ...  '''Multiply by 2.'''
99
            ...  return x * 2
100
            Registering input function f1 as phi function, at key f1.
101
102
            >>> registered_phis.get('f1').__doc__
103
            'Multiply by 2.'
104
105
            >>> input_value = 5
106
            >>> print(f"{input_value} * 2 = {registered_phis.get('f1')(input_value)}")
107
            5 * 2 = 10
108
109
            >>> @PhiFunctionRegistrator.register()
110
            ... class f2:
111
            ...  def __call__(self, data, **kwargs):
112
            ...   return data + 5
113
            Registering input class f2 instance as phi function, at key f2.
114
115
            >>> input_value = 1
116
            >>> print(f"{input_value} + 5 = {registered_phis.get('f2')(input_value)}")
117
            1 + 5 = 6
118
119
            >>> @PhiFunctionRegistrator.register('f3')
120
            ... class MyCustomClass:
121
            ...  def __call__(self, data, **kwargs):
122
            ...   return data + 1
123
            Registering input class MyCustomClass instance as phi function, at key f3.
124
125
            >>> input_value = 3
126
            >>> print(f"{input_value} + 1 = {registered_phis.get('f3')(input_value)}")
127
            3 + 1 = 4
128
129
        Args:
130
            phi_name (str, optional): custom name to register the phi function. Defaults to automatic computation.
131
        """
132
        def wrapper(a_callable):
133
            """Add a callable object to the phi function registry and preserve info for __name__ and __doc__ attributes.
134
135
            The callable object should either be function (defined with def) or a class (defined with class). In case of
136
            a class the class must have a constructor that takes no arguments (or all arguments are optional) and a
137
            __call__ magic method.
138
139
            Registers the callable as a phi function under the given or automatically computed name, makes sure the
140
            __name__ and __doc__ attributes preserve information and notifies potential listeners/observers.
141
142
            Args:
143
                a_callable (Callable): the object (function or class) to register as phi function
144
            """
145
            if hasattr(a_callable, '__code__'):  # it is a function (def func_name ..)
146
                logging.info("Registering input function %s as phi function.", a_callable.__code__.co_name)
147
                key = phi_name if phi_name else cls.get_name(a_callable)
148
                print(f"Registering input function {a_callable.__code__.co_name} as phi function, at key {key}.")
149
                cls._register(a_callable, key)
150
            else:
151
                if not hasattr(a_callable, '__call__'):
152
                    raise RuntimeError("Expected an class definition with a '__call__' instance method defined 1."
153
                                       f" Got {type(a_callable)}")
154
                members = inspect.getmembers(a_callable)
155
                if ('__call__', a_callable.__call__) not in members:
156
                    raise RuntimeError("Expected an class definition with a '__call__' instance method defined 2."
157
                                       f" Got {type(a_callable)}")
158
                instance = a_callable()
159
                instance.__name__ = a_callable.__name__
160
                instance.__doc__ = a_callable.__call__.__doc__
161
                key = phi_name if phi_name else cls.get_name(instance)
162
                print(f"Registering input class {a_callable.__name__} instance as phi function, at key {key}.")
163
                cls._register(instance, key)
164
            return a_callable
165
        return wrapper
166
167
    @classmethod
168
    def _register(cls, a_callable, key_name):
169
        """Register a callable as phi function and notify potential listeners/observers.
170
171
        The phi function is registered under the given key_name or in case of None the name is automatically computed
172
        based on the input callable.
173
174
        Args:
175
            a_callable (Callable): the callable that holds the business logic of the phi function
176
            key_name (str, optional): custom phi name. Defaults to None, which means automatic determination of the name
177
        """
178
        phi_registry.add(key_name, a_callable)
179
        cls.subject.name = key_name
180
        cls.subject.state = a_callable
181
        cls.subject.notify()
182
183
    @staticmethod
184
    def get_name(a_callable: Callable):
185
        """Get the 'name' of the input callable object
186
187
        Args:
188
            a_callable (Callable): a callable object to get its name
189
190
        Returns:
191
            str: the name of the callable object
192
        """
193
        if hasattr(a_callable, 'name'):
194
            return a_callable.name
195
        if hasattr(a_callable, '__code__') and hasattr(a_callable.__code__, 'co_name'):
196
            return a_callable.__code__.co_name
197
        if hasattr(type(a_callable), 'name'):
198
            return type(a_callable).name
199
        if hasattr(type(a_callable), '__name__'):
200
            return type(a_callable).__name__
201
        # TODO replace below line with a raise Exception
202
        # we want to cause an error when we fail to get a sensible string name
203
        return ''
204