Skip to content

Commit 0dd498b

Browse files
committed
Added support for click commands
1 parent 599b8f0 commit 0dd498b

File tree

6 files changed

+87
-15
lines changed

6 files changed

+87
-15
lines changed

ellar_cli/main.py

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,25 @@
22
import sys
33
import typing as t
44

5+
import click
56
import typer
67
from ellar.common.commands import EllarTyper
78
from ellar.common.constants import CALLABLE_COMMAND_INFO, MODULE_METADATA
89
from ellar.core.factory import AppFactory
910
from ellar.core.modules import ModuleSetup
1011
from ellar.core.services import Reflector
11-
from typer import Typer
1212
from typer.models import CommandInfo
1313

1414
from ellar_cli.constants import ELLAR_META
1515

1616
from .manage_commands import create_module, create_project, new_command, runserver
1717
from .service import EllarCLIService
18+
from .typer import EllarCLITyper
1819

1920
__all__ = ["build_typers", "_typer", "typer_callback"]
2021

21-
_typer = Typer(name="ellar")
22+
23+
_typer = EllarCLITyper(name="ellar")
2224
_typer.command(name="new")(new_command)
2325
_typer.command()(runserver)
2426
_typer.command(name="create-project")(create_project)
@@ -41,21 +43,20 @@ def typer_callback(
4143
ctx.meta[ELLAR_META] = meta_
4244

4345

44-
def build_typers() -> t.Any:
46+
def build_typers() -> t.Any: # pragma: no cover
47+
app_name: t.Optional[str] = None
4548
try:
49+
argv = list(sys.argv)
4650
options, args = getopt.getopt(
47-
sys.argv[1:],
48-
"p:",
51+
argv[1:],
52+
"hp:",
4953
["project=", "help"],
5054
)
51-
app_name: t.Optional[str] = None
52-
5355
for k, v in options:
5456
if k in ["-p", "--project"] and v:
5557
app_name = v
56-
except Exception:
57-
typer.Abort()
58-
return 1
58+
except Exception as ex:
59+
click.echo(ex)
5960

6061
meta_: t.Optional[EllarCLIService] = EllarCLIService.import_project_meta(app_name)
6162

@@ -77,3 +78,5 @@ def build_typers() -> t.Any:
7778
CALLABLE_COMMAND_INFO
7879
]
7980
_typer.registered_commands.append(command_info)
81+
elif isinstance(typer_command, click.Command):
82+
_typer.add_click_command(typer_command)

ellar_cli/manage_commands/create_project.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ def get_scaffolding_context(self, working_project_name: str) -> t.Dict:
5757
_prefix = f"{self.prefix}." if self.prefix else ""
5858
template_context = {
5959
"project_name": working_project_name,
60-
"secret_key": f"ellar_{secrets.token_hex(32)}",
60+
"secret_key": f"ellar_{secrets.token_urlsafe(32)}",
6161
"config_prefix": _prefix,
6262
}
6363
return template_context

ellar_cli/typer.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import sys
2+
import typing as t
3+
from dataclasses import dataclass
4+
5+
import click
6+
from typer import Typer
7+
from typer.main import _typer_developer_exception_attr_name, except_hook, get_command
8+
from typer.models import DeveloperExceptionConfig
9+
10+
11+
@dataclass
12+
class _TyperClickCommand:
13+
command: click.Command
14+
name: t.Optional[str]
15+
16+
17+
class EllarCLITyper(Typer):
18+
"""
19+
Adapting Typer and Click Commands
20+
"""
21+
22+
def __init__(self, *args: t.Any, **kwargs: t.Any) -> None:
23+
super().__init__(*args, **kwargs)
24+
self._click_commands: t.List[_TyperClickCommand] = []
25+
26+
def add_click_command(
27+
self, cmd: click.Command, name: t.Optional[str] = None
28+
) -> None:
29+
self._click_commands.append(_TyperClickCommand(command=cmd, name=name))
30+
31+
def __call__(self, *args: t.Any, **kwargs: t.Any) -> t.Any:
32+
if sys.excepthook != except_hook:
33+
sys.excepthook = except_hook # type: ignore[assignment]
34+
try:
35+
typer_click_commands = get_command(self)
36+
for item in self._click_commands:
37+
typer_click_commands.add_command(item.command, item.name) # type: ignore[attr-defined]
38+
return typer_click_commands(*args, **kwargs)
39+
except Exception as e:
40+
# Set a custom attribute to tell the hook to show nice exceptions for user
41+
# code. An alternative/first implementation was a custom exception with
42+
# raise custom_exc from e
43+
# but that means the last error shown is the custom exception, not the
44+
# actual error. This trick improves developer experience by showing the
45+
# actual error last.
46+
setattr(
47+
e,
48+
_typer_developer_exception_attr_name,
49+
DeveloperExceptionConfig(
50+
pretty_exceptions_enable=self.pretty_exceptions_enable,
51+
pretty_exceptions_show_locals=self.pretty_exceptions_show_locals,
52+
pretty_exceptions_short=self.pretty_exceptions_short,
53+
),
54+
)
55+
raise e

tests/sample_app/example_project/commands.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import click
12
from ellar.common import EllarTyper, command
23

34
db = EllarTyper(name="db")
@@ -13,3 +14,8 @@ def create_migration():
1314
def whatever_you_want():
1415
"""Whatever you want"""
1516
print("Whatever you want command")
17+
18+
19+
@click.command()
20+
def say_hello():
21+
click.echo("Hello from ellar.")

tests/sample_app/example_project/root_module.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22
from ellar.core import ModuleBase
33
from ellar.core.connection import Request
44

5-
from .commands import db, whatever_you_want
5+
from .commands import db, say_hello, whatever_you_want
66

77

8-
@Module(commands=[db, whatever_you_want])
8+
@Module(commands=[db, whatever_you_want, say_hello])
99
class ApplicationModule(ModuleBase):
1010
@exception_handler(404)
1111
def exception_404_handler(cls, request: Request, exc: Exception) -> Response:

tests/test_build_typers.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,11 @@ def test_build_typers_command_for_specific_project_works():
4040
os.chdir(sample_app_path)
4141

4242
result = subprocess.run(
43-
["ellar", "-p", "example_project_2", "whatever-you-want"],
43+
["ellar", "-p", "example_project", "whatever-you-want"],
4444
stdout=subprocess.PIPE,
4545
)
4646
assert result.returncode == 0
47-
assert result.stdout == b"Whatever you want command from example_project_2\n"
47+
assert result.stdout == b"Whatever you want command\n"
4848

4949
result = subprocess.run(
5050
["ellar", "-p", "example_project_2", "whatever-you-want"],
@@ -68,3 +68,11 @@ def test_help_command(cli_runner):
6868
["ellar", "-p", "example_project_2", "--help"], stdout=subprocess.PIPE
6969
)
7070
assert result.returncode == 0
71+
72+
73+
def test_click_command_works(cli_runner):
74+
os.chdir(sample_app_path)
75+
result = subprocess.run(["ellar", "say-hello"], stdout=subprocess.PIPE)
76+
assert result.returncode == 0
77+
78+
assert result.stdout == b"Hello from ellar.\n"

0 commit comments

Comments
 (0)