Skip to content

Commit 232a0d7

Browse files
committed
Added ForwardRefModule doc
1 parent df782ec commit 232a0d7

File tree

3 files changed

+89
-3
lines changed

3 files changed

+89
-3
lines changed

docs/overview/modules.md

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,3 +259,80 @@ These can then be resolved if required by any object in the application.
259259

260260
For more details on the use cases of the `injector` module,
261261
you can refer to the documentation [here](https://injector.readthedocs.io/en/latest/terminology.html#module).
262+
263+
264+
## **ForwardRefModule**
265+
`ForwardRefModule` is a powerful feature that allows you to reference a `@Module()` class in your application
266+
without needing to instantiate or configure it directly at the point of reference.
267+
This is particularly useful in scenarios where you have circular dependencies between modules
268+
or when you want to declare dependencies without tightly coupling your modules.
269+
270+
### Forward Reference by Class
271+
272+
In the following example, we have two modules, `ModuleA` and `ModuleB`. `ModuleB`
273+
needs to reference `ModuleA` as a dependency, but instead of instantiating `ModuleA`
274+
directly, it uses `ForwardRefModule` to declare the dependency.
275+
276+
```python
277+
from ellar.common import Module
278+
from ellar.core.modules import ForwardRefModule, ModuleBase, ModuleRefBase
279+
280+
@Module(name="moduleA")
281+
class ModuleA:
282+
pass
283+
284+
285+
@Module(name="ModuleB", modules=[ForwardRefModule(ModuleA)])
286+
class ModuleB(ModuleBase):
287+
@classmethod
288+
def post_build(cls, module_ref: ModuleRefBase) -> None:
289+
assert ModuleA in module_ref.modules
290+
291+
292+
@Module(modules=[ModuleA, ModuleB])
293+
class ApplicationModule(ModuleBase):
294+
pass
295+
```
296+
297+
In this example:
298+
- `ModuleB` references `ModuleA` using `ForwardRefModule`, meaning `ModuleB` knows about `ModuleA` but doesn't instantiate it.
299+
- When `ApplicationModule` is built, both `ModuleA` and `ModuleB` are instantiated. During this build process, `ModuleB` can reference the instance of `ModuleA`, ensuring that all dependencies are resolved properly.
300+
301+
This pattern is particularly useful when modules need to reference each other, creating a situation where they might otherwise be instantiated out of order or cause circular dependencies.
302+
303+
### Forward Reference by Name
304+
305+
`ForwardRefModule` also supports referencing a module by its name, allowing for even more flexibility. This is beneficial when module classes are defined in separate files or when the module class may not be available at the time of reference.
306+
307+
```python
308+
from ellar.common import Module
309+
from ellar.core.modules import ForwardRefModule, ModuleBase, ModuleRefBase
310+
311+
@Module(name="moduleA")
312+
class ModuleA:
313+
pass
314+
315+
316+
@Module(name="ModuleB", modules=[ForwardRefModule(module_name="moduleA")])
317+
class ModuleB(ModuleBase):
318+
@classmethod
319+
def post_build(cls, module_ref: ModuleRefBase) -> None:
320+
assert ModuleA in module_ref.modules
321+
322+
323+
@Module(modules=[ModuleA, ModuleB])
324+
class ApplicationModule(ModuleBase):
325+
pass
326+
```
327+
328+
In this second example:
329+
- `ModuleB` references `ModuleA` by its name, `"moduleA"`.
330+
- During the build process of `ApplicationModule`, the name reference is resolved, ensuring that `ModuleA` is instantiated and injected into `ModuleB` correctly.
331+
332+
This method allows you to define module dependencies without worrying about the order of their definition,
333+
providing greater modularity and flexibility in your application's architecture.
334+
335+
By using `ForwardRefModule`, you can build complex,
336+
interdependent module structures without running into
337+
issues related to instantiation order or circular dependencies,
338+
making your codebase more maintainable and easier to manage.

ellar/core/modules/ref/base.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import typing as t
22
from abc import ABC, abstractmethod
33
from functools import cached_property
4+
from typing import Type
45

56
from ellar.auth.constants import POLICY_KEYS
67
from ellar.auth.policy.base import _PolicyHandlerWithRequirement
@@ -83,13 +84,21 @@ def is_ready(self) -> bool:
8384
@property
8485
def exports(self) -> t.List[t.Type]:
8586
"""gets module ref config"""
86-
return self._exports
87+
return self._exports.copy()
8788

8889
@property
8990
def providers(self) -> t.Dict[t.Type, ProviderConfig]:
9091
"""gets module ref config"""
9192
return self._providers.copy()
9293

94+
@property
95+
def modules(self) -> t.List[Type["ModuleBase"]]:
96+
"""gets module ref config"""
97+
return [
98+
i.value.module # type:ignore[misc]
99+
for i in self.tree_manager.get_module_dependencies(self.module)
100+
]
101+
93102
@t.no_type_check
94103
def get(
95104
self,

tests/test_exceptions/test_custom_exceptions.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22

33
import pytest
44
from ellar.common import IExceptionHandler, IHostContext, Inject, get, ws_route
5-
from ellar.common.exceptions.callable_exceptions import CallableExceptionHandler
6-
from ellar.common.exceptions.handlers import APIException, APIExceptionHandler
75
from ellar.core import Config, WebSocket
6+
from ellar.core.exceptions.callable_exceptions import CallableExceptionHandler
7+
from ellar.core.exceptions.handlers import APIException, APIExceptionHandler
88
from ellar.core.exceptions.service import ExceptionMiddlewareService
99
from ellar.core.middleware import ExceptionMiddleware
1010
from ellar.testing import Test

0 commit comments

Comments
 (0)