Skip to content

Commit bb5fabb

Browse files
committed
fixe for #42
1 parent f809620 commit bb5fabb

File tree

6 files changed

+214
-6
lines changed

6 files changed

+214
-6
lines changed

django_typer/__init__.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@
114114
from typing import ParamSpec
115115

116116

117-
VERSION = (1, 0, 4)
117+
VERSION = (1, 0, 5)
118118

119119
__title__ = "Django Typer"
120120
__version__ = ".".join(str(i) for i in VERSION)
@@ -402,6 +402,27 @@ class Converter:
402402
infrastructure the conversion happens.
403403
"""
404404

405+
def get_params(self, ctx: click.Context) -> t.List[click.Parameter]:
406+
"""
407+
We override get_params to check to make sure that prompt_required is not set for parameters
408+
that have already been prompted for during the initial parse phase. We have to do this
409+
because of we're stuffing the click infrastructure into the django infrastructure and the
410+
django infrastructure forces a two step parse process whereas click does not easily support
411+
separating these.
412+
413+
There may be a more sound approach than this?
414+
"""
415+
modified = []
416+
params = super().get_params(ctx)
417+
for param in params:
418+
if getattr(param, "prompt_required", None) and getattr(
419+
ctx, "supplied_params", {}
420+
).get(param.name, None):
421+
param = deepcopy(param)
422+
setattr(param, "prompt_required", False)
423+
modified.append(param)
424+
return modified
425+
405426
def shell_complete(
406427
self, ctx: click.Context, incomplete: str
407428
) -> t.List[CompletionItem]:
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import sys
2+
import typing as t
3+
4+
if sys.version_info < (3, 9):
5+
from typing_extensions import Annotated
6+
else:
7+
from typing import Annotated
8+
9+
from django.utils.translation import gettext_lazy as _
10+
from typer import Option
11+
12+
from django_typer import TyperCommand, command, group
13+
14+
15+
class Command(TyperCommand):
16+
17+
help = _("Test password prompt")
18+
19+
@command()
20+
def cmd1(
21+
self,
22+
username: str,
23+
password: Annotated[
24+
t.Optional[str], Option("-p", hide_input=True, prompt=True)
25+
] = None,
26+
):
27+
return f"{username} {password}"
28+
29+
@command()
30+
def cmd2(
31+
self,
32+
username: str,
33+
password: Annotated[
34+
t.Optional[str],
35+
Option("-p", hide_input=True, prompt=True, prompt_required=False),
36+
] = None,
37+
):
38+
return f"{username} {password}"
39+
40+
@command()
41+
def cmd3(
42+
self,
43+
username: str,
44+
password: Annotated[
45+
str, Option("-p", hide_input=True, prompt=True, prompt_required=False)
46+
] = "default",
47+
):
48+
return f"{username} {password}"
49+
50+
@group()
51+
def group1(
52+
self,
53+
flag: Annotated[
54+
str, Option("-f", hide_input=True, prompt=True, prompt_required=True)
55+
],
56+
):
57+
self.flag = flag
58+
59+
@group1.command()
60+
def cmd4(
61+
self,
62+
username: str,
63+
password: Annotated[
64+
t.Optional[str], Option("-p", hide_input=True, prompt=True)
65+
] = None,
66+
):
67+
return f"{self.flag} {username} {password}"

django_typer/tests/tests.py

Lines changed: 106 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from typing import Any, Tuple
1212

1313
import django
14+
import pexpect
1415
import pytest
1516
import typer
1617
from click.exceptions import UsageError
@@ -62,7 +63,20 @@ def get_named_arguments(function):
6263
]
6364

6465

65-
def run_command(command, *args, parse_json=True) -> Tuple[str, str]:
66+
def interact(command, *args, **kwargs):
67+
cwd = os.getcwd()
68+
try:
69+
os.chdir(manage_py.parent)
70+
return pexpect.spawn(
71+
" ".join([sys.executable, f"./{manage_py.name}", command, *args]),
72+
env=os.environ,
73+
**kwargs,
74+
)
75+
finally:
76+
os.chdir(cwd)
77+
78+
79+
def run_command(command, *args, parse_json=True, **kwargs) -> Tuple[str, str]:
6680
# we want to use the same test database that was created for the test suite run
6781
cwd = os.getcwd()
6882
try:
@@ -73,6 +87,7 @@ def run_command(command, *args, parse_json=True) -> Tuple[str, str]:
7387
capture_output=True,
7488
text=True,
7589
env=env,
90+
**kwargs,
7691
)
7792

7893
# Check the return code to ensure the script ran successfully
@@ -2753,3 +2768,93 @@ def test_handle_as_init_direct(self):
27532768
self.assertEqual(get_command("handle_as_init")(), "handle")
27542769
self.assertEqual(get_command("handle_as_init", "subcommand")(), "subcommand")
27552770
self.assertEqual(get_command("handle_as_init").subcommand(), "subcommand")
2771+
2772+
2773+
class TestPromptOptions(TestCase):
2774+
2775+
def test_run_with_option_prompt(self):
2776+
2777+
cmd = interact("prompt", "--no-color", "cmd1", "bckohan")
2778+
cmd.expect("Password: ")
2779+
cmd.sendline("test_password")
2780+
2781+
result = cmd.read().decode("utf-8").strip().splitlines()[0]
2782+
self.assertEqual(result, "bckohan test_password")
2783+
2784+
cmd = interact("prompt", "--no-color", "cmd2", "bckohan")
2785+
result = cmd.read().decode("utf-8").strip().splitlines()[0]
2786+
self.assertEqual(result, "bckohan None")
2787+
2788+
cmd = interact("prompt", "--no-color", "cmd2", "bckohan", "-p")
2789+
cmd.expect("Password: ")
2790+
cmd.sendline("test_password2")
2791+
result = cmd.read().decode("utf-8").strip().splitlines()[0]
2792+
self.assertEqual(result, "bckohan test_password2")
2793+
2794+
cmd = interact("prompt", "--no-color", "cmd3", "bckohan")
2795+
result = cmd.read().decode("utf-8").strip().splitlines()[0]
2796+
self.assertEqual(result, "bckohan default")
2797+
2798+
cmd = interact("prompt", "--no-color", "cmd3", "bckohan", "-p")
2799+
cmd.expect(r"Password \[default\]: ")
2800+
cmd.sendline("test_password3")
2801+
result = cmd.read().decode("utf-8").strip().splitlines()[0]
2802+
self.assertEqual(result, "bckohan test_password3")
2803+
2804+
cmd = interact("prompt", "--no-color", "group1", "cmd4", "bckohan")
2805+
cmd.expect(r"Flag: ")
2806+
cmd.sendline("test_flag")
2807+
cmd.expect(r"Password: ")
2808+
cmd.sendline("test_password4")
2809+
result = cmd.read().decode("utf-8").strip().splitlines()[0]
2810+
self.assertEqual(result, "test_flag bckohan test_password4")
2811+
2812+
def test_call_with_option_prompt(self):
2813+
self.assertEqual(
2814+
call_command(
2815+
"prompt", "--no-color", "cmd1", "bckohan", password="test_password"
2816+
),
2817+
"bckohan test_password",
2818+
)
2819+
2820+
self.assertEqual(
2821+
call_command("prompt", "--no-color", "cmd2", "bckohan"), "bckohan None"
2822+
)
2823+
2824+
self.assertEqual(
2825+
call_command(
2826+
"prompt", "--no-color", "cmd2", "bckohan", "-p", "test_password2"
2827+
),
2828+
"bckohan test_password2",
2829+
)
2830+
2831+
self.assertEqual(
2832+
call_command("prompt", "--no-color", "cmd3", "bckohan"), "bckohan default"
2833+
)
2834+
2835+
self.assertEqual(
2836+
call_command(
2837+
"prompt", "--no-color", "cmd3", "bckohan", password="test_password3"
2838+
),
2839+
"bckohan test_password3",
2840+
)
2841+
2842+
self.assertEqual(
2843+
call_command(
2844+
"prompt",
2845+
"--no-color",
2846+
"group1",
2847+
"-f",
2848+
"test_flag",
2849+
"cmd4",
2850+
"bckohan",
2851+
password="test_password4",
2852+
),
2853+
"test_flag bckohan test_password4",
2854+
)
2855+
2856+
# this doesn't work!
2857+
# self.assertEqual(
2858+
# call_command('prompt', '--no-color', 'group1', 'cmd4', 'bckohan', flag='test_flag', password='test_password4'),
2859+
# 'test_flag bckohan test_password4'
2860+
# )

django_typer/tests/typer_test.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,26 @@
11
#!/usr/bin/env python
2+
import typing as t
3+
24
import typer
35

46
app = typer.Typer(name="test")
57
state = {"verbose": False}
68

79

810
@app.command()
9-
def create(username: str, flag: bool = False):
11+
def create(
12+
username: str,
13+
password: t.Annotated[
14+
t.Optional[str],
15+
typer.Option("--password", hide_input=True, prompt=True),
16+
] = None,
17+
):
1018
if state["verbose"]:
1119
print("About to create a user")
1220
print(f"Creating user: {username}")
1321
if state["verbose"]:
1422
print("Just created a user")
15-
print(f"flag: {flag}")
23+
print(f"password: {password}")
1624

1725

1826
@app.command(epilog="Delete Epilog")
@@ -25,7 +33,7 @@ def delete(username: str):
2533

2634

2735
@app.callback(epilog="Main Epilog")
28-
def main(arg: int, verbose: bool = False):
36+
def main(verbose: bool = False):
2937
"""
3038
Manage users in the awesome CLI app.
3139
"""

doc/source/changelog.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22
Change Log
33
==========
44

5+
v1.0.5
6+
======
7+
8+
* Fixed `Options with prompt=True are prompted twice <https://github.com/bckohan/django-typer/issues/42>`_
9+
10+
511
v1.0.4
612
======
713

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "django-typer"
3-
version = "1.0.4"
3+
version = "1.0.5"
44
description = "Use Typer to define the CLI for your Django management commands."
55
authors = ["Brian Kohan <[email protected]>"]
66
license = "MIT"
@@ -87,6 +87,7 @@ scipy = [
8787
{ version = "<=1.10", markers = "python_version <= '3.8'" },
8888
]
8989
django-stubs = "^4.2.7"
90+
pexpect = "^4.9.0"
9091

9192
[tool.poetry.extras]
9293
rich = ["rich"]

0 commit comments

Comments
 (0)