Skip to content

Commit d38c9dd

Browse files
committed
feat: Added lazy import and application context that exposes default config, current running injector and current running application
1 parent 7dc9f25 commit d38c9dd

24 files changed

+938
-45
lines changed

ellar/app/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1+
from .context import current_app, current_config, current_injector
12
from .factory import AppFactory
23
from .main import App
34

45
__all__ = [
56
"App",
67
"AppFactory",
8+
"current_config",
9+
"current_injector",
10+
"current_app",
711
]

ellar/app/context.py

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import os
2+
import typing as t
3+
from contextvars import ContextVar
4+
from types import TracebackType
5+
6+
from ellar.common.constants import ELLAR_CONFIG_MODULE
7+
from ellar.common.utils.functional import SimpleLazyObject, empty
8+
from ellar.core import Config
9+
from ellar.di import EllarInjector
10+
11+
if t.TYPE_CHECKING:
12+
from .main import App
13+
14+
_application_context: ContextVar[
15+
t.Optional[t.Union["ApplicationContext", t.Any]]
16+
] = ContextVar("ellar.app.context")
17+
_application_context.set(empty)
18+
19+
20+
class ApplicationContext:
21+
__slots__ = ("_injector", "_config", "_app")
22+
23+
def __init__(self, config: Config, injector: EllarInjector, app: "App") -> None:
24+
assert isinstance(config, Config), "config must instance of Config"
25+
assert isinstance(
26+
injector, EllarInjector
27+
), "injector must instance of EllarInjector"
28+
29+
self._injector = injector
30+
self._config = config
31+
self._app = app
32+
33+
@property
34+
def app(self) -> "App":
35+
return self._app
36+
37+
@property
38+
def injector(self) -> EllarInjector:
39+
return self._injector
40+
41+
@property
42+
def config(self) -> Config:
43+
return self._config
44+
45+
def __enter__(self) -> "ApplicationContext":
46+
app_context = _application_context.get()
47+
if app_context is empty:
48+
# If app_context exist
49+
_application_context.set(self)
50+
if current_config._wrapped is not empty:
51+
# ensure current_config is in sync with running application context.
52+
current_config._wrapped = self.config
53+
54+
app_context = self
55+
return app_context # type:ignore[return-value]
56+
57+
def __exit__(
58+
self,
59+
exc_type: t.Optional[t.Any],
60+
exc_value: t.Optional[BaseException],
61+
tb: t.Optional[TracebackType],
62+
) -> None:
63+
_application_context.set(empty)
64+
65+
current_app._wrapped = empty # type:ignore[attr-defined]
66+
current_injector._wrapped = empty # type:ignore[attr-defined]
67+
current_config._wrapped = empty
68+
69+
@classmethod
70+
def create(cls, app: "App") -> "ApplicationContext":
71+
return cls(app.config, app.injector, app)
72+
73+
74+
def _get_current_app() -> "App":
75+
if _application_context.get() is empty:
76+
raise RuntimeError("App is not available at this scope.")
77+
78+
app_context = _application_context.get()
79+
return app_context.app # type:ignore[union-attr]
80+
81+
82+
def _get_injector() -> EllarInjector:
83+
if _application_context.get() is empty:
84+
raise RuntimeError("App is not available at this scope.")
85+
86+
app_context = _application_context.get()
87+
return app_context.injector # type:ignore[union-attr]
88+
89+
90+
def _get_application_config() -> Config:
91+
if _application_context.get() is empty:
92+
config_module = os.environ.get(ELLAR_CONFIG_MODULE)
93+
if not config_module:
94+
raise RuntimeError(
95+
"You are trying to access app config outside app context "
96+
"and %s is not specified. This may cause differences in config "
97+
"values when the app." % (ELLAR_CONFIG_MODULE,)
98+
)
99+
100+
app_context = _application_context.get()
101+
return app_context.config # type:ignore[union-attr]
102+
103+
104+
current_app: "App" = t.cast("App", SimpleLazyObject(func=_get_current_app))
105+
current_injector: EllarInjector = t.cast(
106+
EllarInjector, SimpleLazyObject(func=_get_injector)
107+
)
108+
current_config: Config = t.cast(Config, SimpleLazyObject(func=_get_application_config))

ellar/app/factory.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,8 +130,10 @@ def _get_config_kwargs() -> t.Dict:
130130
assert reflect.get_metadata(MODULE_WATERMARK, module), "Only Module is allowed"
131131

