Skip to content
Permalink
Browse files

Stop supporting decorating classes with @Inject

It is a future of limited use (just a convenience) but it can confuse
some people (it's not immediately clear how @inject-decorated class'
constructors behave and what's the relationship between Injector and
constructors). Additionally it doesn't cooperate nicely with static
analysis tools like mypy, for example:

    @Inject(s=SomeOtherClass)
    class C:
        pass

No tool knows there's an attribute all C instances have and that its
type is SomeOtherClass. They'll know in this case:

    class C:
        @Inject(s=SomeOtherClass)
        def __init__(self, s: SomeOtherClass) -> None:
            self.s = s

Granted, there's repetition but an Injector change will land soon to
address this.
  • Loading branch information
jstasiak committed Oct 17, 2016
1 parent 3766717 commit 25f2455d926a721ca6087f6ec2acfdc85d1e01aa
Showing with 3 additions and 137 deletions.
  1. +2 −24 injector.py
  2. +1 −113 injector_test.py
@@ -1007,32 +1007,10 @@ def inject(self_, *args, **kwargs):
inject.__bindings__ = merged_bindings
return inject

def class_wrapper(cls):
orig_init = cls.__init__

original_keys = tuple(bindings.keys())

for k in list(bindings.keys()):
bindings[k.lstrip('_')] = bindings.pop(k)

@inject(**bindings)
def init(self, *args, **kwargs):
try:
for key in original_keys:
normalized_key = key.lstrip('_')
setattr(self, key, kwargs.pop(normalized_key))
except KeyError as e:
reraise(e, CallError(
'Keyword argument %s not found when calling %s' % (
normalized_key, '%s.%s' % (cls.__name__, '__init__'))))

orig_init(self, *args, **kwargs)
cls.__init__ = init
return cls

def multi_wrapper(something):
if isinstance(something, type):
return class_wrapper(something)
raise TypeError('Decorating classes with @inject is no longer supported, ' +
'provide constructor and decorate it')
else:
return method_wrapper(something)

@@ -931,114 +931,6 @@ def test_singleton_scope_is_thread_safe(self):
assert (a is b)


class TestClassInjection(object):
def setup(self):
class A(object):
counter = 0

def __init__(self):
A.counter += 1

@inject(a=A)
class B(object):
pass

@inject(injectable=A)
class C(object):
def __init__(self, noninjectable):
self.noninjectable = noninjectable

self.injector = Injector()
self.A = A
self.B = B
self.C = C

def test_inject_decorator_works_when_metaclass_used(self):
WithABCMeta = abc.ABCMeta(str('WithABCMeta'), (object,), {})

@inject(y=int)
class X(WithABCMeta):
pass

self.injector.get(X)

def test_instantiation_still_requires_parameters(self):
for cls in (self.B, self.C):
with pytest.raises(Exception):
cls()

try:
self.C(noninjectable=1)
assert False, 'Should have raised an exception'
except Exception as e:
check_exception_contains_stuff(e, ('C.__init__', 'injectable'))

with pytest.raises(Exception):
self.C(injectable=self.A())

def test_injection_works(self):
b = self.injector.get(self.B)
a = b.a
assert (type(a) == self.A)

def test_assisted_injection_works(self):
builder = self.injector.get(AssistedBuilder(self.C))
c = builder.build(noninjectable=5)

assert((type(c.injectable), c.noninjectable) == (self.A, 5))

def test_members_are_injected_only_once(self):
b = self.injector.get(self.B)
_1 = b.a
_2 = b.a
assert (self.A.counter == 1 and _1 is _2)

def test_each_instance_gets_new_injection(self):
count = 3
objs = [self.injector.get(self.B).a for i in range(count)]

assert (self.A.counter == count)
assert (len(set(objs)) == count)

def test_members_can_be_overwritten(self):
b = self.injector.get(self.B)
b.a = 123

assert (b.a == 123)

def test_injected_members_starting_with_underscore_generate_sane_constructor(self):
@inject(_b=self.B)
class X(object):
pass

x = self.injector.get(X)
assert (type(x._b) == self.B)

x = X(b=314)
assert (x._b == 314)

def test_correct_exception_is_raised_when_argument_is_missing(self):
@inject(s=str)
class X(object):
pass

with pytest.raises(CallError):
self.B()

with pytest.raises(CallError):
self.B('something')

def test_mutating_dict_while_iterating_it_bug(self):
bindings = dict(('_' + str(i), str) for i in range(1000))

@inject(**bindings)
class X(object):
pass

injector = Injector()
injector.get(X)


def test_provides_and_scope_decorator_collaboration():
@provides(int)
@singleton
@@ -1105,15 +997,11 @@ class Test2(Module):
def __init__(self, name):
pass

@inject(name=int)
class Test3(Module):
pass

@inject(name=int)
def configure(binder, name):
pass

for module in [Test, Test2, Test3, configure, Test()]:
for module in [Test, Test2, configure, Test()]:
with warnings.catch_warnings(record=True) as w:
print(module)
Injector(module)

0 comments on commit 25f2455

Please sign in to comment.
You can’t perform that action at this time.