Skip to content

Commit 3072dc7

Browse files
authored
Merge pull request #90 from NikiTsv/Override-deps-for-composable-configs
Optionally allow overriding of dependencies
2 parents f01c3ce + e62eb13 commit 3072dc7

File tree

5 files changed

+67
-28
lines changed

5 files changed

+67
-28
lines changed

CHANGES.md

+3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
python-inject changes
22
=====================
33

4+
### 5.0.1 (2023-10-16)
5+
- Optionally allow overriding dependencies.
6+
47
### 5.0.0 (2023-06-10)
58
- Support for PEP0604 for Python>=3.10.
69

README.md

+21-2
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,6 @@ class User(object):
100100

101101
# Create an optional configuration.
102102
def my_config(binder):
103-
binder.install(my_config2) # Add bindings from another config.
104103
binder.bind(Cache, RedisCache('localhost:1234'))
105104

106105
# Configure a shared injector.
@@ -133,13 +132,33 @@ and optionally `inject.clear()` to clean up on tear down:
133132
class MyTest(unittest.TestCase):
134133
def setUp(self):
135134
inject.clear_and_configure(lambda binder: binder
136-
.bind(Cache, Mock()) \
135+
.bind(Cache, MockCache()) \
137136
.bind(Validator, TestValidator()))
138137

