tests.test_inject   B
last analyzed

Complexity

Total Complexity 45

Size/Duplication

Total Lines 393
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 278
dl 0
loc 393
rs 8.8
c 0
b 0
f 0
wmc 45

33 Methods

Rating   Name   Duplication   Size   Complexity  
A Animal.sound() 0 3 1
A TestInject.test_inject_async_function() 0 3 1
A TestInject.test_set_instance() 0 9 1
A Cat.sound() 0 2 1
A TestInject.test_injection_with_different_container() 0 3 1
A TestInject.test_inject_list() 0 8 1
A Chicken.sound() 0 2 1
A Bird.sound() 0 2 1
B TestInject.test_inject_function() 0 48 1
A TestInject.test_inject_with_default() 0 17 1
A TestInject.test_inject_with_discovery() 0 19 1
A TestInject.test_inject_here() 0 3 1
A TestInject.test_simple_injection_without_parentheses() 0 3 1
A TestInject.test_inject_prio_list() 0 3 1
A TestInject.test_injection_name() 0 4 1
A TestInject.test_register_on_the_fly() 0 19 1
A TestInject.test_inject_into_async_function() 0 7 1
A TestInject.test_inject_multiple() 0 5 1
A TestInject.test_inject_with_given_values() 0 20 1
A Dog.sound() 0 2 1
A Mouse.sound() 0 2 1
A TestInject.test_signature_preservation() 0 15 1
A TestInject.test_singleton() 0 4 1
A TestInject.test_simple_injection() 0 3 1
A TestInject.test_inject_class() 0 3 1
A TestInject.test_inject_fail() 0 16 3
A TestInject.test_inject_method() 0 14 1
A TestInject.test_injection_with_multiple_containers() 0 12 1
A TestInject.test_inject_list_of_classes() 0 6 1
A TestInject.test_inject_injectable() 0 3 1
A TestInject.test_inject_on_class() 0 5 2
A Goat.sound() 0 2 1
A TestInject.test_inject_prio() 0 3 1

9 Functions

Rating   Name   Duplication   Size   Complexity  
A func_empty_float() 0 3 1
A func_list_empty() 0 3 1
A func_str_str() 0 4 1
A func_cat_str() 0 3 1
A func_int_str() 0 3 1
A async_func_str_str() 0 3 1
A injectable_inject() 0 4 1
A func_list_of_str_empty() 0 3 1
A inject_injectable() 0 4 1

How to fix   Complexity   

Complexity

Complex classes like tests.test_inject often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
import asyncio
2
from abc import ABC, abstractmethod
3
from pathlib import Path
4
from typing import Type, List, Callable, Any, Awaitable
5
from unittest import TestCase
6
from jacked import inject, injectable, Injectable
7
from jacked._container import Container, DEFAULT_CONTAINER
8
from jacked._discover import discover
9
from jacked._exceptions import InvalidUsageError, InjectionError
10
from jacked._inject import inject_here
11
from test_resources.color import Color
12
13
14
CUSTOM_CONTAINER = Container()
15
16
17
class Animal(ABC):
18
    @abstractmethod
19
    def sound(self):
20
        raise NotImplementedError
21
22
23
@injectable(singleton=True)
24
class Dog(Animal):
25
    """Docstring for Dog"""
26
    def sound(self):
27
        return 'bark'
28
29
30
@injectable  # No parentheses.
31
class Cat(Animal):
32
    def sound(self):
33
        return 'meow'
34
35
36
@injectable(priority=42)
37
class Bird(Animal):
38
    def sound(self):
39
        return 'tweet'
40
41
42
@injectable(name='Elephant', meta={'size': 'enormous', 'name': 'overridden'})
43
class Mouse(Animal):
44
    def sound(self):
45
        return 'peep'
46
47
48
@injectable(container=CUSTOM_CONTAINER)
49
class Goat(Animal):
50
    def sound(self):
51
        return 'meh'
52
53
54
# The same object injected in two different containers under different names.
55
@injectable()
56
@injectable(container=CUSTOM_CONTAINER, name='Kip')
57
class Chicken(Animal):
58
    def sound(self):
59
        return 'tok'
60
61
62
@injectable()
63
def func_str_str(x: str) -> str:
64
    """Docstring for func_str_str"""
65
    return x.upper()
66
67
68
@injectable
69
@inject
70
def injectable_inject(dog: Dog) -> Dog:
71
    return dog
72
73
74
@inject
75
@injectable
76
def inject_injectable(dog: Dog) -> Dog:
77
    return dog
78
79
80
@injectable()
81
def func_int_str(x: int) -> str:
82
    return str(x)
83
84
85
@injectable()
86
def func_empty_float() -> float:
87
    return 42.0
88
89
90
@injectable()
91
def func_list_empty(x: list):
92
    return x + [1, 2, 3]
93
94
95
@injectable
96
def func_list_of_str_empty(x: List[str]):
97
    return x + ['42']
