Passed
Pull Request — dev (#32)
by Konstantinos
03:07 queued 01:29
created

ce()   A

Complexity

Conditions 1

Size

Total Lines 8
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
nop 0
dl 0
loc 8
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 Subject
8
9
logger = logging.getLogger(__name__)
10
11
12
class PhiFunctionMetaclass(type):
13
    """Class type with a single broadcasting (notifies listeners) facility.
14
15
    Classes using this class as metaclass, obtain a single broadcasting facility
16
    as a class attribute. The class attribute is called 'subject' can be referenced
17
    as any class attribute.
18
19
    Example:
20
        class MyExampleClass(metaclass=PhiFunctionMetaclass):
21
            pass
22
23
        instance_object_1 = MyExampleClass()
24
        instance_object_2 = MyExampleClass()
25
        assert id(MyExampleClass.subject) == id(instance_object_1.subject) == id(instance_object_2.subject)
26
    """
27
    def __new__(mcs, *args, **kwargs):
28
        """Create a new class type object and set the 'subject' attribute to a new Subject instance; the broadcaster.
29
30
        Returns:
31
            PhiFunctionMetaclass: the new class type object
32
        """
33
        phi_function_class = super().__new__(mcs, *args, **kwargs)
34
        phi_function_class.subject = Subject([])
35
        return phi_function_class
36
37
38
class PhiFunctionRegistrator(metaclass=PhiFunctionMetaclass):
39
    """Add phi functions to the registry and notify observers/listeners.
40
41
    This class provides the 'register' decorator, that client can use to decorate either functions (defined with the
42
    def python special word), or classes (defined with the python class special word).
43
    """
44
45
    # NICE TO HAVE: make the decorator work without parenthesis
46
    @classmethod
47
    def register(cls, phi_name=''):
48
        r"""Add a new phi function to phi function registry and notify listeners/observers.
49
50
        Use this decorator around either a callable function (defined with the 'def' python special word) or a class
51
        with a takes-no-arguments (or all-optional-arguments) constructor and a __call__ magic method.
52
53
        All phi functions are expected to be registered with a __name__ and a __doc__ attribute.
54
55
        You can select your custom phi_name under which to register the phi function or default to an automatic
56
        determination of the phi_name to use.
57
58
        Automatic determination of phi_name is done by examining either the __name__ attribute of the function or the
59
        class name of the class.
60
61
        Example:
62
63
            >>> from so_magic.data.features.phi import PhiFunctionRegistry, PhiFunctionRegistrator
64
65
            >>> registered_phis = PhiFunctionRegistry()
66
67
            >>> @PhiFunctionRegistrator.register()
68
            ... def f1(x):
69
            ...  '''Multiply by 2.'''
70
            ...  return x * 2
71
            Registering input function f1 as phi function, at key f1.
72
73
            >>> registered_phis.get('f1').__doc__
74
            'Multiply by 2.'
75
76
            >>> input_value = 5
77
            >>> print(f"{input_value} * 2 = {registered_phis.get('f1')(input_value)}")
78
            5 * 2 = 10
79
80
            >>> @PhiFunctionRegistrator.register()
81
            ... class f2:
82
            ...  def __call__(self, data, **kwargs):
83
            ...   return data + 5
84
            Registering input class f2 instance as phi function, at key f2.
85
86
            >>> input_value = 1
87
            >>> print(f"{input_value} + 5 = {registered_phis.get('f2')(input_value)}")
88
            1 + 5 = 6
89
90
            >>> @PhiFunctionRegistrator.register('f3')
91
            ... class MyCustomClass:
92
            ...  def __call__(self, data, **kwargs):
93
            ...   return data + 1
94
            Registering input class MyCustomClass instance as phi function, at key f3.
95
96
            >>> input_value = 3
97
            >>> print(f"{input_value} + 1 = {registered_phis.get('f3')(input_value)}")
98
            3 + 1 = 4
99
100
        Args:
101
            phi_name (str, optional): custom name to register the phi function. Defaults to automatic computation.
102
        """
103
        def wrapper(a_callable):
104
            """Add a callable object to the phi function registry and preserve info for __name__ and __doc__ attributes.
105
106
            The callable object should either be function (defined with def) or a class (defined with class). In case of
107
            a class the class must have a constructor that takes no arguments (or all arguments are optional) and a
108
            __call__ magic method.
109
110
            Registers the callable as a phi function under the given or automatically computed name, makes sure the
111
            __name__ and __doc__ attributes preserve information and notifies potential listeners/observers.
112
113
            Args:
114
                a_callable (Callable): the object (function or class) to register as phi function
115
            """
116
            if hasattr(a_callable, '__code__'):  # it is a function (def func_name ..)
117
                logging.info("Registering input function %s as phi function.", a_callable.__code__.co_name)
118
                key = phi_name if phi_name else cls.get_name(a_callable)
119
                print(f"Registering input function {a_callable.__code__.co_name} as phi function, at key {key}.")
120
                cls._register(a_callable, key)
121
            else:
122
                if not hasattr(a_callable, '__call__'):
123
                    raise RuntimeError("Expected an class definition with a '__call__' instance method defined 1."
124
                                       f" Got {type(a_callable)}")
125
                members = inspect.getmembers(a_callable)
126
                if ('__call__', a_callable.__call__) not in members:
127
                    raise RuntimeError("Expected an class definition with a '__call__' instance method defined 2."
128
                                       f" Got {type(a_callable)}")
129
                instance = a_callable()
130
                instance.__name__ = a_callable.__name__
131
                instance.__doc__ = a_callable.__call__.__doc__
132
                key = phi_name if phi_name else cls.get_name(instance)
133
                print(f"Registering input class {a_callable.__name__} instance as phi function, at key {key}.")
134
                cls._register(instance, key)
135
            return a_callable
136
        return wrapper
137
138
    @classmethod
139
    def _register(cls, a_callable, key_name):
140
        """Register a callable as phi function and notify potential listeners/observers.
141
142
        The phi function is registered under the given key_name or in case of None the name is automatically computed
143
        based on the input callable.
144
145
        Args:
146
            a_callable (Callable): the callable that holds the business logic of the phi function
147
            key_name (str, optional): custom phi name. Defaults to None, which means automatic determination of the name
148
        """
149
        cls.subject.name = key_name
150
        cls.subject.state = a_callable
151
        cls.subject.notify()
152
153
    @staticmethod
154
    def get_name(a_callable: Callable):
155
        """Get the 'name' of the input callable object
156
157
        Args:
158
            a_callable (Callable): a callable object to get its name
159
160
        Returns:
161
            str: the name of the callable object
162
        """
163
        if hasattr(a_callable, 'name'):
164
            return a_callable.name
165
        if hasattr(a_callable, '__code__') and hasattr(a_callable.__code__, 'co_name'):
166
            return a_callable.__code__.co_name
167
        if hasattr(type(a_callable), 'name'):
168
            return type(a_callable).name
169
        if hasattr(type(a_callable), '__name__'):
170
            return type(a_callable).__name__
171
        raise PhiFunctionNameDeterminationError()
172
173
174
class PhiFunctionNameDeterminationError(Exception): pass
175