132132
config = Config(app_configured=True, **_get_config_kwargs())
133+
133134
injector = EllarInjector(auto_bind=config.INJECTOR_AUTO_BIND)
134135
injector.container.register_instance(config, concrete_type=Config)
136+
135137
service = EllarAppService(injector, config)
136138
service.register_core_services()
137139

ellar/app/lifespan.py

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
from ellar.common import IApplicationShutdown, IApplicationStartup, logger
55
from ellar.reflect import asynccontextmanager
66

7+
from .context import ApplicationContext
8+
79
if t.TYPE_CHECKING: # pragma: no cover
810
from ellar.app import App
911

@@ -53,13 +55,16 @@ async def run_all_shutdown_actions(self, app: "App") -> None:
5355
async def lifespan(self, app: "App") -> t.AsyncIterator[t.Any]:
5456
logger.logger.debug("Executing Modules Startup Handlers")
5557

56-
async with create_task_group() as tg:
57-
tg.start_soon(self.run_all_startup_actions, app)
58-
59-
try:
60-
async with self._lifespan_context(app) as ctx: # type:ignore[attr-defined]
61-
yield ctx
62-
finally:
63-
logger.logger.debug("Executing Modules Shutdown Handlers")
58+
with ApplicationContext.create(app):
6459
async with create_task_group() as tg:
65-
tg.start_soon(self.run_all_shutdown_actions, app)
60+
tg.start_soon(self.run_all_startup_actions, app)
61+
62+
try:
63+
async with self._lifespan_context(
64+
app
65+
) as ctx: # type:ignore[attr-defined]
66+
yield ctx
67+
finally:
68+
logger.logger.debug("Executing Modules Shutdown Handlers")
69+
async with create_task_group() as tg:
70+
tg.start_soon(self.run_all_shutdown_actions, app)

ellar/app/main.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from ellar.common.models import EllarInterceptor, GuardCanActivate
1313
from ellar.common.templating import Environment
1414
from ellar.common.types import ASGIApp, T, TReceive, TScope, TSend
15+
from ellar.core import reflector
1516
from ellar.core.conf import Config
1617
from ellar.core.middleware import (
1718
CORSMiddleware,
@@ -32,15 +33,15 @@
3233
)
3334
from ellar.core.routing import ApplicationRouter
3435
from ellar.core.services import Reflector
35-
from ellar.core.templating import AppTemplating
3636
from ellar.core.versioning import BaseAPIVersioning, VersioningSchemes
37-
from ellar.di.injector import EllarInjector
37+
from ellar.di import EllarInjector
3838
from starlette.routing import BaseRoute, Mount
3939

4040
from .lifespan import EllarApplicationLifespan
41+
from .mixin import AppMixin
4142

4243

43-
class App(AppTemplating):
44+
class App(AppMixin):
4445
def __init__(
4546
self,
4647
config: "Config",
@@ -198,7 +199,7 @@ def has_static_files(self) -> bool: # type: ignore
198199
)
199200

200201
@property
201-
def config(self) -> "Config": # type: ignore
202+
def config(self) -> "Config":
202203
return self._config
203204

204205
def build_middleware_stack(self) -> ASGIApp:
@@ -303,9 +304,9 @@ def add_exception_handler(
303304
def rebuild_middleware_stack(self) -> None:
304305
self.middleware_stack = self.build_middleware_stack()
305306

306-
@cached_property
307+
@property
307308
def reflector(self) -> Reflector:
308-
return self.injector.get(Reflector) # type: ignore[no-any-return]
309+
return reflector
309310

310311
@cached_property
311312
def __identity_scheme(self) -> IIdentitySchemes:

ellar/core/templating/app.py renamed to ellar/app/mixin.py

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,21 +12,21 @@
1212
ModuleTemplating,
1313
)
1414
from ellar.common.types import ASGIApp
15+
from ellar.core.conf import Config
1516
from ellar.core.connection import Request
1617
from ellar.core.staticfiles import StaticFiles
18+
from ellar.di import EllarInjector
1719
from jinja2 import Environment as BaseEnvironment
1820
from starlette.templating import pass_context
1921

20-
if t.TYPE_CHECKING: # pragma: no cover
21-
from ellar.core.conf import Config
22-
from ellar.core.main import App
23-
from ellar.di import EllarInjector
22+
if t.TYPE_CHECKING:
23+
from .main import App
2424

2525

