Skip to content

Commit d626492

Browse files
authored
Merge pull request #38 from python-ellar/support_click_commands
Added support for click commands
2 parents 599b8f0 + b209f18 commit d626492

File tree

7 files changed

+92
-20
lines changed

7 files changed

+92
-20
lines changed

README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
<p align="center">
2-
<a href="#" target="blank"><img src="https://eadwincode.github.io/ellar/img/EllarLogoB.png" width="200" alt="Ellar Logo" /></a>
2+
<a href="#" target="blank"><img src="https://python-ellar.github.io/ellar/img/EllarLogoB.png" width="200" alt="Ellar Logo" /></a>
33
</p>
44

55
<p align="center"> Ellar CLI Tool for Scaffolding Ellar Projects and Modules and also running Ellar Commands</p>
66

7-
![Test](https://github.com/eadwinCode/ellar-cli/actions/workflows/test_full.yml/badge.svg)
8-
![Coverage](https://img.shields.io/codecov/c/github/eadwinCode/ellar-cli)
7+
![Test](https://github.com/python-ellar/ellar-cli/actions/workflows/test_full.yml/badge.svg)
8+
![Coverage](https://img.shields.io/codecov/c/github/python-ellar/ellar-cli)
99
[![PyPI version](https://badge.fury.io/py/ellar-cli.svg)](https://badge.fury.io/py/ellar-cli)
1010
[![PyPI version](https://img.shields.io/pypi/v/ellar-cli.svg)](https://pypi.python.org/pypi/ellar-cli)
1111
[![PyPI version](https://img.shields.io/pypi/pyversions/ellar-cli.svg)](https://pypi.python.org/pypi/ellar-cli)
@@ -15,7 +15,7 @@ Ellar CLI is an abstracted tool for the Ellar web framework that helps in the st
1515
framework, module project scaffold, running the project local server using UVICORN, and running custom commands registered in the application module or any Ellar module.
1616

1717
## Installation
18-
if you have [ellar](https://github.com/eadwinCode/ellar) install ready
18+
if you have [ellar](https://github.com/python-ellar/ellar) install ready
1919
```
2020
pip install ellar-cli
2121
```
@@ -47,4 +47,4 @@ Commands:
4747
4848
```
4949

50-
Full Documentation: [Here](https://eadwincode.github.io/ellar/commands)
50+
Full Documentation: [Here](https://python-ellar.github.io/ellar/cli/introduction/)

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)