98
99
100
@injectable()
101
def func_cat_str(cat: Cat) -> str:
102
    return cat.sound()
103
104
105
@injectable()
106
async def async_func_str_str(x: int) -> str:
107
    return '42'
108
109
110
class TestInject(TestCase):
111
    @inject()
112
    def test_simple_injection(self, cat: Cat):
113
        self.assertEqual('meow', cat.sound())
114
115
    @inject()
116
    def test_singleton(self, dog1: Dog, dog2: Dog, cat1: Cat, cat2: Cat):
117
        self.assertEqual(dog1, dog2)
118
        self.assertNotEqual(cat1, cat2)
119
120
    @inject()
121
    def test_inject_prio(self, animal_type: Type[Animal]):
122
        self.assertEqual(Bird, animal_type)
123
124
    @inject()
125
    def test_inject_prio_list(self, animal_types: List[Type[Animal]]):
126
        self.assertEqual(Bird, animal_types[0])
127
128
    @inject()
129
    def test_injection_name(self, mouse: Mouse):
130
        self.assertEqual('Elephant', mouse.__meta__.name)
131
        self.assertEqual('enormous', mouse.__meta__.size)
132
133
    @inject
134
    def test_simple_injection_without_parentheses(self, cat: Cat):
135
        self.assertEqual('meow', cat.sound())
136
137
    @inject(container=CUSTOM_CONTAINER)
138
    def test_injection_with_different_container(self, animal: Animal):
139
        self.assertEqual('meh', animal.sound())
140
141
    def test_inject_here(self):
142
        cat = inject_here(Cat)
143
        self.assertEqual('meow', cat.sound())
144
145
    def test_register_on_the_fly(self):
146
147
        class SomeBaseClass:
148
            pass
149
150
        class SomeDerivedClass(SomeBaseClass):
151
            pass
152
153
        injectable_ = Injectable(
154
            subject=SomeDerivedClass,
155
            priority=0,
156
            singleton=False,
157
            meta={'name': SomeDerivedClass.__name__}
158
        )
159
160
        DEFAULT_CONTAINER.register(injectable_)
161
        injected = inject_here(SomeBaseClass)
162
163
        self.assertTrue(isinstance(injected, SomeBaseClass))
164
165
    def test_set_instance(self):
166
167
        @injectable(singleton=True)
168
        class C:
169
            pass
170
171
        inst = C()
172
        DEFAULT_CONTAINER.set_instance(C, inst, 999)
173
        self.assertEqual(inst, inject_here(C))
174
175
    def test_injection_with_multiple_containers(self):
176
177
        @inject(container=CUSTOM_CONTAINER)
178
        def func1(chicken: Chicken):
179
            self.assertEqual('Kip', chicken.__meta__['name'])
180
181
        @inject()
182
        def func2(chicken: Chicken):
183
            self.assertEqual('Chicken', chicken.__meta__['name'])
184
185
        func1()
186
        func2()
187
188
    @inject()
189
    def test_inject_multiple(self, cat1: Cat, cat2: Cat):
190
        self.assertEqual('meow', cat1.sound())
191
        self.assertEqual('meow', cat2.sound())
192
        self.assertNotEqual(cat1, cat2)
193
194
    @inject()
195
    def test_inject_class(self, t: Type[Cat]):
196
        self.assertEqual(Cat, t)
197
198
    @inject()
199
    def test_inject_list(self, animals: List[Animal]):
200
        sounds = set([animal.sound() for animal in animals])
201
        self.assertTrue('bark' in sounds)
202
        self.assertTrue('meow' in sounds)
203
        self.assertTrue('tweet' in sounds)
204
        self.assertTrue('peep' in sounds)
205
        self.assertTrue('meh' not in sounds)  # Different container.
206
207
    @inject()
208
    def test_inject_list_of_classes(self, animals: List[Type[Animal]]):
209
        sounds = set([animal().sound() for animal in animals])
210
        self.assertTrue('bark' in sounds)
211
        self.assertTrue('meow' in sounds)
212
        self.assertTrue('tweet' in sounds)
213
214
    def test_inject_with_default(self):
215
216
        class NotInjectable:
217
            pass
218
219
        @inject()
220
        def _func(cat: Cat, obj: NotInjectable = 42):
221
            self.assertEqual('meow', cat.sound())
222
            self.assertEqual(42, obj)
223
224
        @inject()
225
        def _func2(animal: Cat = Dog()):
226
            # Dog is a default value but should be overridden by the injection.
227
            self.assertEqual('meow', animal.sound())
228
229
        _func()
230
        _func2()
231
232
    def test_inject_with_given_values(self):
233
234
        @inject()
235
        def _func(animal: Cat, another_animal: Bird):
236
            self.assertEqual('bark', animal.sound())
237
            self.assertEqual('tweet', another_animal.sound())
