so_magic.data.features.phi   A
last analyzed

Complexity

Total Complexity 14

Size/Duplication

Total Lines 184
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 50
dl 0
loc 184
rs 10
c 0
b 0
f 0
wmc 14

4 Methods

Rating   Name   Duplication   Size   Complexity  
A PhiFunctionMetaclass.__new__() 0 9 1
B PhiFunctionRegistrator.get_name() 0 19 6
B PhiFunctionRegistrator.register() 0 100 6
A PhiFunctionRegistrator._register() 0 14 1
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 PhiFunctionRegistrator
64
            >>> from so_magic.utils import Observer, ObjectRegistry
65
66
            >>> class PhiFunctionRegistry(Observer):
67
            ...  def __init__(self):
68
            ...   self.registry = ObjectRegistry()
69
            ...  def update(self, subject, *args, **kwargs):
70
            ...   self.registry.add(subject.name, subject.state)
71
72
            >>> phis = PhiFunctionRegistry()
73
74
            >>> PhiFunctionRegistrator.subject.add(phis)
75
76
            >>> @PhiFunctionRegistrator.register()
77
            ... def f1(x):
78
            ...  '''Multiply by 2.'''
79
            ...  return x * 2
80
            Registering input function f1 as phi function, at key f1.
81
82
            >>> phis.registry.get('f1').__doc__
83
            'Multiply by 2.'
84
85
            >>> input_value = 5
86
            >>> print(f"{input_value} * 2 = {phis.registry.get('f1')(input_value)}")
87
            5 * 2 = 10
88
89
            >>> @PhiFunctionRegistrator.register()
90
            ... class f2:
91
            ...  def __call__(self, data, **kwargs):
92
            ...   return data + 5
93
            Registering input class f2 instance as phi function, at key f2.
94
95
            >>> input_value = 1
96
            >>> print(f"{input_value} + 5 = {phis.registry.get('f2')(input_value)}")
97
            1 + 5 = 6
98
99
            >>> @PhiFunctionRegistrator.register('f3')
100
            ... class MyCustomClass:
101
            ...  def __call__(self, data, **kwargs):
102
            ...   return data + 1
103
            Registering input class MyCustomClass instance as phi function, at key f3.
104
105
            >>> input_value = 3
106
            >>> print(f"{input_value} + 1 = {phis.registry.get('f3')(input_value)}")
107
            3 + 1 = 4
108
109
        Args:
110
            phi_name (str, optional): custom name to register the phi function. Defaults to automatic computation.
111
        """
112
        def wrapper(a_callable):
113
            """Add a callable object to the phi function registry and preserve info for __name__ and __doc__ attributes.
114
115
            The callable object should either be function (defined with def) or a class (defined with class). In case of
116
            a class the class must have a constructor that takes no arguments (or all arguments are optional) and a
117
            __call__ magic method.
118
119
            Registers the callable as a phi function under the given or automatically computed name, makes sure the
120
            __name__ and __doc__ attributes preserve information and notifies potential listeners/observers.
121
122
            Args:
123
                a_callable (Callable): the object (function or class) to register as phi function
124
            """
125
            if hasattr(a_callable, '__code__'):  # it is a function (def func_name ..)
126
                logging.info("Registering input function %s as phi function.", a_callable.__code__.co_name)
127
                key = phi_name if phi_name else cls.get_name(a_callable)
128
                print(f"Registering input function {a_callable.__code__.co_name} as phi function, at key {key}.")
129
                cls._register(a_callable, key)
130
            else:
131
                if not hasattr(a_callable, '__call__'):
132
                    raise RuntimeError("Expected an class definition with a '__call__' instance method defined 1."
133
                                       f" Got {type(a_callable)}")
134
                members = inspect.getmembers(a_callable)
135
                if ('__call__', a_callable.__call__) not in members:
136
                    raise RuntimeError("Expected an class definition with a '__call__' instance method defined 2."
137
                                       f" Got {type(a_callable)}")
138
                instance = a_callable()
139
                instance.__name__ = a_callable.__name__
140
                instance.__doc__ = a_callable.__call__.__doc__
141
                key = phi_name if phi_name else cls.get_name(instance)
142
                print(f"Registering input class {a_callable.__name__} instance as phi function, at key {key}.")
143
                cls._register(instance, key)
144
            return a_callable
145
        return wrapper
146
147
    @classmethod
148
    def _register(cls, a_callable, key_name):
149
        """Register a callable as phi function and notify potential listeners/observers.
150
151
        The phi function is registered under the given key_name or in case of None the name is automatically computed
152
        based on the input callable.
153
154
        Args:
155
            a_callable (Callable): the callable that holds the business logic of the phi function
156
            key_name (str, optional): custom phi name. Defaults to None, which means automatic determination of the name
157
        """
158
        cls.subject.name = key_name
159
        cls.subject.state = a_callable
160
        cls.subject.notify()
161
162
    @staticmethod
163
    def get_name(a_callable: Callable):
164
        """Get the 'name' of the input callable object
165
166
        Args:
167
            a_callable (Callable): a callable object to get its name
168
169
        Returns:
170
            str: the name of the callable object
171
        """
172
        if hasattr(a_callable, 'name'):
173
            return a_callable.name
174
        if hasattr(a_callable, '__code__') and hasattr(a_callable.__code__, 'co_name'):
175
            return a_callable.__code__.co_name
176
        if hasattr(type(a_callable), 'name'):
177
            return type(a_callable).name
178
        if hasattr(type(a_callable), '__name__'):
179
            return type(a_callable).__name__
180
        raise PhiFunctionNameDeterminationError()
181
182
183
class PhiFunctionNameDeterminationError(Exception): pass
184