Skip to content

Optionally allow overriding of dependencies #90

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -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.

Expand Down
23 changes: 21 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.

Expand Down
29 changes: 19 additions & 10 deletions src/inject/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down Expand Up @@ -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):
Expand All @@ -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:
Expand Down Expand Up @@ -358,33 +361,39 @@ 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

with _INJECTOR_LOCK:
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:
Expand Down
18 changes: 10 additions & 8 deletions test/test_binder.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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)
self.assertRaisesRegex(InjectorException, "Constructor cannot be None", binder.bind_to_constructor, int, None)
22 changes: 14 additions & 8 deletions test/test_inject_configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@


class TestInjectConfiguration(BaseTestInject):

def test_configure__should_create_injector(self):
injector0 = inject.configure()
injector1 = inject.get_injector()
Expand All @@ -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()
Expand Down Expand Up @@ -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