Passed
Push — dev ( 4bfa05...67478a )
by Konstantinos
01:31
created

green_magic.data.command_factories   A

Complexity

Total Complexity 21

Size/Duplication

Total Lines 168
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 21
eloc 117
dl 0
loc 168
rs 10
c 0
b 0
f 0

13 Methods

Rating   Name   Duplication   Size   Complexity  
A MyDecorator.magic_decorator() 0 25 2
A CommandRegistrator.func_decorator() 0 9 2
A GenericCommandFactory.construct() 0 3 1
A CommandRegistrator.__new__() 0 5 1
A CommandRegistrator.__getitem__() 0 4 2
A NominalAttributeListEncodeCommandFactory.construct() 0 11 1
A FunctionCommandFactory.construct() 0 5 2
A CommandFactory.pick() 0 8 3
A AbstractCommandFactory.construct() 0 3 1
A BaseCommandFactory.register_as_subclass() 0 6 1
A CommandFactory.create() 0 21 2
A BaseCommandFactory.create() 0 5 2
A MagicCommandFactory.__call__() 0 5 1
1
from abc import ABC, abstractmethod
2
import attr
3
from green_magic.utils import Command
4
from green_magic.utils import Subject
5
6
7
class MyDecorator(type):
8
    """Metaclass that provides a decorator able to be invoked both with and without parenthesis.
9
    The wrapper function logic should be implemented by the client code.
10
    """
11
    @classmethod
12
    def magic_decorator(mcs, arg=None):
13
        print("DEBUG magic_decorator 1, args:", str(arg))
14
        def decorator(func):
15
            print("DEBUG magic_decorator 2, args:", str(func))
16
            def wrapper(*a, **ka):
17
                print("DEBUG magic_decorator 3, args: [{}]".format(', '.join(str(x) for x in a)))
18
                ffunc = a[0]
19
                mcs._wrapper(ffunc, *a[1:], **ka)
20
                return ffunc
21
            print("DEBUG magic_decorator 4, func: {}".format(str(func)))
22
            return wrapper
23
24
        if callable(arg):
25
            print("Decoration invokation WITHOUT parenthesis")
26
            _ = decorator(arg)
27
            print("OUT: {}".format(str(_)))
28
            print(type(_))
29
            return _  # return 'wrapper'
30
        else:
31
            print("Decoration invokation WITH parenthesis")
32
            _ = decorator
33
            print(f"OUT: {str(_)}")
34
            print(type(_))
35
            return _  # ... or 'decorator'
36
37
class CommandRegistrator(MyDecorator):
38
    """Classes can use this class as metaclass to obtain a single registration point accessible as class attribute
39
    """
40
    def __new__(mcs, *args, **kwargs):
41
        class_object = super().__new__(mcs, *args, **kwargs)
42
        class_object.state = None
43
        class_object.registry = {}
44
        return class_object
45
46
    def __getitem__(self, item):
47
        if item not in self.registry:
48
            raise RuntimeError(f"Key '{item}' fot found in registry: [{', '.join(str(x) for x in self.registry.keys())}]")
49
        return self.registry[item]
50
51
    def func_decorator(cls):
52
        def wrapper(a_callable):
53
            if hasattr(a_callable, '__code__'):  # it a function (def func_name ..)
54
                print(f"Registering input function {a_callable.__code__.co_name}")
55
                cls.registry[a_callable.__code__.co_name] = a_callable
56
            else:
57
                raise RuntimeError(f"Expected a function to be decorated; got {type(a_callable)}")
58
            return a_callable
59
        return wrapper
60
61
class AbstractCommandFactory(ABC):
62
    @abstractmethod
63
    def construct(self, *args, **kwargs) -> Command:
64
        raise NotImplementedError
65
66
class BaseCommandFactory(AbstractCommandFactory, ABC):
67
68
    subclasses = {}
69
70
    @classmethod
71
    def register_as_subclass(cls, factory_type):
72
        def wrapper(subclass):
73
            cls.subclasses[factory_type] = subclass
74
            return subclass
75
        return wrapper
76
77
    @classmethod
