1535 lines
39 KiB
Python
1535 lines
39 KiB
Python
# encoding: utf-8
|
||
#
|
||
# Copyright (C) 2010 Alec Thomas <alec@swapoff.org>
|
||
# All rights reserved.
|
||
#
|
||
# This software is licensed as described in the file COPYING, which
|
||
# you should have received as part of this distribution.
|
||
#
|
||
# Author: Alec Thomas <alec@swapoff.org>
|
||
|
||
"""Functional tests for the "Injector" dependency injection framework."""
|
||
|
||
from contextlib import contextmanager
|
||
from typing import Any, NewType, Optional, Union
|
||
import abc
|
||
import sys
|
||
import threading
|
||
import traceback
|
||
import warnings
|
||
|
||
from typing import Dict, List, NewType
|
||
|
||
import pytest
|
||
|
||
from injector import (
|
||
Binder,
|
||
CallError,
|
||
Inject,
|
||
Injector,
|
||
NoInject,
|
||
Scope,
|
||
InstanceProvider,
|
||
ClassProvider,
|
||
get_bindings,
|
||
inject,
|
||
multiprovider,
|
||
noninjectable,
|
||
singleton,
|
||
threadlocal,
|
||
UnsatisfiedRequirement,
|
||
CircularDependency,
|
||
Module,
|
||
SingletonScope,
|
||
ScopeDecorator,
|
||
AssistedBuilder,
|
||
provider,
|
||
ProviderOf,
|
||
ClassAssistedBuilder,
|
||
Error,
|
||
UnknownArgument,
|
||
)
|
||
|
||
|
||
class EmptyClass:
|
||
pass
|
||
|
||
|
||
class DependsOnEmptyClass:
|
||
@inject
|
||
def __init__(self, b: EmptyClass):
|
||
"""Construct a new DependsOnEmptyClass."""
|
||
self.b = b
|
||
|
||
|
||
def prepare_nested_injectors():
|
||
def configure(binder):
|
||
binder.bind(str, to='asd')
|
||
|
||
parent = Injector(configure)
|
||
child = parent.create_child_injector()
|
||
return parent, child
|
||
|
||
|
||
def check_exception_contains_stuff(exception, stuff):
|
||
stringified = str(exception)
|
||
|
||
for thing in stuff:
|
||
assert thing in stringified, '%r should be present in the exception representation: %s' % (
|
||
thing,
|
||
stringified,
|
||
)
|
||
|
||
|
||
def test_child_injector_inherits_parent_bindings():
|
||
parent, child = prepare_nested_injectors()
|
||
assert child.get(str) == parent.get(str)
|
||
|
||
|
||
def test_child_injector_overrides_parent_bindings():
|
||
parent, child = prepare_nested_injectors()
|
||
child.binder.bind(str, to='qwe')
|
||
|
||
assert (parent.get(str), child.get(str)) == ('asd', 'qwe')
|
||
|
||
|
||
def test_child_injector_rebinds_arguments_for_parent_scope():
|
||
class Cls:
|
||
val = ""
|
||
|
||
class A(Cls):
|
||
@inject
|
||
def __init__(self, val: str):
|
||
self.val = val
|
||
|
||
def configure_parent(binder):
|
||
binder.bind(Cls, to=A)
|
||
binder.bind(str, to="Parent")
|
||
|
||
def configure_child(binder):
|
||
binder.bind(str, to="Child")
|
||
|
||
parent = Injector(configure_parent)
|
||
assert parent.get(Cls).val == "Parent"
|
||
child = parent.create_child_injector(configure_child)
|
||
assert child.get(Cls).val == "Child"
|
||
|
||
|
||
def test_scopes_are_only_bound_to_root_injector():
|
||
parent, child = prepare_nested_injectors()
|
||
|
||
class A:
|
||
pass
|
||
|
||
parent.binder.bind(A, to=A, scope=singleton)
|
||
assert parent.get(A) is child.get(A)
|
||
|
||
|
||
def test_get_default_injected_instances():
|
||
def configure(binder):
|
||
binder.bind(DependsOnEmptyClass)
|
||
binder.bind(EmptyClass)
|
||
|
||
injector = Injector(configure)
|
||
assert injector.get(Injector) is injector
|
||
assert injector.get(Binder) is injector.binder
|
||
|
||
|
||
def test_instantiate_injected_method():
|
||
a = DependsOnEmptyClass('Bob')
|
||
assert a.b == 'Bob'
|
||
|
||
|
||
def test_method_decorator_is_wrapped():
|
||
assert DependsOnEmptyClass.__init__.__doc__ == 'Construct a new DependsOnEmptyClass.'
|
||
assert DependsOnEmptyClass.__init__.__name__ == '__init__'
|
||
|
||
|
||
def test_decorator_works_for_function_with_no_args():
|
||
@inject
|
||
def wrapped(*args, **kwargs):
|
||
pass
|
||
|
||
|
||
def test_providers_arent_called_for_dependencies_that_are_already_provided():
|
||
def configure(binder):
|
||
binder.bind(int, to=lambda: 1 / 0)
|
||
|
||
class A:
|
||
@inject
|
||
def __init__(self, i: int):
|
||
pass
|
||
|
||
injector = Injector(configure)
|
||
builder = injector.get(AssistedBuilder[A])
|
||
|
||
with pytest.raises(ZeroDivisionError):
|
||
builder.build()
|
||
|
||
builder.build(i=3)
|
||
|
||
|
||
def test_inject_direct():
|
||
def configure(binder):
|
||
binder.bind(DependsOnEmptyClass)
|
||
binder.bind(EmptyClass)
|
||
|
||
injector = Injector(configure)
|
||
a = injector.get(DependsOnEmptyClass)
|
||
assert isinstance(a, DependsOnEmptyClass)
|
||
assert isinstance(a.b, EmptyClass)
|
||
|
||
|
||
def test_configure_multiple_modules():
|
||
def configure_a(binder):
|
||
binder.bind(DependsOnEmptyClass)
|
||
|
||
def configure_b(binder):
|
||
binder.bind(EmptyClass)
|
||
|
||
injector = Injector([configure_a, configure_b])
|
||
a = injector.get(DependsOnEmptyClass)
|
||
assert isinstance(a, DependsOnEmptyClass)
|
||
assert isinstance(a.b, EmptyClass)
|
||
|
||
|
||
def test_inject_with_missing_dependency():
|
||
def configure(binder):
|
||
binder.bind(DependsOnEmptyClass)
|
||
|
||
injector = Injector(configure, auto_bind=False)
|
||
with pytest.raises(UnsatisfiedRequirement):
|
||
injector.get(EmptyClass)
|
||
|
||
|
||
def test_inject_named_interface():
|
||
class A:
|
||
@inject
|
||
def __init__(self, b: EmptyClass):
|
||
self.b = b
|
||
|
||
def configure(binder):
|
||
binder.bind(A)
|
||
binder.bind(EmptyClass)
|
||
|
||
injector = Injector(configure)
|
||
a = injector.get(A)
|
||
assert isinstance(a, A)
|
||
assert isinstance(a.b, EmptyClass)
|
||
|
||
|
||
class TransitiveC:
|
||
pass
|
||
|
||
|
||
class TransitiveB:
|
||
@inject
|
||
def __init__(self, c: TransitiveC):
|
||
self.c = c
|
||
|
||
|
||
class TransitiveA:
|
||
@inject
|
||
def __init__(self, b: TransitiveB):
|
||
self.b = b
|
||
|
||
|
||
def test_transitive_injection():
|
||
def configure(binder):
|
||
binder.bind(TransitiveA)
|
||
binder.bind(TransitiveB)
|
||
binder.bind(TransitiveC)
|
||
|
||
injector = Injector(configure)
|
||
a = injector.get(TransitiveA)
|
||
assert isinstance(a, TransitiveA)
|
||
assert isinstance(a.b, TransitiveB)
|
||
assert isinstance(a.b.c, TransitiveC)
|
||
|
||
|
||
def test_transitive_injection_with_missing_dependency():
|
||
def configure(binder):
|
||
binder.bind(TransitiveA)
|
||
binder.bind(TransitiveB)
|
||
|
||
injector = Injector(configure, auto_bind=False)
|
||
with pytest.raises(UnsatisfiedRequirement):
|
||
injector.get(TransitiveA)
|
||
with pytest.raises(UnsatisfiedRequirement):
|
||
injector.get(TransitiveB)
|
||
|
||
|
||
def test_inject_singleton():
|
||
class A:
|
||
@inject
|
||
def __init__(self, b: EmptyClass):
|
||
self.b = b
|
||
|
||
def configure(binder):
|
||
binder.bind(A)
|
||
binder.bind(EmptyClass, scope=SingletonScope)
|
||
|
||
injector1 = Injector(configure)
|
||
a1 = injector1.get(A)
|
||
a2 = injector1.get(A)
|
||
assert a1.b is a2.b
|
||
|
||
|
||
@singleton
|
||
class SingletonB:
|
||
pass
|
||
|
||
|
||
def test_inject_decorated_singleton_class():
|
||
class A:
|
||
@inject
|
||
def __init__(self, b: SingletonB):
|
||
self.b = b
|
||
|
||
def configure(binder):
|
||
binder.bind(A)
|
||
binder.bind(SingletonB)
|
||
|
||
injector1 = Injector(configure)
|
||
a1 = injector1.get(A)
|
||
a2 = injector1.get(A)
|
||
assert a1.b is a2.b
|
||
|
||
|
||
def test_threadlocal():
|
||
@threadlocal
|
||
class A:
|
||
def __init__(self):
|
||
pass
|
||
|
||
def configure(binder):
|
||
binder.bind(A)
|
||
|
||
injector = Injector(configure)
|
||
a1 = injector.get(A)
|
||
a2 = injector.get(A)
|
||
|
||
assert a1 is a2
|
||
|
||
a3 = [None]
|
||
ready = threading.Event()
|
||
|
||
def inject_a3():
|
||
a3[0] = injector.get(A)
|
||
ready.set()
|
||
|
||
threading.Thread(target=inject_a3).start()
|
||
ready.wait(1.0)
|
||
|
||
assert a2 is not a3[0] and a3[0] is not None
|
||
|
||
|
||
class Interface2:
|
||
pass
|
||
|
||
|
||
def test_injecting_interface_implementation():
|
||
class Implementation:
|
||
pass
|
||
|
||
class A:
|
||
@inject
|
||
def __init__(self, i: Interface2):
|
||
self.i = i
|
||
|
||
def configure(binder):
|
||
binder.bind(A)
|
||
binder.bind(Interface2, to=Implementation)
|
||
|
||
injector = Injector(configure)
|
||
a = injector.get(A)
|
||
assert isinstance(a.i, Implementation)
|
||
|
||
|
||
class CyclicInterface:
|
||
pass
|
||
|
||
|
||
class CyclicA:
|
||
@inject
|
||
def __init__(self, i: CyclicInterface):
|
||
self.i = i
|
||
|
||
|
||
class CyclicB:
|
||
@inject
|
||
def __init__(self, a: CyclicA):
|
||
self.a = a
|
||
|
||
|
||
def test_cyclic_dependencies():
|
||
def configure(binder):
|
||
binder.bind(CyclicInterface, to=CyclicB)
|
||
binder.bind(CyclicA)
|
||
|
||
injector = Injector(configure)
|
||
with pytest.raises(CircularDependency):
|
||
injector.get(CyclicA)
|
||
|
||
|
||
class CyclicInterface2:
|
||
pass
|
||
|
||
|
||
class CyclicA2:
|
||
@inject
|
||
def __init__(self, i: CyclicInterface2):
|
||
self.i = i
|
||
|
||
|
||
class CyclicB2:
|
||
@inject
|
||
def __init__(self, a_builder: AssistedBuilder[CyclicA2]):
|
||
self.a = a_builder.build(i=self)
|
||
|
||
|
||
def test_dependency_cycle_can_be_worked_broken_by_assisted_building():
|
||
def configure(binder):
|
||
binder.bind(CyclicInterface2, to=CyclicB2)
|
||
binder.bind(CyclicA2)
|
||
|
||
injector = Injector(configure)
|
||
|
||
# Previously it'd detect a circular dependency here:
|
||
# 1. Constructing CyclicA2 requires CyclicInterface2 (bound to CyclicB2)
|
||
# 2. Constructing CyclicB2 requires assisted build of CyclicA2
|
||
# 3. Constructing CyclicA2 triggers circular dependency check
|
||
assert isinstance(injector.get(CyclicA2), CyclicA2)
|
||
|
||
|
||
class Interface5:
|
||
constructed = False
|
||
|
||
def __init__(self):
|
||
Interface5.constructed = True
|
||
|
||
|
||
def test_that_injection_is_lazy():
|
||
class A:
|
||
@inject
|
||
def __init__(self, i: Interface5):
|
||
self.i = i
|
||
|
||
def configure(binder):
|
||
binder.bind(Interface5)
|
||
binder.bind(A)
|
||
|
||
injector = Injector(configure)
|
||
assert not (Interface5.constructed)
|
||
injector.get(A)
|
||
assert Interface5.constructed
|
||
|
||
|
||
def test_module_provider():
|
||
class MyModule(Module):
|
||
@provider
|
||
def provide_name(self) -> str:
|
||
return 'Bob'
|
||
|
||
module = MyModule()
|
||
injector = Injector(module)
|
||
assert injector.get(str) == 'Bob'
|
||
|
||
|
||
def test_module_class_gets_instantiated():
|
||
name = 'Meg'
|
||
|
||
class MyModule(Module):
|
||
def configure(self, binder):
|
||
binder.bind(str, to=name)
|
||
|
||
injector = Injector(MyModule)
|
||
assert injector.get(str) == name
|
||
|
||
|
||
def test_inject_and_provide_coexist_happily():
|
||
class MyModule(Module):
|
||
@provider
|
||
def provide_weight(self) -> float:
|
||
return 50.0
|
||
|
||
@provider
|
||
def provide_age(self) -> int:
|
||
return 25
|
||
|
||
# TODO(alec) Make provider/inject order independent.
|
||
@provider
|
||
@inject
|
||
def provide_description(self, age: int, weight: float) -> str:
|
||
return 'Bob is %d and weighs %0.1fkg' % (age, weight)
|
||
|
||
assert Injector(MyModule()).get(str) == 'Bob is 25 and weighs 50.0kg'
|
||
|
||
|
||
Names = NewType('Names', List[str])
|
||
Passwords = NewType('Ages', Dict[str, str])
|
||
|
||
|
||
def test_multibind():
|
||
# First let's have some explicit multibindings
|
||
def configure(binder):
|
||
binder.multibind(List[str], to=['not a name'])
|
||
binder.multibind(Dict[str, str], to={'asd': 'qwe'})
|
||
# To make sure Lists and Dicts of different subtypes are treated distinctly
|
||
binder.multibind(List[int], to=[1, 2, 3])
|
||
binder.multibind(Dict[str, int], to={'weight': 12})
|
||
# To see that NewTypes are treated distinctly
|
||
binder.multibind(Names, to=['Bob'])
|
||
binder.multibind(Passwords, to={'Bob': 'password1'})
|
||
|
||
# Then @multiprovider-decorated Module methods
|
||
class CustomModule(Module):
|
||
@multiprovider
|
||
def provide_some_ints(self) -> List[int]:
|
||
return [4, 5, 6]
|
||
|
||
@multiprovider
|
||
def provide_some_strs(self) -> List[str]:
|
||
return ['not a name either']
|
||
|
||
@multiprovider
|
||
def provide_str_to_str_mapping(self) -> Dict[str, str]:
|
||
return {'xxx': 'yyy'}
|
||
|
||
@multiprovider
|
||
def provide_str_to_int_mapping(self) -> Dict[str, int]:
|
||
return {'height': 33}
|
||
|
||
@multiprovider
|
||
def provide_names(self) -> Names:
|
||
return ['Alice', 'Clarice']
|
||
|
||
@multiprovider
|
||
def provide_passwords(self) -> Passwords:
|
||
return {'Alice': 'aojrioeg3', 'Clarice': 'clarice30'}
|
||
|
||
injector = Injector([configure, CustomModule])
|
||
assert injector.get(List[str]) == ['not a name', 'not a name either']
|
||
assert injector.get(List[int]) == [1, 2, 3, 4, 5, 6]
|
||
assert injector.get(Dict[str, str]) == {'asd': 'qwe', 'xxx': 'yyy'}
|
||
assert injector.get(Dict[str, int]) == {'weight': 12, 'height': 33}
|
||
assert injector.get(Names) == ['Bob', 'Alice', 'Clarice']
|
||
assert injector.get(Passwords) == {'Bob': 'password1', 'Alice': 'aojrioeg3', 'Clarice': 'clarice30'}
|
||
|
||
|
||
def test_regular_bind_and_provider_dont_work_with_multibind():
|
||
# We only want multibind and multiprovider to work to avoid confusion
|
||
|
||
Names = NewType('Names', List[str])
|
||
Passwords = NewType('Passwords', Dict[str, str])
|
||
|
||
class MyModule(Module):
|
||
with pytest.raises(Error):
|
||
|
||
@provider
|
||
def provide_strs(self) -> List[str]:
|
||
return []
|
||
|
||
with pytest.raises(Error):
|
||
|
||
@provider
|
||
def provide_names(self) -> Names:
|
||
return []
|
||
|
||
with pytest.raises(Error):
|
||
|
||
@provider
|
||
def provide_strs_in_dict(self) -> Dict[str, str]:
|
||
return {}
|
||
|
||
with pytest.raises(Error):
|
||
|
||
@provider
|
||
def provide_passwords(self) -> Passwords:
|
||
return {}
|
||
|
||
injector = Injector()
|
||
binder = injector.binder
|
||
|
||
with pytest.raises(Error):
|
||
binder.bind(List[str], to=[])
|
||
|
||
with pytest.raises(Error):
|
||
binder.bind(Names, to=[])
|
||
|
||
with pytest.raises(Error):
|
||
binder.bind(Dict[str, str], to={})
|
||
|
||
with pytest.raises(Error):
|
||
binder.bind(Passwords, to={})
|
||
|
||
|
||
def test_auto_bind():
|
||
class A:
|
||
pass
|
||
|
||
injector = Injector()
|
||
assert isinstance(injector.get(A), A)
|
||
|
||
|
||
def test_auto_bind_with_newtype():
|
||
# Reported in https://github.com/alecthomas/injector/issues/117
|
||
class A:
|
||
pass
|
||
|
||
AliasOfA = NewType('AliasOfA', A)
|
||
injector = Injector()
|
||
assert isinstance(injector.get(AliasOfA), A)
|
||
|
||
|
||
class Request:
|
||
pass
|
||
|
||
|
||
class RequestScope(Scope):
|
||
def configure(self):
|
||
self.context = None
|
||
|
||
@contextmanager
|
||
def __call__(self, request):
|
||
assert self.context is None
|
||
self.context = {}
|
||
binder = self.injector.get(Binder)
|
||
binder.bind(Request, to=request, scope=RequestScope)
|
||
yield
|
||
self.context = None
|
||
|
||
def get(self, key, provider):
|
||
if self.context is None:
|
||
raise UnsatisfiedRequirement(None, key)
|
||
try:
|
||
return self.context[key]
|
||
except KeyError:
|
||
provider = InstanceProvider(provider.get(self.injector))
|
||
self.context[key] = provider
|
||
return provider
|
||
|
||
|
||
request = ScopeDecorator(RequestScope)
|
||
|
||
|
||
@request
|
||
class Handler:
|
||
def __init__(self, request):
|
||
self.request = request
|
||
|
||
|
||
class RequestModule(Module):
|
||
@provider
|
||
@inject
|
||
def handler(self, request: Request) -> Handler:
|
||
return Handler(request)
|
||
|
||
|
||
def test_custom_scope():
|
||
|
||
injector = Injector([RequestModule()], auto_bind=False)
|
||
|
||
with pytest.raises(UnsatisfiedRequirement):
|
||
injector.get(Handler)
|
||
|
||
scope = injector.get(RequestScope)
|
||
request = Request()
|
||
|
||
with scope(request):
|
||
handler = injector.get(Handler)
|
||
assert handler.request is request
|
||
|
||
with pytest.raises(UnsatisfiedRequirement):
|
||
injector.get(Handler)
|
||
|
||
|
||
def test_binder_install():
|
||
class ModuleA(Module):
|
||
def configure(self, binder):
|
||
binder.bind(str, to='hello world')
|
||
|
||
class ModuleB(Module):
|
||
def configure(self, binder):
|
||
binder.install(ModuleA())
|
||
|
||
injector = Injector([ModuleB()])
|
||
assert injector.get(str) == 'hello world'
|
||
|
||
|
||
def test_binder_provider_for_method_with_explicit_provider():
|
||
injector = Injector()
|
||
binder = injector.binder
|
||
provider = binder.provider_for(int, to=InstanceProvider(1))
|
||
assert type(provider) is InstanceProvider
|
||
assert provider.get(injector) == 1
|
||
|
||
|
||
def test_binder_provider_for_method_with_instance():
|
||
injector = Injector()
|
||
binder = injector.binder
|
||
provider = binder.provider_for(int, to=1)
|
||
assert type(provider) is InstanceProvider
|
||
assert provider.get(injector) == 1
|
||
|
||
|
||
def test_binder_provider_for_method_with_class():
|
||
injector = Injector()
|
||
binder = injector.binder
|
||
provider = binder.provider_for(int)
|
||
assert type(provider) is ClassProvider
|
||
assert provider.get(injector) == 0
|
||
|
||
|
||
def test_binder_provider_for_method_with_class_to_specific_subclass():
|
||
class A:
|
||
pass
|
||
|
||
class B(A):
|
||
pass
|
||
|
||
injector = Injector()
|
||
binder = injector.binder
|
||
provider = binder.provider_for(A, B)
|
||
assert type(provider) is ClassProvider
|
||
assert isinstance(provider.get(injector), B)
|
||
|
||
|
||
def test_binder_provider_for_type_with_metaclass():
|
||
# use a metaclass cross python2/3 way
|
||
# otherwise should be:
|
||
# class A(object, metaclass=abc.ABCMeta):
|
||
# passa
|
||
A = abc.ABCMeta('A', (object,), {})
|
||
|
||
injector = Injector()
|
||
binder = injector.binder
|
||
assert isinstance(binder.provider_for(A, None).get(injector), A)
|
||
|
||
|
||
class ClassA:
|
||
def __init__(self, parameter):
|
||
pass
|
||
|
||
|
||
class ClassB:
|
||
@inject
|
||
def __init__(self, a: ClassA):
|
||
pass
|
||
|
||
|
||
def test_injecting_undecorated_class_with_missing_dependencies_raises_the_right_error():
|
||
injector = Injector()
|
||
try:
|
||
injector.get(ClassB)
|
||
except CallError as ce:
|
||
check_exception_contains_stuff(ce, ('ClassA.__init__', 'ClassB'))
|
||
|
||
|
||
def test_call_to_method_with_legitimate_call_error_raises_type_error():
|
||
class A:
|
||
def __init__(self):
|
||
max()
|
||
|
||
injector = Injector()
|
||
with pytest.raises(TypeError):
|
||
injector.get(A)
|
||
|
||
|
||
def test_call_error_str_representation_handles_single_arg():
|
||
ce = CallError('zxc')
|
||
assert str(ce) == 'zxc'
|
||
|
||
|
||
class NeedsAssistance:
|
||
@inject
|
||
def __init__(self, a: str, b):
|
||
self.a = a
|
||
self.b = b
|
||
|
||
|
||
def test_assisted_builder_works_when_got_directly_from_injector():
|
||
injector = Injector()
|
||
builder = injector.get(AssistedBuilder[NeedsAssistance])
|
||
obj = builder.build(b=123)
|
||
assert (obj.a, obj.b) == (str(), 123)
|
||
|
||
|
||
def test_assisted_builder_works_when_injected():
|
||
class X:
|
||
@inject
|
||
def __init__(self, builder: AssistedBuilder[NeedsAssistance]):
|
||
self.obj = builder.build(b=234)
|
||
|
||
injector = Injector()
|
||
x = injector.get(X)
|
||
assert (x.obj.a, x.obj.b) == (str(), 234)
|
||
|
||
|
||
class Interface:
|
||
b = 0
|
||
|
||
|
||
def test_assisted_builder_uses_bindings():
|
||
def configure(binder):
|
||
binder.bind(Interface, to=NeedsAssistance)
|
||
|
||
injector = Injector(configure)
|
||
builder = injector.get(AssistedBuilder[Interface])
|
||
x = builder.build(b=333)
|
||
assert (type(x), x.b) == (NeedsAssistance, 333)
|
||
|
||
|
||
def test_assisted_builder_uses_concrete_class_when_specified():
|
||
class X:
|
||
pass
|
||
|
||
def configure(binder):
|
||
# meant only to show that provider isn't called
|
||
binder.bind(X, to=lambda: 1 / 0)
|
||
|
||
injector = Injector(configure)
|
||
builder = injector.get(ClassAssistedBuilder[X])
|
||
builder.build()
|
||
|
||
|
||
def test_assisted_builder_injection_is_safe_to_use_with_multiple_injectors():
|
||
class X:
|
||
@inject
|
||
def __init__(self, builder: AssistedBuilder[NeedsAssistance]):
|
||
self.builder = builder
|
||
|
||
i1, i2 = Injector(), Injector()
|
||
b1 = i1.get(X).builder
|
||
b2 = i2.get(X).builder
|
||
assert (b1._injector, b2._injector) == (i1, i2)
|
||
|
||
|
||
def test_assisted_builder_injection_is_safe_to_use_with_child_injectors():
|
||
class X:
|
||
@inject
|
||
def __init__(self, builder: AssistedBuilder[NeedsAssistance]):
|
||
self.builder = builder
|
||
|
||
i1 = Injector()
|
||
i2 = i1.create_child_injector()
|
||
b1 = i1.get(X).builder
|
||
b2 = i2.get(X).builder
|
||
assert (b1._injector, b2._injector) == (i1, i2)
|
||
|
||
|
||
class TestThreadSafety:
|
||
def setup(self):
|
||
self.event = threading.Event()
|
||
|
||
def configure(binder):
|
||
binder.bind(str, to=lambda: self.event.wait() and 'this is str')
|
||
|
||
class XXX:
|
||
@inject
|
||
def __init__(self, s: str):
|
||
pass
|
||
|
||
self.injector = Injector(configure)
|
||
self.cls = XXX
|
||
|
||
def gather_results(self, count):
|
||
objects = []
|
||
lock = threading.Lock()
|
||
|
||
def target():
|
||
o = self.injector.get(self.cls)
|
||
with lock:
|
||
objects.append(o)
|
||
|
||
threads = [threading.Thread(target=target) for i in range(count)]
|
||
|
||
for t in threads:
|
||
t.start()
|
||
|
||
self.event.set()
|
||
|
||
for t in threads:
|
||
t.join()
|
||
|
||
return objects
|
||
|
||
def test_injection_is_thread_safe(self):
|
||
objects = self.gather_results(2)
|
||
assert len(objects) == 2
|
||
|
||
def test_singleton_scope_is_thread_safe(self):
|
||
self.injector.binder.bind(self.cls, scope=singleton)
|
||
a, b = self.gather_results(2)
|
||
assert a is b
|
||
|
||
|
||
def test_provider_and_scope_decorator_collaboration():
|
||
@provider
|
||
@singleton
|
||
def provider_singleton() -> int:
|
||
return 10
|
||
|
||
@singleton
|
||
@provider
|
||
def singleton_provider() -> int:
|
||
return 10
|
||
|
||
assert provider_singleton.__binding__.scope == SingletonScope
|
||
assert singleton_provider.__binding__.scope == SingletonScope
|
||
|
||
|
||
def test_injecting_into_method_of_object_that_is_falseish_works():
|
||
# regression test
|
||
|
||
class X(dict):
|
||
@inject
|
||
def __init__(self, s: str):
|
||
pass
|
||
|
||
injector = Injector()
|
||
injector.get(X)
|
||
|
||
|
||
Name = NewType("Name", str)
|
||
Message = NewType("Message", str)
|
||
|
||
|
||
def test_callable_provider_injection():
|
||
@inject
|
||
def create_message(name: Name):
|
||
return "Hello, " + name
|
||
|
||
def configure(binder):
|
||
binder.bind(Name, to="John")
|
||
binder.bind(Message, to=create_message)
|
||
|
||
injector = Injector([configure])
|
||
msg = injector.get(Message)
|
||
assert msg == "Hello, John"
|
||
|
||
|
||
def test_providerof():
|
||
counter = [0]
|
||
|
||
def provide_str():
|
||
counter[0] += 1
|
||
return 'content'
|
||
|
||
def configure(binder):
|
||
binder.bind(str, to=provide_str)
|
||
|
||
injector = Injector(configure)
|
||
|
||
assert counter[0] == 0
|
||
|
||
provider = injector.get(ProviderOf[str])
|
||
assert counter[0] == 0
|
||
|
||
assert provider.get() == 'content'
|
||
assert counter[0] == 1
|
||
|
||
assert provider.get() == injector.get(str)
|
||
assert counter[0] == 3
|
||
|
||
|
||
def test_providerof_cannot_be_bound():
|
||
def configure(binder):
|
||
binder.bind(ProviderOf[int], to=InstanceProvider(None))
|
||
|
||
with pytest.raises(Exception):
|
||
Injector(configure)
|
||
|
||
|
||
def test_providerof_is_safe_to_use_with_multiple_injectors():
|
||
def configure1(binder):
|
||
binder.bind(int, to=1)
|
||
|
||
def configure2(binder):
|
||
binder.bind(int, to=2)
|
||
|
||
injector1 = Injector(configure1)
|
||
injector2 = Injector(configure2)
|
||
|
||
provider_of = ProviderOf[int]
|
||
|
||
provider1 = injector1.get(provider_of)
|
||
provider2 = injector2.get(provider_of)
|
||
|
||
assert provider1.get() == 1
|
||
assert provider2.get() == 2
|
||
|
||
|
||
def test_special_interfaces_work_with_auto_bind_disabled():
|
||
class InjectMe:
|
||
pass
|
||
|
||
def configure(binder):
|
||
binder.bind(InjectMe, to=InstanceProvider(InjectMe()))
|
||
|
||
injector = Injector(configure, auto_bind=False)
|
||
|
||
# This line used to fail with:
|
||
# Traceback (most recent call last):
|
||
# File "/projects/injector/injector_test.py", line 1171,
|
||
# in test_auto_bind_disabled_regressions
|
||
# injector.get(ProviderOf(InjectMe))
|
||
# File "/projects/injector/injector.py", line 687, in get
|
||
# binding = self.binder.get_binding(None, key)
|
||
# File "/projects/injector/injector.py", line 459, in get_binding
|
||
# raise UnsatisfiedRequirement(cls, key)
|
||
# UnsatisfiedRequirement: unsatisfied requirement on
|
||
# <injector.ProviderOf object at 0x10ff01550>
|
||
injector.get(ProviderOf[InjectMe])
|
||
|
||
# This used to fail with an error similar to the ProviderOf one
|
||
injector.get(ClassAssistedBuilder[InjectMe])
|
||
|
||
|
||
def test_binding_an_instance_regression():
|
||
text = b'hello'.decode()
|
||
|
||
def configure(binder):
|
||
# Yes, this binding doesn't make sense strictly speaking but
|
||
# it's just a sample case.
|
||
binder.bind(bytes, to=text)
|
||
|
||
injector = Injector(configure)
|
||
# This used to return empty bytes instead of the expected string
|
||
assert injector.get(bytes) == text
|
||
|
||
|
||
class PartialB:
|
||
@inject
|
||
def __init__(self, a: EmptyClass, b: str):
|
||
self.a = a
|
||
self.b = b
|
||
|
||
|
||
def test_class_assisted_builder_of_partially_injected_class_old():
|
||
class C:
|
||
@inject
|
||
def __init__(self, a: EmptyClass, builder: ClassAssistedBuilder[PartialB]):
|
||
self.a = a
|
||
self.b = builder.build(b='C')
|
||
|
||
c = Injector().get(C)
|
||
assert isinstance(c, C)
|
||
assert isinstance(c.b, PartialB)
|
||
assert isinstance(c.b.a, EmptyClass)
|
||
|
||
|
||
class ImplicitA:
|
||
pass
|
||
|
||
|
||
class ImplicitB:
|
||
@inject
|
||
def __init__(self, a: ImplicitA):
|
||
self.a = a
|
||
|
||
|
||
class ImplicitC:
|
||
@inject
|
||
def __init__(self, b: ImplicitB):
|
||
self.b = b
|
||
|
||
|
||
def test_implicit_injection_for_python3():
|
||
injector = Injector()
|
||
c = injector.get(ImplicitC)
|
||
assert isinstance(c, ImplicitC)
|
||
assert isinstance(c.b, ImplicitB)
|
||
assert isinstance(c.b.a, ImplicitA)
|
||
|
||
|
||
def test_annotation_based_injection_works_in_provider_methods():
|
||
class MyModule(Module):
|
||
def configure(self, binder):
|
||
binder.bind(int, to=42)
|
||
|
||
@provider
|
||
def provide_str(self, i: int) -> str:
|
||
return str(i)
|
||
|
||
@singleton
|
||
@provider
|
||
def provide_object(self) -> object:
|
||
return object()
|
||
|
||
injector = Injector(MyModule)
|
||
assert injector.get(str) == '42'
|
||
assert injector.get(object) is injector.get(object)
|
||
|
||
|
||
class Fetcher:
|
||
def fetch(self, user_id):
|
||
assert user_id == 333
|
||
return {'name': 'John'}
|
||
|
||
|
||
class Processor:
|
||
@noninjectable('provider_id')
|
||
@inject
|
||
@noninjectable('user_id')
|
||
def __init__(self, fetcher: Fetcher, user_id: int, provider_id: str):
|
||
assert provider_id == 'not injected'
|
||
data = fetcher.fetch(user_id)
|
||
self.name = data['name']
|
||
|
||
|
||
def test_assisted_building_is_supported():
|
||
def configure(binder):
|
||
binder.bind(int, to=897)
|
||
binder.bind(str, to='injected')
|
||
|
||
injector = Injector(configure)
|
||
processor_builder = injector.get(AssistedBuilder[Processor])
|
||
|
||
with pytest.raises(CallError):
|
||
processor_builder.build()
|
||
|
||
processor = processor_builder.build(user_id=333, provider_id='not injected')
|
||
assert processor.name == 'John'
|
||
|
||
|
||
def test_raises_when_noninjectable_arguments_defined_with_invalid_arguments():
|
||
with pytest.raises(UnknownArgument):
|
||
|
||
class A:
|
||
@inject
|
||
@noninjectable('c')
|
||
def __init__(self, b: str):
|
||
self.b = b
|
||
|
||
|
||
def test_can_create_instance_with_untyped_noninjectable_argument():
|
||
class Parent:
|
||
@inject
|
||
@noninjectable('child1', 'child2')
|
||
def __init__(self, child1, *, child2):
|
||
self.child1 = child1
|
||
self.child2 = child2
|
||
|
||
injector = Injector()
|
||
parent_builder = injector.get(AssistedBuilder[Parent])
|
||
parent = parent_builder.build(child1='injected1', child2='injected2')
|
||
|
||
assert parent.child1 == 'injected1'
|
||
assert parent.child2 == 'injected2'
|
||
|
||
|
||
def test_implicit_injection_fails_when_annotations_are_missing():
|
||
class A:
|
||
def __init__(self, n):
|
||
self.n = n
|
||
|
||
injector = Injector()
|
||
with pytest.raises(CallError):
|
||
injector.get(A)
|
||
|
||
|
||
def test_injection_works_in_presence_of_return_value_annotation():
|
||
# Code with PEP 484-compatible type hints will have __init__ methods
|
||
# annotated as returning None[1] and this didn't work well with Injector.
|
||
#
|
||
# [1] https://www.python.org/dev/peps/pep-0484/#the-meaning-of-annotations
|
||
|
||
class A:
|
||
@inject
|
||
def __init__(self, s: str) -> None:
|
||
self.s = s
|
||
|
||
def configure(binder):
|
||
binder.bind(str, to='this is string')
|
||
|
||
injector = Injector([configure])
|
||
|
||
# Used to fail with:
|
||
# injector.UnknownProvider: couldn't determine provider for None to None
|
||
a = injector.get(A)
|
||
|
||
# Just a sanity check, if the code above worked we're almost certain
|
||
# we're good but just in case the return value annotation handling changed
|
||
# something:
|
||
assert a.s == 'this is string'
|
||
|
||
|
||
def test_things_dont_break_in_presence_of_args_or_kwargs():
|
||
class A:
|
||
@inject
|
||
def __init__(self, s: str, *args: int, **kwargs: str):
|
||
assert not args
|
||
assert not kwargs
|
||
|
||
injector = Injector()
|
||
|
||
# The following line used to fail with something like this:
|
||
# Traceback (most recent call last):
|
||
# File "/ve/injector/injector_test_py3.py", line 192,
|
||
# in test_things_dont_break_in_presence_of_args_or_kwargs
|
||
# injector.get(A)
|
||
# File "/ve/injector/injector.py", line 707, in get
|
||
# result = scope_instance.get(key, binding.provider).get(self)
|
||
# File "/ve/injector/injector.py", line 142, in get
|
||
# return injector.create_object(self._cls)
|
||
# File "/ve/injector/injector.py", line 744, in create_object
|
||
# init(instance, **additional_kwargs)
|
||
# File "/ve/injector/injector.py", line 1082, in inject
|
||
# kwargs=kwargs
|
||
# File "/ve/injector/injector.py", line 851, in call_with_injection
|
||
# **dependencies)
|
||
# File "/ve/injector/injector_test_py3.py", line 189, in __init__
|
||
# assert not kwargs
|
||
# AssertionError: assert not {'args': 0, 'kwargs': ''}
|
||
injector.get(A)
|
||
|
||
|
||
def test_forward_references_in_annotations_are_handled():
|
||
# See https://www.python.org/dev/peps/pep-0484/#forward-references for details
|
||
|
||
class CustomModule(Module):
|
||
@provider
|
||
def provide_x(self) -> 'X':
|
||
return X('hello')
|
||
|
||
@inject
|
||
def fun(s: 'X') -> 'X':
|
||
return s
|
||
|
||
# The class needs to be module-global in order for the string -> object
|
||
# resolution mechanism to work. I could make it work with locals but it
|
||
# doesn't seem worth it.
|
||
global X
|
||
|
||
class X:
|
||
def __init__(self, message: str) -> None:
|
||
self.message = message
|
||
|
||
try:
|
||
injector = Injector(CustomModule)
|
||
assert injector.call_with_injection(fun).message == 'hello'
|
||
finally:
|
||
del X
|
||
|
||
|
||
def test_more_useful_exception_is_raised_when_parameters_type_is_any():
|
||
@inject
|
||
def fun(a: Any) -> None:
|
||
pass
|
||
|
||
injector = Injector()
|
||
|
||
# This was the exception before:
|
||
#
|
||
# TypeError: Cannot instantiate <class 'typing.AnyMeta'>
|
||
#
|
||
# Now:
|
||
#
|
||
# injector.CallError: Call to AnyMeta.__new__() failed: Cannot instantiate
|
||
# <class 'typing.AnyMeta'> (injection stack: ['injector_test_py3'])
|
||
#
|
||
# In this case the injection stack doesn't provide too much information but
|
||
# it quickly gets helpful when the stack gets deeper.
|
||
with pytest.raises((CallError, TypeError)):
|
||
injector.call_with_injection(fun)
|
||
|
||
|
||
def test_optionals_are_ignored_for_now():
|
||
@inject
|
||
def fun(s: str = None):
|
||
return s
|
||
|
||
assert Injector().call_with_injection(fun) == ''
|
||
|
||
|
||
def test_explicitly_passed_parameters_override_injectable_values():
|
||
# The class needs to be defined globally for the 'X' forward reference to be able to be resolved.
|
||
global X
|
||
|
||
# We test a method on top of regular function to exercise the code path that's
|
||
# responsible for handling methods.
|
||
class X:
|
||
@inject
|
||
def method(self, s: str) -> str:
|
||
return s
|
||
|
||
@inject
|
||
def method_typed_self(self: 'X', s: str) -> str:
|
||
return s
|
||
|
||
@inject
|
||
def function(s: str) -> str:
|
||
return s
|
||
|
||
injection_counter = 0
|
||
|
||
def provide_str() -> str:
|
||
nonlocal injection_counter
|
||
injection_counter += 1
|
||
return 'injected string'
|
||
|
||
def configure(binder: Binder) -> None:
|
||
binder.bind(str, to=provide_str)
|
||
|
||
injector = Injector([configure])
|
||
x = X()
|
||
|
||
try:
|
||
assert injection_counter == 0
|
||
|
||
assert injector.call_with_injection(x.method) == 'injected string'
|
||
assert injection_counter == 1
|
||
assert injector.call_with_injection(x.method_typed_self) == 'injected string'
|
||
assert injection_counter == 2
|
||
assert injector.call_with_injection(function) == 'injected string'
|
||
assert injection_counter == 3
|
||
|
||
assert injector.call_with_injection(x.method, args=('passed string',)) == 'passed string'
|
||
assert injection_counter == 3
|
||
assert injector.call_with_injection(x.method_typed_self, args=('passed string',)) == 'passed string'
|
||
assert injection_counter == 3
|
||
assert injector.call_with_injection(function, args=('passed string',)) == 'passed string'
|
||
assert injection_counter == 3
|
||
|
||
assert injector.call_with_injection(x.method, kwargs={'s': 'passed string'}) == 'passed string'
|
||
assert injection_counter == 3
|
||
assert (
|
||
injector.call_with_injection(x.method_typed_self, kwargs={'s': 'passed string'})
|
||
== 'passed string'
|
||
)
|
||
assert injection_counter == 3
|
||
assert injector.call_with_injection(function, kwargs={'s': 'passed string'}) == 'passed string'
|
||
assert injection_counter == 3
|
||
finally:
|
||
del X
|
||
|
||
|
||
class AssistedB:
|
||
@inject
|
||
def __init__(self, a: EmptyClass, b: str):
|
||
self.a = a
|
||
self.b = b
|
||
|
||
|
||
def test_class_assisted_builder_of_partially_injected_class():
|
||
class C:
|
||
@inject
|
||
def __init__(self, a: EmptyClass, builder: ClassAssistedBuilder[AssistedB]):
|
||
self.a = a
|
||
self.b = builder.build(b='C')
|
||
|
||
c = Injector().get(C)
|
||
assert isinstance(c, C)
|
||
assert isinstance(c.b, AssistedB)
|
||
assert isinstance(c.b.a, EmptyClass)
|
||
|
||
|
||
# The test taken from Alec Thomas' pull request: https://github.com/alecthomas/injector/pull/73
|
||
def test_child_scope():
|
||
TestKey = NewType('TestKey', str)
|
||
TestKey2 = NewType('TestKey2', str)
|
||
|
||
def parent_module(binder):
|
||
binder.bind(TestKey, to='in parent', scope=singleton)
|
||
|
||
def first_child_module(binder):
|
||
binder.bind(TestKey2, to='in first child', scope=singleton)
|
||
|
||
def second_child_module(binder):
|
||
binder.bind(TestKey2, to='in second child', scope=singleton)
|
||
|
||
injector = Injector(modules=[parent_module])
|
||
first_child_injector = injector.create_child_injector(modules=[first_child_module])
|
||
second_child_injector = injector.create_child_injector(modules=[second_child_module])
|
||
|
||
assert first_child_injector.get(TestKey) is first_child_injector.get(TestKey)
|
||
assert first_child_injector.get(TestKey) is second_child_injector.get(TestKey)
|
||
assert first_child_injector.get(TestKey2) is not second_child_injector.get(TestKey2)
|
||
|
||
|
||
def test_custom_scopes_work_as_expected_with_child_injectors():
|
||
class CustomSingletonScope(SingletonScope):
|
||
pass
|
||
|
||
custom_singleton = ScopeDecorator(CustomSingletonScope)
|
||
|
||
def parent_module(binder):
|
||
binder.bind(str, to='parent value', scope=custom_singleton)
|
||
|
||
def child_module(binder):
|
||
binder.bind(str, to='child value', scope=custom_singleton)
|
||
|
||
parent = Injector(modules=[parent_module])
|
||
child = parent.create_child_injector(modules=[child_module])
|
||
print('parent, child: %s, %s' % (parent, child))
|
||
assert parent.get(str) == 'parent value'
|
||
assert child.get(str) == 'child value'
|
||
|
||
|
||
# Test for https://github.com/alecthomas/injector/issues/75
|
||
def test_inject_decorator_does_not_break_manual_construction_of_pyqt_objects():
|
||
class PyQtFake:
|
||
@inject
|
||
def __init__(self):
|
||
pass
|
||
|
||
def __getattribute__(self, item):
|
||
if item == '__injector__':
|
||
raise RuntimeError(
|
||
'A PyQt class would raise this exception if getting '
|
||
'self.__injector__ before __init__ is called and '
|
||
'self.__injector__ has not been set by Injector.'
|
||
)
|
||
return object.__getattribute__(self, item)
|
||
|
||
instance = PyQtFake() # This used to raise the exception
|
||
|
||
assert isinstance(instance, PyQtFake)
|
||
|
||
|
||
def test_using_an_assisted_builder_with_a_provider_raises_an_injector_error():
|
||
class MyModule(Module):
|
||
@provider
|
||
def provide_a(self, builder: AssistedBuilder[EmptyClass]) -> EmptyClass:
|
||
return builder.build()
|
||
|
||
injector = Injector(MyModule)
|
||
|
||
with pytest.raises(Error):
|
||
injector.get(EmptyClass)
|
||
|
||
|
||
def test_newtype_integration_works():
|
||
UserID = NewType('UserID', int)
|
||
|
||
def configure(binder):
|
||
binder.bind(UserID, to=123)
|
||
|
||
injector = Injector([configure])
|
||
assert injector.get(UserID) == 123
|
||
|
||
|
||
@pytest.mark.skipif(sys.version_info < (3, 6), reason="Requires Python 3.6+")
|
||
def test_dataclass_integration_works():
|
||
import dataclasses
|
||
|
||
# Python 3.6+-only syntax below
|
||
exec(
|
||
"""
|
||
@inject
|
||
@dataclasses.dataclass
|
||
class Data:
|
||
name: str
|
||
""",
|
||
locals(),
|
||
globals(),
|
||
)
|
||
|
||
def configure(binder):
|
||
binder.bind(str, to='data')
|
||
|
||
injector = Injector([configure])
|
||
assert injector.get(Data).name == 'data'
|
||
|
||
|
||
def test_get_bindings():
|
||
def function1(a: int) -> None:
|
||
pass
|
||
|
||
assert get_bindings(function1) == {}
|
||
|
||
@inject
|
||
def function2(a: int) -> None:
|
||
pass
|
||
|
||
assert get_bindings(function2) == {'a': int}
|
||
|
||
@inject
|
||
@noninjectable('b')
|
||
def function3(a: int, b: str) -> None:
|
||
pass
|
||
|
||
assert get_bindings(function3) == {'a': int}
|
||
|
||
# Let's verify that the inject/noninjectable ordering doesn't matter
|
||
@noninjectable('b')
|
||
@inject
|
||
def function3b(a: int, b: str) -> None:
|
||
pass
|
||
|
||
assert get_bindings(function3b) == {'a': int}
|
||
|
||
# The simple case of no @inject but injection requested with Inject[...]
|
||
def function4(a: Inject[int], b: str) -> None:
|
||
pass
|
||
|
||
assert get_bindings(function4) == {'a': int}
|
||
|
||
# Using @inject with Inject is redundant but it should not break anything
|
||
@inject
|
||
def function5(a: Inject[int], b: str) -> None:
|
||
pass
|
||
|
||
assert get_bindings(function5) == {'a': int, 'b': str}
|
||
|
||
# We need to be able to exclude a parameter from injection with NoInject
|
||
@inject
|
||
def function6(a: int, b: NoInject[str]) -> None:
|
||
pass
|
||
|
||
assert get_bindings(function6) == {'a': int}
|
||
|
||
# The presence of NoInject should not trigger anything on its own
|
||
def function7(a: int, b: NoInject[str]) -> None:
|
||
pass
|
||
|
||
assert get_bindings(function7) == {}
|
||
|
||
# There was a bug where in case of multiple NoInject-decorated parameters only the first one was
|
||
# actually made noninjectable and we tried to inject something we couldn't possibly provide
|
||
# into the second one.
|
||
@inject
|
||
def function8(a: NoInject[int], b: NoInject[int]) -> None:
|
||
pass
|
||
|
||
assert get_bindings(function8) == {}
|
||
|
||
# Default arguments to NoInject annotations should behave the same as noninjectable decorator w.r.t 'None'
|
||
@inject
|
||
@noninjectable('b')
|
||
def function9(self, a: int, b: Optional[str] = None):
|
||
pass
|
||
|
||
@inject
|
||
def function10(self, a: int, b: NoInject[Optional[str]] = None):
|
||
# b:s type is Union[NoInject[Union[str, None]], None]
|
||
pass
|
||
|
||
assert get_bindings(function9) == {'a': int} == get_bindings(function10)
|
||
|
||
# If there's a return type annottion that contains an a forward reference that can't be
|
||
# resolved (for whatever reason) we don't want that to break things for us – return types
|
||
# don't matter for the purpose of dependency injection.
|
||
@inject
|
||
def function11(a: int) -> 'InvalidForwardReference':
|
||
pass
|
||
|
||
assert get_bindings(function11) == {'a': int}
|
||
|
||
|
||
# Tests https://github.com/alecthomas/injector/issues/202
|
||
@pytest.mark.skipif(sys.version_info < (3, 10), reason="Requires Python 3.10+")
|
||
def test_get_bindings_for_pep_604():
|
||
@inject
|
||
def function1(a: int | None) -> None:
|
||
pass
|
||
|
||
assert get_bindings(function1) == {'a': int}
|
||
|
||
@inject
|
||
def function1(a: int | str) -> None:
|
||
pass
|
||
|
||
assert get_bindings(function1) == {'a': Union[int, str]}
|