Skip to content

Commit b5ca79e

Browse files
authored
Merge pull request #159 from python-ellar/stack_build_on_lifespan
App stack build on lifespan
2 parents 37246a4 + c4c35e4 commit b5ca79e

File tree

21 files changed

+1043
-122
lines changed

21 files changed

+1043
-122
lines changed

ellar/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
""" Ellar - Python ASGI web framework for building fast, efficient, and scalable RESTful APIs and server-side applications. """
22

3-
__version__ = "0.6.0"
3+
__version__ = "0.6.2"

ellar/app/context.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,12 @@
88
from ellar.common.utils.functional import SimpleLazyObject, empty
99
from ellar.core import Config
1010
from ellar.di import EllarInjector
11+
from ellar.events import app_context_started_events, app_context_teardown_events
1112

1213
if t.TYPE_CHECKING:
1314
from ellar.app.main import App
1415

15-
_application_context: ContextVar[
16+
app_context_var: ContextVar[
1617
t.Optional[t.Union["ApplicationContext", t.Any]]
1718
] = ContextVar("ellar.app.context")
1819

@@ -48,14 +49,15 @@ def config(self) -> Config:
4849
return self._config
4950

5051
def __enter__(self) -> "ApplicationContext":
51-
app_context = _application_context.get(empty)
52+
app_context = app_context_var.get(empty)
5253
if app_context is empty:
5354
# If app_context exist
54-
_application_context.set(self)
55+
app_context_var.set(self)
5556
if current_config._wrapped is not empty: # pragma: no cover
5657
# ensure current_config is in sync with running application context.
5758
current_config._wrapped = self.config
5859
app_context = self
60+
app_context_started_events.run()
5961
return app_context # type:ignore[return-value]
6062

6163
def __exit__(
@@ -64,7 +66,8 @@ def __exit__(
6466
exc_value: t.Optional[BaseException],
6567
tb: t.Optional[TracebackType],
6668
) -> None:
67-
_application_context.set(empty)
69+
app_context_teardown_events.run()
70+
app_context_var.set(empty)
6871

6972
current_app._wrapped = empty # type:ignore[attr-defined]
7073
current_injector._wrapped = empty # type:ignore[attr-defined]
@@ -76,23 +79,23 @@ def create(cls, app: "App") -> "ApplicationContext":
7679

7780

7881
def _get_current_app() -> "App":
79-
app_context = _application_context.get(empty)
82+
app_context = app_context_var.get(empty)
8083
if app_context is empty:
8184
raise RuntimeError("ApplicationContext is not available at this scope.")
8285

8386
return app_context.app # type:ignore[union-attr]
8487

8588

8689
def _get_injector() -> EllarInjector:
87-
app_context = _application_context.get(empty)
90+
app_context = app_context_var.get(empty)
8891
if app_context is empty:
8992
raise RuntimeError("ApplicationContext is not available at this scope.")
9093

9194
return app_context.injector # type:ignore[union-attr]
9295

9396

9497
def _get_application_config() -> Config:
95-
app_context = _application_context.get(empty)
98+
app_context = app_context_var.get(empty)
9699
if app_context is empty:
97100
config_module = os.environ.get(ELLAR_CONFIG_MODULE)
98101
if not config_module:

ellar/app/factory.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,14 @@ def read_all_module(cls, module_config: ModuleSetup) -> t.Dict[t.Type, ModuleSet
4242
:param module_config: Module Type
4343
:return: t.Dict[t.Type, t.Type[ModuleBase]]
4444
"""
45+
global_module_config = module_config
4546
modules = (
4647
reflect.get_metadata(MODULE_METADATA.MODULES, module_config.module) or []
4748
)
4849
module_dependency = OrderedDict()
4950
for module in modules:
5051
if isinstance(module, LazyModuleImport):
51-
module = module.get_module(module_config.module.__name__)
52+
module = module.get_module(global_module_config.module.__name__)
5253

5354
if isinstance(module, DynamicModule):
5455
module.apply_configuration()
@@ -167,7 +168,7 @@ def _get_config_kwargs() -> t.Dict:
167168

168169
if module_changed:
169170
app.router.extend(routes)
170-
app.rebuild_stack()
171+
# app.rebuild_stack()
171172

172173
return app
173174

ellar/app/main.py

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -81,20 +81,11 @@ def __init__(
8181
self.config.DEFAULT_LIFESPAN_HANDLER # type: ignore[arg-type]
8282
).lifespan,
8383
)
84-
if (
85-
self.config.STATIC_MOUNT_PATH
86-
and self.config.STATIC_MOUNT_PATH not in self.router.routes
87-
):
88-
self.router.append(AppStaticFileMount(self))
8984

9085
self._finalize_app_initialization()
91-
self.middleware_stack = self.build_middleware_stack()
86+
self.middleware_stack: t.Optional[ASGIApp] = None
9287
self._config_logging()
9388

94-
self.reload_event_manager += lambda app: self._update_jinja_env_filters( # type:ignore[misc]
95-
self.jinja_environment
96-
)
97-
9889
def _config_logging(self) -> None:
9990
log_level = (
10091
self.config.LOG_LEVEL.value
@@ -150,8 +141,8 @@ def install_module(
150141
if isinstance(module_ref, ModuleTemplateRef):
151142
self.router.extend(module_ref.routes)
152143

153-
self.rebuild_stack()
154-
144+
# self.rebuild_stack()
145+
self._update_jinja_env_filters(self.jinja_environment)
155146
return t.cast(T, module_ref.get_module_instance())
156147

157148
def get_guards(self) -> t.List[t.Union[t.Type[GuardCanActivate], GuardCanActivate]]:
@@ -257,6 +248,17 @@ def application_context(self) -> ApplicationContext:
257248
async def __call__(self, scope: TScope, receive: TReceive, send: TSend) -> None:
258249
with self.application_context() as ctx:
259250
scope["app"] = ctx.app
251+
if self.middleware_stack is None:
252+
self.middleware_stack = self.build_middleware_stack()
253+
254+
self._update_jinja_env_filters(self.jinja_environment)
255+
256+
if (
257+
self.config.STATIC_MOUNT_PATH
258+
and self.config.STATIC_MOUNT_PATH not in self.router.routes
259+
):
260+
self.router.append(AppStaticFileMount(self))
261+
260262
await self.middleware_stack(scope, receive, send)
261263

262264
@property
@@ -291,17 +293,17 @@ def add_exception_handler(
291293
self,
292294
*exception_handlers: IExceptionHandler,
293295
) -> None:
294-
_added_any = False
296+
# _added_any = False
295297
for exception_handler in exception_handlers:
296298
if exception_handler not in self._exception_handlers:
297299
self._exception_handlers.append(exception_handler)
298-
_added_any = True
299-
if _added_any:
300-
self.rebuild_stack()
300+
# _added_any = True
301+
# if _added_any:
302+
# self.rebuild_stack()
301303

302-
def rebuild_stack(self) -> None:
303-
self.middleware_stack = self.build_middleware_stack()
304-
self.reload_event_manager.run(self)
304+
# def rebuild_stack(self) -> None:
305+
# self.middleware_stack = self.build_middleware_stack()
306+
# self.reload_event_manager.run(self)
305307

306308
@property
307309
def reflector(self) -> Reflector:

ellar/app/mixin.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
from ellar.core.conf import Config
1515
from ellar.core.connection import Request
1616
from ellar.di import EllarInjector
17-
from ellar.events import EventManager
1817
from jinja2 import Environment as BaseEnvironment
1918
from starlette.templating import pass_context
2019

@@ -26,8 +25,7 @@ class AppMixin(JinjaTemplating):
2625
_static_app: t.Optional[ASGIApp]
2726
_injector: EllarInjector
2827
_config: Config
29-
rebuild_stack: t.Callable
30-
reload_event_manager = EventManager()
28+
# rebuild_stack: t.Callable
3129

3230
def get_module_loaders(self) -> t.Generator[ModuleTemplating, None, None]:
3331
for loader in self._injector.get_templating_modules().values():
@@ -42,7 +40,7 @@ def debug(self, value: bool) -> None:
4240
del self.__dict__["jinja_environment"]
4341
self._config.DEBUG = value
4442
# TODO: Add warning
45-
self.rebuild_stack()
43+
# self.rebuild_stack()
4644

4745
@cached_property
4846
def jinja_environment(self) -> BaseEnvironment: # type: ignore[override]

ellar/common/decorators/modules.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from ellar.common.exceptions import ImproperConfiguration
99
from ellar.common.models import ControllerBase
1010
from ellar.common.routing import ModuleMount, ModuleRouter
11+
from ellar.common.utils.importer import get_main_directory_by_stack
1112
from ellar.di import ProviderConfig, SingletonScope, injectable
1213
from ellar.reflect import reflect
1314
from starlette.routing import Host, Mount
@@ -76,6 +77,7 @@ def Module(
7677
7778
:return: t.TYPE[ModuleBase]
7879
"""
80+
base_directory = get_main_directory_by_stack(base_directory, stack_level=2) # type:ignore[arg-type]
7981
kwargs = AttributeDict(
8082
name=name,
8183
controllers=list(controllers),

ellar/common/utils/importer.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
import inspect
2+
import os
13
import re
24
import typing as t
5+
from pathlib import Path
36

47
_module_import_regex = re.compile("(((\\w+)?(\\.<\\w+>)?(\\.\\w+))+)", re.IGNORECASE)
58

@@ -70,3 +73,49 @@ def get_class_import(klass: t.Union[t.Type, t.Any]) -> str: # pragma: no cover
7073
if len(split_result) == 2:
7174
return f"{split_result[0]}:{split_result[1]}"
7275
return result
76+
77+
78+
def get_main_directory_by_stack(
79+
path: str, stack_level: int, from_dir: t.Optional[str] = None
80+
) -> str:
81+
"""
82+
Gets Directory Based on execution stack level or from a base directory
83+
84+
example:
85+
from pathlib import Path
86+
87+
directory = get_main_directory_by_stack("__main__", stack_level=1)
88+
file_directory = Path(__file__).resolve()
89+
90+
assert directory == str(file_directory)
91+
92+
directory = get_main_directory_by_stack("__main__", stack_level=2)
93+
assert directory == str(file_directory.parent)
94+
"""
95+
forced_path_to_string = str(path)
96+
if forced_path_to_string.startswith("__main__") or forced_path_to_string.startswith(
97+
"/__main__"
98+
):
99+
__main__, others = forced_path_to_string.replace("/", " ").split("__main__")
100+
__parent__ = False
101+
102+
if "__parent__" in others:
103+
__parent__ = True
104+
105+
if not from_dir:
106+
stack = inspect.stack()[stack_level]
107+
__main__parent = Path(stack.filename).resolve().parent
108+
else:
109+
# let's work with a given base directory
110+
__main__parent = Path(from_dir).resolve()
111+
112+
if __parent__:
113+
parent_split = others.split("__parent__")
114+
for item in parent_split:
115+
if item == " ":
116+
__main__parent = __main__parent.parent
117+
else:
118+
return os.path.join(__main__parent, item.strip())
119+
120+
return os.path.join(str(__main__parent), others.strip())
121+
return path

ellar/core/modules/config.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -189,10 +189,10 @@ def get_module(
189189
module_cls: t.Type["ModuleBase"] = t.cast(
190190
t.Type["ModuleBase"], import_from_string(self.module)
191191
)
192-
except Exception:
192+
except Exception as ex:
193193
raise ImproperConfiguration(
194-
f'Unable to import "{self.module}" registered in Module[{root_module_name}]'
195-
) from None
194+
f'Unable to import "{self.module}" registered in "{root_module_name}"'
195+
) from ex
196196
self.validate_module(module_cls)
197197
if self.setup_method:
198198
setup_method = getattr(module_cls, self.setup_method)

ellar/core/routing/file_mount.py

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
import inspect
21
import os.path
32
import typing as t
4-
from pathlib import Path
53

64
from ellar.common.types import ASGIApp
5+
from ellar.common.utils.importer import get_main_directory_by_stack
76
from ellar.core.staticfiles import StaticFiles
87
from starlette.middleware import Middleware
98
from starlette.routing import BaseRoute, Mount
@@ -22,11 +21,7 @@ def __init__(
2221
middleware: t.Optional[t.Sequence[Middleware]] = None,
2322
base_directory: t.Optional[str] = None,
2423
) -> None:
25-
if base_directory == "__parent__":
26-
# stacks = inspect.stack()
27-
stack = inspect.stack()[1]
28-
base_directory = Path(stack.filename).resolve().parent # type:ignore[assignment]
29-
24+
base_directory = get_main_directory_by_stack(base_directory, stack_level=2) # type: ignore[arg-type]
3025
if base_directory:
3126
directories = [
3227
str(os.path.join(base_directory, directory))
@@ -62,7 +57,6 @@ def __init__(self, app: "App") -> None:
6257
packages=packages,
6358
)
6459
# subscribe to app reload
65-
app.reload_event_manager += self._reload_static_files # type:ignore[misc]
6660

6761
def _get_static_directories(self, app: "App") -> tuple:
6862
static_directories = t.cast(t.List, app.config.STATIC_DIRECTORIES or [])
@@ -71,8 +65,8 @@ def _get_static_directories(self, app: "App") -> tuple:
7165
static_directories.append(module.static_directory)
7266
return static_directories, app.config.STATIC_FOLDER_PACKAGES
7367

74-
def _reload_static_files(self, app: "App") -> None:
75-
directories, packages = self._get_static_directories(app)
76-
self.app = self._combine_app_with_middleware(
77-
StaticFiles(directories=directories, packages=packages)
78-
)
68+
# def _reload_static_files(self, app: "App") -> None:
69+
# directories, packages = self._get_static_directories(app)
70+
# self.app = self._combine_app_with_middleware(
71+
# StaticFiles(directories=directories, packages=packages)
72+
# )

ellar/events/__init__.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from .base import EventHandler, EventManager
2+
3+
app_context_started_events = EventManager()
4+
app_context_teardown_events = EventManager()
5+
6+
__all__ = [
7+
"app_context_started_events",
8+
"app_context_teardown_events",
9+
"EventHandler",
10+
"EventManager",
11+
]

0 commit comments

Comments
 (0)