From e62eb134e61faf0a23b43ed2ddebb88244701c46 Mon Sep 17 00:00:00 2001 From: Nikolay Tsvetanov Date: Mon, 16 Oct 2023 15:15:13 +0300 Subject: [PATCH] allow overriding dependencies --- CHANGES.md | 3 +++ README.md | 23 +++++++++++++++++++++-- src/inject/__init__.py | 29 +++++++++++++++++++---------- test/test_binder.py | 18 ++++++++++-------- test/test_inject_configuration.py | 22 ++++++++++++++-------- 5 files changed, 67 insertions(+), 28 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 6056782..f2c5f50 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,9 @@ python-inject changes ===================== +### 5.0.1 (2023-10-16) +- Optionally allow overriding dependencies. + ### 5.0.0 (2023-06-10) - Support for PEP0604 for Python>=3.10. diff --git a/README.md b/README.md index 5e1ef10..e89e15f 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,6 @@ class User(object): # Create an optional configuration. def my_config(binder): - binder.install(my_config2) # Add bindings from another config. binder.bind(Cache, RedisCache('localhost:1234')) # Configure a shared injector. @@ -133,13 +132,33 @@ and optionally `inject.clear()` to clean up on tear down: class MyTest(unittest.TestCase): def setUp(self): inject.clear_and_configure(lambda binder: binder - .bind(Cache, Mock()) \ + .bind(Cache, MockCache()) \ .bind(Validator, TestValidator())) def tearDown(self): inject.clear() ``` +## Composable configurations +You can reuse configurations and override already registered dependencies to fit the needs in different environments or specific tests. +```python + def base_config(binder): + # ... more dependencies registered here + binder.bind(Validator, RealValidator()) + binder.bind(Cache, RedisCache('localhost:1234')) + + def tests_config(binder): + # reuse existing configuration + binder.install(base_config) + + # override only certain dependencies + binder.bind(Validator, TestValidator()) + binder.bind(Cache, MockCache()) + + inject.clear_and_configure(tests_config, allow_override=True) + +``` + ## Thread-safety After configuration the injector is thread-safe and can be safely reused by multiple threads. diff --git a/src/inject/__init__.py b/src/inject/__init__.py index 6210336..7100b1c 100644 --- a/src/inject/__init__.py +++ b/src/inject/__init__.py @@ -122,8 +122,9 @@ def __init__(self, constructor: Callable, previous_error: TypeError): class Binder(object): _bindings: Dict[Binding, Constructor] - def __init__(self) -> None: + def __init__(self, allow_override: bool = False) -> None: self._bindings = {} + self.allow_override = allow_override def install(self, config: BinderCallable) -> 'Binder': """Install another callable configuration.""" @@ -171,7 +172,7 @@ def _check_class(self, cls: Binding) -> None: if cls is None: raise InjectorException('Binding key cannot be None') - if cls in self._bindings: + if not self.allow_override and cls in self._bindings: raise InjectorException('Duplicate binding, key=%s' % cls) if self._is_forward_str(cls): @@ -197,10 +198,12 @@ def _is_forward_str(self, cls: Binding) -> bool: class Injector(object): _bindings: Dict[Binding, Constructor] - def __init__(self, config: Optional[BinderCallable] = None, bind_in_runtime: bool = True): + def __init__( + self, config: Optional[BinderCallable] = None, bind_in_runtime: bool = True, allow_override: bool = False + ): self._bind_in_runtime = bind_in_runtime if config: - binder = Binder() + binder = Binder(allow_override) config(binder) self._bindings = binder._bindings else: @@ -358,7 +361,9 @@ def injection_wrapper(*args: Any, **kwargs: Any) -> T: return injection_wrapper -def configure(config: Optional[BinderCallable] = None, bind_in_runtime: bool = True) -> Injector: +def configure( + config: Optional[BinderCallable] = None, bind_in_runtime: bool = True, allow_override: bool = False +) -> Injector: """Create an injector with a callable config or raise an exception when already configured.""" global _INJECTOR @@ -366,25 +371,29 @@ def configure(config: Optional[BinderCallable] = None, bind_in_runtime: bool = T if _INJECTOR: raise InjectorException('Injector is already configured') - _INJECTOR = Injector(config, bind_in_runtime=bind_in_runtime) + _INJECTOR = Injector(config, bind_in_runtime=bind_in_runtime, allow_override=allow_override) logger.debug('Created and configured an injector, config=%s', config) return _INJECTOR -def configure_once(config: Optional[BinderCallable] = None, bind_in_runtime: bool = True) -> Injector: +def configure_once( + config: Optional[BinderCallable] = None, bind_in_runtime: bool = True, allow_override: bool = False +) -> Injector: """Create an injector with a callable config if not present, otherwise, do nothing.""" with _INJECTOR_LOCK: if _INJECTOR: return _INJECTOR - return configure(config, bind_in_runtime=bind_in_runtime) + return configure(config, bind_in_runtime=bind_in_runtime, allow_override=allow_override) -def clear_and_configure(config: Optional[BinderCallable] = None, bind_in_runtime: bool = True) -> Injector: +def clear_and_configure( + config: Optional[BinderCallable] = None, bind_in_runtime: bool = True, allow_override: bool = False +) -> Injector: """Clear an existing injector and create another one with a callable config.""" with _INJECTOR_LOCK: clear() - return configure(config, bind_in_runtime=bind_in_runtime) + return configure(config, bind_in_runtime=bind_in_runtime, allow_override=allow_override) def is_configured() -> bool: diff --git a/test/test_binder.py b/test/test_binder.py index 7194a5c..9decd2a 100644 --- a/test/test_binder.py +++ b/test/test_binder.py @@ -13,15 +13,19 @@ def test_bind(self): def test_bind__class_required(self): binder = Binder() - self.assertRaisesRegex(InjectorException, 'Binding key cannot be None', - binder.bind, None, None) + self.assertRaisesRegex(InjectorException, 'Binding key cannot be None', binder.bind, None, None) def test_bind__duplicate_binding(self): binder = Binder() binder.bind(int, 123) - self.assertRaisesRegex(InjectorException, "Duplicate binding", - binder.bind, int, 456) + self.assertRaisesRegex(InjectorException, "Duplicate binding", binder.bind, int, 456) + + def test_bind__allow_override(self): + binder = Binder(allow_override=True) + binder.bind(int, 123) + binder.bind(int, 456) + assert int in binder._bindings def test_bind_provider(self): provider = lambda: 123 @@ -32,8 +36,7 @@ def test_bind_provider(self): def test_bind_provider__provider_required(self): binder = Binder() - self.assertRaisesRegex(InjectorException, "Provider cannot be None", - binder.bind_to_provider, int, None) + self.assertRaisesRegex(InjectorException, "Provider cannot be None", binder.bind_to_provider, int, None) def test_bind_constructor(self): constructor = lambda: 123 @@ -44,5 +47,4 @@ def test_bind_constructor(self): def test_bind_constructor__constructor_required(self): binder = Binder() - self.assertRaisesRegex(InjectorException, "Constructor cannot be None", - binder.bind_to_constructor, int, None) \ No newline at end of file + self.assertRaisesRegex(InjectorException, "Constructor cannot be None", binder.bind_to_constructor, int, None) diff --git a/test/test_inject_configuration.py b/test/test_inject_configuration.py index a36b5b8..447b8d5 100644 --- a/test/test_inject_configuration.py +++ b/test/test_inject_configuration.py @@ -4,7 +4,6 @@ class TestInjectConfiguration(BaseTestInject): - def test_configure__should_create_injector(self): injector0 = inject.configure() injector1 = inject.get_injector() @@ -19,8 +18,7 @@ def test_configure__should_add_bindings(self): def test_configure__already_configured(self): inject.configure() - self.assertRaisesRegex(InjectorException, 'Injector is already configured', - inject.configure) + self.assertRaisesRegex(InjectorException, 'Injector is already configured', inject.configure) def test_configure_once__should_create_injector(self): injector = inject.configure_once() @@ -48,11 +46,19 @@ def test_clear_and_configure(self): assert injector1 is not injector0 def test_get_injector_or_die(self): - self.assertRaisesRegex(InjectorException, 'No injector is configured', - inject.get_injector_or_die) + self.assertRaisesRegex(InjectorException, 'No injector is configured', inject.get_injector_or_die) def test_configure__runtime_binding_disabled(self): injector = inject.configure(bind_in_runtime=False) - self.assertRaisesRegex(InjectorException, - "No binding was found for key=<.* 'int'>", - injector.get_instance, int) + self.assertRaisesRegex(InjectorException, "No binding was found for key=<.* 'int'>", injector.get_instance, int) + + def test_configure__install_allow_override(self): + def base_config(binder): + binder.bind(int, 123) + + def config(binder): + binder.install(base_config) + binder.bind(int, 456) + + injector = inject.configure(config, allow_override=True) + assert injector.get_instance(int) == 456