Skip to content

Commit 1a70415

Browse files
authored
Merge pull request #201 from python-ellar/dynamic_command
feat: Dynamic command
2 parents 31b55c6 + a24197f commit 1a70415

File tree

8 files changed

+82
-30
lines changed

8 files changed

+82
-30
lines changed

ellar/core/middleware/middleware.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,8 @@ def __init__(self, cls: t.Type[T], **options: t.Any) -> None:
1919
@t.no_type_check
2020
def __call__(self, app: ASGIApp, injector: EllarInjector) -> T:
2121
self.kwargs.update(app=app)
22-
return injector.create_object(self.cls, additional_kwargs=self.kwargs)
22+
try:
23+
return injector.create_object(self.cls, additional_kwargs=self.kwargs)
24+
except TypeError: # pragma: no cover
25+
# TODO: Fix future typing for lower python version.
26+
return self.cls(**self.kwargs)

ellar/core/modules/config.py

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import dataclasses
22
import typing as t
33

4+
import click
45
from ellar.common import ControllerBase, ModuleRouter
56
from ellar.common.constants import MODULE_METADATA, MODULE_WATERMARK
67
from ellar.common.exceptions import ImproperConfiguration
@@ -34,26 +35,32 @@ class DynamicModule:
3435
routers: t.Sequence[t.Union[BaseRoute, ModuleRouter]] = dataclasses.field(
3536
default_factory=lambda: ()
3637
)
38+
39+
commands: t.Sequence[t.Union[click.Command, click.Group, t.Any]] = (
40+
dataclasses.field(default_factory=lambda: ())
41+
)
42+
3743
_is_configured: bool = False
3844

3945
def __post_init__(self) -> None:
4046
if not reflect.get_metadata(MODULE_WATERMARK, self.module):
4147
raise ImproperConfiguration(f"{self.module.__name__} is not a valid Module")
4248

49+
# # Commands needs to be registered so that
50+
# if self.commands:
51+
# reflect.define_metadata(MODULE_METADATA.COMMANDS, self.commands, self.module)
52+
4353
def apply_configuration(self) -> None:
4454
if self._is_configured:
4555
return
4656

4757
kwargs = {
48-
"controllers": list(self.controllers),
49-
"routers": list(self.routers),
50-
"providers": list(self.providers),
58+
MODULE_METADATA.CONTROLLERS: list(self.controllers),
59+
MODULE_METADATA.ROUTERS: list(self.routers),
60+
MODULE_METADATA.PROVIDERS: list(self.providers),
61+
MODULE_METADATA.COMMANDS: list(self.commands),
5162
}
52-
for key in [
53-
MODULE_METADATA.CONTROLLERS,
54-
MODULE_METADATA.ROUTERS,
55-
MODULE_METADATA.PROVIDERS,
56-
]:
63+
for key in kwargs.keys():
5764
value = kwargs[key]
5865
if value:
5966
reflect.delete_metadata(key, self.module)

ellar/di/utils.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,6 @@
55
def fail_silently(func: t.Callable, *args: t.Any, **kwargs: t.Any) -> t.Optional[t.Any]:
66
try:
77
return func(*args, **kwargs)
8-
except Exception: # pragma: no cover
9-
pass
8+
except Exception as ee: # pragma: no cover
9+
print(ee)
1010
return None

ellar/reflect/_reflect.py

