Skip to content

Commit 7f8d1d4

Browse files
authored
fix(cli): pre parse launch arguments when cli command executed (#4161)
1 parent 91a303a commit 7f8d1d4

File tree

3 files changed

+59
-22
lines changed

3 files changed

+59
-22
lines changed

litestar/cli/_utils.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,10 @@ def __init__(
190190
) -> None:
191191
"""Init ``LitestarExtensionGroup``"""
192192
super().__init__(name=name, commands=commands, **attrs)
193+
193194
self._prepare_done = False
195+
self._preparsed_app_dir: str | None = None
196+
self._preparsed_app_path: Path | None = None
194197

195198
for entry_point in entry_points(group="litestar.commands"):
196199
command = entry_point.load()
@@ -205,7 +208,9 @@ def _prepare(self, ctx: Context) -> None:
205208
env: LitestarEnv | None = ctx.obj
206209
else:
207210
try:
208-
env = ctx.obj = LitestarEnv.from_env(ctx.params.get("app_path"), ctx.params.get("app_dir"))
211+
app_path = ctx.params.get("app_path", self._preparsed_app_path)
212+
app_dir = ctx.params.get("app_dir", self._preparsed_app_dir)
213+
env = ctx.obj = LitestarEnv.from_env(app_path, app_dir)
209214
except LitestarCLIException:
210215
env = None
211216

@@ -226,6 +231,23 @@ def make_context( # type: ignore[override]
226231
self._prepare(ctx)
227232
return ctx
228233

234+
def parse_args(self, ctx: Context, args: list[str]) -> list[str]:
235+
"""Preparse launch arguments and save app_path & app_dir to slots.
236+
This block is triggered in any case, but its results are only used if the --help command is invoked.
237+
"""
238+
parser = self.make_parser(ctx)
239+
240+
original_ignore_unknown_option = ctx.ignore_unknown_options
241+
ctx.ignore_unknown_options = True
242+
243+
opts, remaining_args, order = parser.parse_args(list(args))
244+
self._preparsed_app_path = opts.get("app_path", None)
245+
self._preparsed_app_dir = opts.get("app_dir", None)
246+
247+
ctx.ignore_unknown_options = original_ignore_unknown_option
248+
249+
return super().parse_args(ctx, args)
250+
229251
def list_commands(self, ctx: Context) -> list[str]:
230252
self._prepare(ctx)
231253
return super().list_commands(ctx)

tests/unit/test_cli/conftest.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
if TYPE_CHECKING:
2626
from unittest.mock import MagicMock
2727

28-
from litestar.cli._utils import LitestarGroup
28+
from litestar.cli._utils import LitestarExtensionGroup
2929

3030

3131
@pytest.fixture(autouse=True)
@@ -34,10 +34,10 @@ def reset_litestar_app_env(monkeypatch: MonkeyPatch) -> None:
3434

3535

3636
@pytest.fixture()
37-
def root_command() -> LitestarGroup:
37+
def root_command() -> LitestarExtensionGroup:
3838
import litestar.cli.main
3939

40-
return cast("LitestarGroup", importlib.reload(litestar.cli.main).litestar_group)
40+
return cast("LitestarExtensionGroup", importlib.reload(litestar.cli.main).litestar_group)
4141

4242

4343
@pytest.fixture
Lines changed: 33 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,43 @@
1-
import textwrap
2-
31
from click.testing import CliRunner
42

5-
from litestar.cli._utils import LitestarGroup
3+
from litestar.cli._utils import LitestarExtensionGroup
64
from tests.unit.test_cli.conftest import CreateAppFileFixture
75

6+
APPLICATION_WITH_CLI_PLUGIN = """
7+
from litestar import Litestar
8+
from litestar.plugins import CLIPluginProtocol
9+
10+
class CLIPlugin(CLIPluginProtocol):
11+
def on_cli_init(self, cli):
12+
@cli.command()
13+
def mycommand(app: Litestar):
14+
\"\"\"Description of plugin command\"\"\"
15+
print(f"App is loaded: {app is not None}")
816
9-
def test_basic_command(runner: CliRunner, create_app_file: CreateAppFileFixture, root_command: LitestarGroup) -> None:
10-
app_file_content = textwrap.dedent(
11-
"""
12-
from litestar import Litestar
13-
from litestar.plugins import CLIPluginProtocol
17+
app = Litestar(plugins=[CLIPlugin()])
18+
"""
1419

15-
class CLIPlugin(CLIPluginProtocol):
16-
def on_cli_init(self, cli):
17-
@cli.command()
18-
def foo(app: Litestar):
19-
print(f"App is loaded: {app is not None}")
2020

21-
app = Litestar(plugins=[CLIPlugin()])
22-
"""
23-
)
24-
app_file = create_app_file("command_test_app.py", content=app_file_content)
25-
result = runner.invoke(root_command, ["--app", f"{app_file.stem}:app", "foo"])
21+
def test_basic_command(
22+
runner: CliRunner,
23+
create_app_file: CreateAppFileFixture,
24+
root_command: LitestarExtensionGroup,
25+
) -> None:
26+
app_file = create_app_file("command_test_app.py", content=APPLICATION_WITH_CLI_PLUGIN)
27+
result = runner.invoke(root_command, ["--app", f"{app_file.stem}:app", "mycommand"])
2628

2729
assert not result.exception
2830
assert "App is loaded: True" in result.output
31+
32+
33+
def test_plugin_command_appears_in_help_message(
34+
runner: CliRunner,
35+
create_app_file: CreateAppFileFixture,
36+
root_command: LitestarExtensionGroup,
37+
) -> None:
38+
app_file = create_app_file("command_test_app.py", content=APPLICATION_WITH_CLI_PLUGIN)
39+
result = runner.invoke(root_command, ["--app", f"{app_file.stem}:app", "--help"])
40+
41+
assert not result.exception
42+
assert "mycommand" in result.output
43+
assert "Description of plugin command" in result.output

0 commit comments

Comments
 (0)