78
    def create(cls, factory_type, *args, **kwargs):
79
        if factory_type not in cls.subclasses:
80
            raise ValueError('Bad "Factory type" \'{}\''.format(factory_type))
81
        return cls.subclasses[factory_type](*args, **kwargs)
82
83
84
@BaseCommandFactory.register_as_subclass('generic')
85
class GenericCommandFactory(AbstractCommandFactory):
86
    def construct(self, *args, **kwargs) -> Command:
87
        print("GENERIC", args)
88
        return Command(*args, **kwargs)
89
90
@BaseCommandFactory.register_as_subclass('function')
91
class FunctionCommandFactory(AbstractCommandFactory):
92
    def construct(self, *args, **kwargs) -> Command:
93
        print("FUNCTION", args)
94
        if len(args) < 1:
95
            raise RuntimeError("Will break")
96
        return Command(args[0], '__call__', *args[1:])
97
98
99
@BaseCommandFactory.register_as_subclass('encode_nominal_subsets')
100
class NominalAttributeListEncodeCommandFactory(AbstractCommandFactory):
101
    def construct(self, *args, **kwargs) -> Command:
102
        from green_magic.data.features.phis import ListOfCategoricalPhi, DatapointsAttributePhi
103
        assert len(args) > 0
104
        datapoints = args[0]
105
        attribute = args[1]
106
        new_attribute = args[2]
107
        def _command(_datapoints, _attribute, _new_attribute):
108
            phi = ListOfCategoricalPhi(DatapointsAttributePhi(_datapoints))
109
            new_values = phi(_attribute)
110
            _datapoints.mutator.add_column(_datapoints, new_values, _new_attribute)
111
        return Command(_command, '__call__', datapoints, attribute, new_attribute)
112
113
114
class CommandFactory:
115
    """A factory class able to construct new command objects."""
116
    constructors = {k: v().construct for k, v in BaseCommandFactory.subclasses.items()}
117
    @classmethod
118
    def pick(cls, *args, **kwargs):
119
        decision = {True: 'function', False: 'generic'}
120
        is_function = hasattr(args[0], '__code__')
121
        print(f'is function?: {is_function}')
122
        print(f'PICK: {args}')
123
        dec2 = {'function': lambda x: x[0].__code__.co_name, 'generic': lambda x: type(x[0]).__name__ + '-' + x[1]}
124
        return decision[is_function], kwargs.get('name', dec2[decision[is_function]](args))
125
126
    @classmethod
127
    def create(cls, *args, **kwargs) -> Command:
128
        """Call to create a new Command object. The input arguments can be in two formats:
129
130
        1. create(an_object, method, *arguments)
131
        In this case the command is of the form an_object.method(*arguments)
132
133
        2. create(a_function, *arguments)
134
        In this case the command is of the form a_function(*arguments)
135
136
        Returns:
137
            Command: an instance of a command object
138
        """
139
        print("OPA 1", args)
140
141
        key, name = cls.pick(*args, **kwargs)
142
        print(f"KEY: {key}, NAME: {name}")
143
        if len(args) < 1:
144
            raise RuntimeError(args)
145
        print("OPA 2", args)
146
        return cls.constructors[key](*args), name
147
        # is_function = hasattr(args[0], '__code__')
148
        # if is_function:  # if receiver is a function; creating from function
149
        #     return cls.command_constructor(args[0], '__call__', *args[1:]), args[0].__code__.co_name
150
        # return cls.command_constructor(args[0], args[1], *args[2:]), type(args[0]) + '-' + args[1]
151
152
153
@attr.s
154
class MagicCommandFactory(Subject):
155
    """Instances of this class act as callable command factories that notify,
156
    subscribed observers/listeners upon new command object creation.
157
158
    Args:
159
        command_factory (CommandFactory, optional): an instance of a CommandFActory
160
    """
161
    command_factory = attr.ib(init=True, default=CommandFactory())
162
163
    def __call__(self, *args, **kwargs):
164
        assert args
165
        self._state, self.name = self.command_factory.create(*args)
166
        self.notify()
167
        return self._state
168