Lines changed: 9 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -131,25 +131,17 @@ def _clone_meta_data(
131131

132132
@asynccontextmanager
133133
async def async_context(self) -> t.AsyncGenerator[None, None]:
134-
cached_meta_data = self._meta_data
135-
try:
136-
self._meta_data = self._clone_meta_data()
137-
yield
138-
finally:
139-
self._meta_data.clear()
140-
self._meta_data = cached_meta_data
134+
cached_meta_data = self._clone_meta_data()
135+
yield
136+
reflect._meta_data.clear()
137+
reflect._meta_data = WeakKeyDictionary(dict=cached_meta_data)
141138

142139
@contextmanager
143-
def context(
144-
self,
145-
) -> t.Generator:
146-
cached_meta_data = self._meta_data
147-
try:
148-
self._meta_data = self._clone_meta_data()
149-
yield
150-
finally:
151-
self._meta_data.clear()
152-
self._meta_data = cached_meta_data
140+
def context(self) -> t.Generator:
141+
cached_meta_data = self._clone_meta_data()
142+
yield
143+
reflect._meta_data.clear()
144+
reflect._meta_data = WeakKeyDictionary(dict=cached_meta_data)
153145

154146

155147
reflect = _Reflect()

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ classifiers = [
4141

4242
dependencies = [
4343
"injector == 0.21.0",
44-
"starlette == 0.37.1",
44+
"starlette == 0.37.2",
4545
"pydantic >=2.5.1,<3.0.0",
4646
"typing-extensions>=4.8.0",
4747
"jinja2",

tests/conftest.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from pathlib import PurePath, PurePosixPath, PureWindowsPath
33
from uuid import uuid4
44

5+
import click.testing
56
import pytest
67
from pydantic import create_model
78
from starlette.testclient import TestClient
@@ -33,3 +34,8 @@ def fixture_model_with_path(request):
3334
@pytest.fixture
3435
def random_type():
3536
return type(f"Random{uuid4().hex[:6]}", (), {})
37+
38+
39+
@pytest.fixture
40+
def cli_runner():
41+
return click.testing.CliRunner()

tests/test_modules/test_module_config.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from abc import ABC
22
from unittest.mock import patch
33

4+
import click
45
import pytest
56
from ellar.app import App
67
from ellar.common import (
@@ -23,6 +24,16 @@
2324
from ..main import router
2425

2526

27+
@click.command(name="command-one")
28+
def command_one():
29+
click.echo("Hello World command one")
30+
31+
32+
@click.command(name="command-two")
33+
def command_two():
34+
click.echo("Hello World command two")
35+
36+
2637
class IDynamic(ABC):
2738
a: int
2839
b: float
@@ -116,6 +127,17 @@ class LazyModuleImportWithSetup(ModuleBase):
116127
pass
117128

118129

130+
@Module(commands=[command_one])
131+
class DynamicModuleRegisterCommand(ModuleBase, IModuleSetup):
132+
@classmethod
133+
def setup(cls, command_three_text: str) -> DynamicModule:
134+
@click.command
135+
def command_three():
136+
click.echo(command_three_text)
137+
138+
return DynamicModule(cls, commands=[command_one, command_two, command_three])
139+
140+
119141
def test_invalid_lazy_module_import():
120142
with pytest.raises(ImproperConfiguration) as ex:
121143
LazyModuleImport("tests.test_modules.test_module_config:IDynamic").get_module()
@@ -303,3 +325,24 @@ def test_can_not_apply_dynamic_module_twice():
303325
with patch.object(reflect.__class__, "define_metadata") as mock_define_metadata:
304326
dynamic_module.apply_configuration()
305327
assert mock_define_metadata.called is False
328+
329+
330+
def test_dynamic_command_register_command(cli_runner):
331+
commands = reflect.get_metadata(
332+
MODULE_METADATA.COMMANDS, DynamicModuleRegisterCommand
333+
)
334+
assert len(commands) == 1
335+
res = cli_runner.invoke(commands[0], [])
336+
assert res.stdout == "Hello World command one\n"
337+
338+
with reflect.context():
339+
DynamicModuleRegisterCommand.setup("Command Three Here").apply_configuration()
340+
commands = reflect.get_metadata(
341+
MODULE_METADATA.COMMANDS, DynamicModuleRegisterCommand
342+
)
343+
assert len(commands) == 3
344+
345+
res = cli_runner.invoke(commands[2], [])
346+
assert res.stdout == "Command Three Here\n"
347+
348+
assert len(reflect._meta_data) > 10

tests/test_routing/test_route_endpoint_params.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ def get_requests_case_2(
4747
config: Inject[Config],
4848
):
4949
assert isinstance(config, Config) # True
50-
assert host is None # Starlette TestClient client info is None
50+
assert host == "testclient"
5151
assert isinstance(session, dict) and len(session) == 0
5252
return True
5353

0 commit comments

Comments
 (0)