238
239
        class C:
240
            @inject()
241
            def method(self, animal: Cat, another_animal: Bird):
242
                outer_self.assertEqual('bark', animal.sound())
243
                outer_self.assertEqual('tweet', another_animal.sound())
244
245
        outer_self = self
246
247
        _func(animal=Dog())
248
        _func(Dog())
249
250
        C().method(animal=Dog())
251
        C().method(Dog())
252
253
    def test_inject_on_class(self):
254
        with self.assertRaises(InvalidUsageError):
255
            @inject()
256
            class C:
257
                pass
258
259
    def test_inject_function(self):
260
261
        @inject()
262
        def _func1(callable_: Callable[[int], str]):
263
            self.assertEqual(func_int_str, callable_)
264
265
        @inject()
266
        def _func2(callables: List[Callable[[str], str]]):
267
            self.assertEqual(1, len(callables))
268
            self.assertEqual('HELLO', callables[0]('hello'))
269
270
        @inject()
271
        def _func3(callable_: Callable[[], float]):
272
            self.assertEqual(func_empty_float, callable_)
273
274
        @inject()
275
        def _func4(callables: List[Callable[[Any], str]]):
276
            self.assertEqual(3, len(callables))
277
            self.assertTrue(func_int_str in callables)
278
            self.assertTrue(func_str_str in callables)
279
280
        @inject()
281
        def _func5(callables: List[Callable[[str], Any]]):
282
            self.assertEqual(1, len(callables))
283
            self.assertEqual(func_str_str, callables[0])
284
285
        @inject()
286
        def _func6(callable: Callable[[Animal], str]):
287
            self.assertEqual(func_cat_str, callable)
288
289
        @inject()
290
        def _func7(callables: List[Callable[[list], None]]):
291
            self.assertTrue(func_list_empty in callables)
292
            self.assertTrue(func_list_of_str_empty in callables)
293
294
        @inject()
295
        def _func8(callables: List[Callable[[List[str]], None]]):
296
            self.assertTrue(func_list_empty not in callables)
297
            self.assertTrue(func_list_of_str_empty in callables)
298
299
        _func1()
300
        _func2()
301
        _func3()
302
        _func4()
303
        _func5()
304
        _func6()
305
        _func7()
306
        _func8()
307
308
    def test_inject_method(self):
309
310
        local_container = Container()
311
312
        class SomeLocalClass:
313
            @injectable(container=local_container)
314
            def m(self, x: int) -> str:
315
                return '42'
316
317
        @inject(container=local_container)
318
        def func(method: Callable[[Any, int], str]):
319
            self.assertEqual(SomeLocalClass.m, method)
320
321
        func()
322
323
    def test_inject_into_async_function(self):
324
325
        @inject()
326
        async def func(animal: Cat):
327
            self.assertEqual('meow', animal.sound())
328
329
        asyncio.get_event_loop().run_until_complete(func())
330
331
    @inject()
332
    def test_inject_async_function(self, f1: Callable[[int], Awaitable[str]]):
333
        self.assertEqual(async_func_str_str, f1)
334
335
        # TODO test also with typing.Coroutine
336
337
    def test_inject_fail(self):
338
339
        class NotInjectable:
340
            pass
341
342
        @inject()
343
        def _func(obj: NotInjectable):
344
            pass
345
346
        with self.assertRaises(InjectionError):
347
            _func()
348
349
        try:
350
            _func()
351
        except InjectionError as err:
352
            self.assertEqual('obj', err.subject.name)
353
354
    def test_inject_with_discovery(self):
355
356
        @inject()
357
        def get_all_colors(colors: List[Color]):
358
            color_names = set([color.name() for color in colors])
359
            self.assertEqual(3, len(color_names))
360
            self.assertTrue('RED' in color_names)
361
            self.assertTrue('GREEN' in color_names)
362
            self.assertTrue('BLUE' in color_names)
363
364
        p = Path(__file__).parent.parent.joinpath('test_resources/injectables')
365
        discoveries = discover(str(p))
366
        module_names = [module.__name__ for module in discoveries]
367
368
        self.assertTrue('red' in module_names)
369
        self.assertTrue('green' in module_names)
370
        self.assertTrue('blue' in module_names)
371
372
        get_all_colors()
373
374
    def test_signature_preservation(self):
375
376
        @inject
377
        def func1(dog: Dog):
378
            """Docstring for func1"""
379
            self.assertEqual('Docstring for Dog', dog.__class__.__doc__)
380
381
        @inject
382
        def func2(c: Callable[[str], str]):
383
            self.assertEqual('Docstring for func_str_str', c.__doc__)
384
385
        func1()
386
        func2()
387
388
        self.assertEqual('Docstring for func1', func1.__doc__)
389
390
    @inject
391
    def test_inject_injectable(self, l: List[Callable[[Dog], Dog]]):
392
        self.assertEqual(2, len(l))
393