139138
def tearDown(self):
140139
inject.clear()
141140
```
142141

142+
## Composable configurations
143+
You can reuse configurations and override already registered dependencies to fit the needs in different environments or specific tests.
144+
```python
145+
def base_config(binder):
146+
# ... more dependencies registered here
147+
binder.bind(Validator, RealValidator())
148+
binder.bind(Cache, RedisCache('localhost:1234'))
149+
150+
def tests_config(binder):
151+
# reuse existing configuration
152+
binder.install(base_config)
153+
154+
# override only certain dependencies
155+
binder.bind(Validator, TestValidator())
156+
binder.bind(Cache, MockCache())
157+
158+
inject.clear_and_configure(tests_config, allow_override=True)
159+
160+
```
161+
143162
## Thread-safety
144163
After configuration the injector is thread-safe and can be safely reused by multiple threads.
145164

src/inject/__init__.py

+19-10
Original file line numberDiff line numberDiff line change
@@ -122,8 +122,9 @@ def __init__(self, constructor: Callable, previous_error: TypeError):
122122
class Binder(object):
123123
_bindings: Dict[Binding, Constructor]
124124

125-
def __init__(self) -> None:
125+
def __init__(self, allow_override: bool = False) -> None:
126126
self._bindings = {}
127+
self.allow_override = allow_override
127128

128129
def install(self, config: BinderCallable) -> 'Binder':
129130
"""Install another callable configuration."""
@@ -171,7 +172,7 @@ def _check_class(self, cls: Binding) -> None:
171172
if cls is None:
172173
raise InjectorException('Binding key cannot be None')
173174

174-
if cls in self._bindings:
175+
if not self.allow_override and cls in self._bindings:
175176
raise InjectorException('Duplicate binding, key=%s' % cls)
176177

177178
if self._is_forward_str(cls):
@@ -197,10 +198,12 @@ def _is_forward_str(self, cls: Binding) -> bool:
197198
class Injector(object):
198199
_bindings: Dict[Binding, Constructor]
199200

200-
def __init__(self, config: Optional[BinderCallable] = None, bind_in_runtime: bool = True):
201+
def __init__(
202+
self, config: Optional[BinderCallable] = None, bind_in_runtime: bool = True, allow_override: bool = False
203+
):
201204
self._bind_in_runtime = bind_in_runtime
202205
if config:
203-
binder = Binder()
206+
binder = Binder(allow_override)
204207
config(binder)
205208
self._bindings = binder._bindings
206209
else:
@@ -358,33 +361,39 @@ def injection_wrapper(*args: Any, **kwargs: Any) -> T:
358361
return injection_wrapper
359362

360363

361-
def configure(config: Optional[BinderCallable] = None, bind_in_runtime: bool = True) -> Injector:
364+
def configure(
365+
config: Optional[BinderCallable] = None, bind_in_runtime: bool = True, allow_override: bool = False
366+
) -> Injector:
362367
"""Create an injector with a callable config or raise an exception when already configured."""
363368
global _INJECTOR
364369

365370
with _INJECTOR_LOCK:
366371
if _INJECTOR:
367372
raise InjectorException('Injector is already configured')
368373

369-
_INJECTOR = Injector(config, bind_in_runtime=bind_in_runtime)
374+
_INJECTOR = Injector(config, bind_in_runtime=bind_in_runtime, allow_override=allow_override)
370375
logger.debug('Created and configured an injector, config=%s', config)
371376
return _INJECTOR
372377

373378

374-
def configure_once(config: Optional[BinderCallable] = None, bind_in_runtime: bool = True) -> Injector:
379+
def configure_once(
380+
config: Optional[BinderCallable] = None, bind_in_runtime: bool = True, allow_override: bool = False
381+
) -> Injector:
375382
"""Create an injector with a callable config if not present, otherwise, do nothing."""
376383
with _INJECTOR_LOCK:
377384
if _INJECTOR:
378385
return _INJECTOR
379386

380-
return configure(config, bind_in_runtime=bind_in_runtime)
387+
return configure(config, bind_in_runtime=bind_in_runtime, allow_override=allow_override)
381388

382389

383-
def clear_and_configure(config: Optional[BinderCallable] = None, bind_in_runtime: bool = True) -> Injector:
390+
def clear_and_configure(
391+
config: Optional[BinderCallable] = None, bind_in_runtime: bool = True, allow_override: bool = False
392+
) -> Injector:
384393
"""Clear an existing injector and create another one with a callable config."""
385394
with _INJECTOR_LOCK:
386395
clear()
387-
return configure(config, bind_in_runtime=bind_in_runtime)
396+
return configure(config, bind_in_runtime=bind_in_runtime, allow_override=allow_override)
388397

389398

390399
def is_configured() -> bool:

test/test_binder.py

+10-8
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,19 @@ def test_bind(self):
1313
def test_bind__class_required(self):
1414
binder = Binder()
1515

16-
self.assertRaisesRegex(InjectorException, 'Binding key cannot be None',
17-
binder.bind, None, None)
16+
self.assertRaisesRegex(InjectorException, 'Binding key cannot be None', binder.bind, None, None)
1817

1918
def test_bind__duplicate_binding(self):
2019
binder = Binder()
2120
binder.bind(int, 123)
2221

23-
self.assertRaisesRegex(InjectorException, "Duplicate binding",
24-
binder.bind, int, 456)
22+
self.assertRaisesRegex(InjectorException, "Duplicate binding", binder.bind, int, 456)
23+
24+
def test_bind__allow_override(self):
25+
binder = Binder(allow_override=True)
26+
binder.bind(int, 123)
27+
binder.bind(int, 456)
28+
assert int in binder._bindings
2529

2630
def test_bind_provider(self):
2731
provider = lambda: 123
@@ -32,8 +36,7 @@ def test_bind_provider(self):
3236

3337
def test_bind_provider__provider_required(self):
3438
binder = Binder()
35-
self.assertRaisesRegex(InjectorException, "Provider cannot be None",
36-
binder.bind_to_provider, int, None)
39+
self.assertRaisesRegex(InjectorException, "Provider cannot be None", binder.bind_to_provider, int, None)
3740

3841
def test_bind_constructor(self):
3942
constructor = lambda: 123
@@ -44,5 +47,4 @@ def test_bind_constructor(self):
4447

4548
def test_bind_constructor__constructor_required(self):
4649
binder = Binder()
47-
self.assertRaisesRegex(InjectorException, "Constructor cannot be None",
48-
binder.bind_to_constructor, int, None)
50+
self.assertRaisesRegex(InjectorException, "Constructor cannot be None", binder.bind_to_constructor, int, None)

test/test_inject_configuration.py

+14-8
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55

66
class TestInjectConfiguration(BaseTestInject):
7-
87
def test_configure__should_create_injector(self):
98
injector0 = inject.configure()
109
injector1 = inject.get_injector()
@@ -19,8 +18,7 @@ def test_configure__should_add_bindings(self):
1918
def test_configure__already_configured(self):
2019
inject.configure()
2120

22-
self.assertRaisesRegex(InjectorException, 'Injector is already configured',
23-
inject.configure)
21+
self.assertRaisesRegex(InjectorException, 'Injector is already configured', inject.configure)
2422

2523
def test_configure_once__should_create_injector(self):
2624
injector = inject.configure_once()
@@ -48,11 +46,19 @@ def test_clear_and_configure(self):
4846
assert injector1 is not injector0
4947

5048
def test_get_injector_or_die(self):
51-
self.assertRaisesRegex(InjectorException, 'No injector is configured',
52-
inject.get_injector_or_die)
49+
self.assertRaisesRegex(InjectorException, 'No injector is configured', inject.get_injector_or_die)
5350

5451
def test_configure__runtime_binding_disabled(self):
5552
injector = inject.configure(bind_in_runtime=False)
56-
self.assertRaisesRegex(InjectorException,
57-
"No binding was found for key=<.* 'int'>",
58-
injector.get_instance, int)
53+
self.assertRaisesRegex(InjectorException, "No binding was found for key=<.* 'int'>", injector.get_instance, int)
54+
55+
def test_configure__install_allow_override(self):
56+
def base_config(binder):
57+
binder.bind(int, 123)
58+
59+
def config(binder):
60+
binder.install(base_config)
61+
binder.bind(int, 456)
62+
63+
injector = inject.configure(config, allow_override=True)
64+
assert injector.get_instance(int) == 456

0 commit comments

Comments
 (0)