26-
class AppTemplating(JinjaTemplating):
27-
config: "Config"
26+
class AppMixin(JinjaTemplating):
2827
_static_app: t.Optional[ASGIApp]
29-
_injector: "EllarInjector"
28+
_injector: EllarInjector
29+
_config: Config
3030
has_static_files: bool
3131

3232
@abstractmethod
@@ -43,12 +43,12 @@ def get_module_loaders(self) -> t.Generator[ModuleTemplating, None, None]:
4343

4444
@property
4545
def debug(self) -> bool:
46-
return self.config.DEBUG
46+
return self._config.DEBUG
4747

4848
@debug.setter
4949
def debug(self, value: bool) -> None:
5050
del self.__dict__["jinja_environment"]
51-
self.config.DEBUG = value
51+
self._config.DEBUG = value
5252
# TODO: Add warning
5353
self.rebuild_middleware_stack()
5454

@@ -70,7 +70,7 @@ def select_jinja_auto_escape(filename: str) -> bool:
7070
"autoescape": select_jinja_auto_escape,
7171
}
7272
jinja_options: t.Dict = t.cast(
73-
t.Dict, self.config.JINJA_TEMPLATES_OPTIONS or {}
73+
t.Dict, self._config.JINJA_TEMPLATES_OPTIONS or {}
7474
)
7575

7676
for k, v in options_defaults.items():
@@ -81,12 +81,12 @@ def url_for(context: dict, name: str, **path_params: t.Any) -> URL:
8181
request = t.cast(Request, context["request"])
8282
return request.url_for(name, **path_params)
8383

84-
app: App = t.cast("App", self)
84+
app: "App" = t.cast("App", self)
8585

8686
jinja_env = Environment(app, **jinja_options)
8787
jinja_env.globals.update(
8888
url_for=url_for,
89-
config=self.config,
89+
config=self._config,
9090
)
9191
jinja_env.policies["json.dumps_function"] = json.dumps
9292
return jinja_env
@@ -97,7 +97,7 @@ def create_global_jinja_loader(self) -> JinjaLoader:
9797
def create_static_app(self) -> ASGIApp:
9898
return StaticFiles(
9999
directories=self.static_files, # type: ignore[arg-type]
100-
packages=self.config.STATIC_FOLDER_PACKAGES,
100+
packages=self._config.STATIC_FOLDER_PACKAGES,
101101
)
102102

103103
def reload_static_app(self) -> None:
@@ -107,12 +107,12 @@ def reload_static_app(self) -> None:
107107
self._update_jinja_env_filters(self.jinja_environment)
108108

109109
def _update_jinja_env_filters(self, jinja_environment: BaseEnvironment) -> None:
110-
jinja_environment.globals.update(self.config.get(TEMPLATE_GLOBAL_KEY, {}))
111-
jinja_environment.filters.update(self.config.get(TEMPLATE_FILTER_KEY, {}))
110+
jinja_environment.globals.update(self._config.get(TEMPLATE_GLOBAL_KEY, {}))
111+
jinja_environment.filters.update(self._config.get(TEMPLATE_FILTER_KEY, {}))
112112

113113
@cached_property
114114
def static_files(self) -> t.List[str]:
115-
static_directories = t.cast(t.List, self.config.STATIC_DIRECTORIES or [])
115+
static_directories = t.cast(t.List, self._config.STATIC_DIRECTORIES or [])
116116
for module in self.get_module_loaders():
117117
if module.static_directory:
118118
static_directories.append(module.static_directory)

ellar/app/services.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,12 @@
1212
IInterceptorsConsumer,
1313
IWebSocketContextFactory,
1414
)
15-
from ellar.core.context import ExecutionContextFactory, HostContextFactory
16-
from ellar.core.context.factory import (
15+
from ellar.core.exceptions.service import ExceptionMiddlewareService
16+
from ellar.core.execution_context import ExecutionContextFactory, HostContextFactory
17+
from ellar.core.execution_context.factory import (
1718
HTTPConnectionContextFactory,
1819
WebSocketContextFactory,
1920
)
20-
from ellar.core.exceptions.service import ExceptionMiddlewareService
2121
from ellar.core.guards import GuardConsumer
2222
from ellar.core.interceptors import EllarInterceptorConsumer
2323
from ellar.core.services import Reflector, reflector

ellar/common/templating/loader.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from .environment import Environment
77

88
if t.TYPE_CHECKING: # pragma: no cover
9-
from ellar.core.main import App
9+
from ellar.app.main import App
1010

1111

1212
class JinjaLoader(BaseLoader):

0 commit comments

Comments
 (0)