Skip to content

Commit 3be7d85

Browse files
committed
add typer tabs to basic tutorial
1 parent 3545496 commit 3be7d85

15 files changed

+394
-76
lines changed

django_typer/tests/test_poll_example.py

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import contextlib
22
from io import StringIO
3+
import pytest
4+
import sys
35

46
from django.core.management import call_command
57
from django.test import SimpleTestCase
@@ -18,13 +20,16 @@
1820
]
1921

2022

23+
@pytest.mark.skipif(sys.version_info < (3, 9), reason="requires python 3.9+")
2124
class TestPollExample(SimpleTestCase):
2225
q1 = None
2326
q2 = None
2427
q3 = None
2528

2629
databases = {"default"}
2730

31+
typer = ""
32+
2833
def setUp(self):
2934
self.q1 = Question.objects.create(
3035
question_text="Is Putin a war criminal?",
@@ -51,15 +56,18 @@ def test_poll_complete(self):
5156
result1 = StringIO()
5257
with contextlib.redirect_stdout(result1):
5358
call_command(
54-
"shellcompletion", "complete", shell=shell, cmd_str="closepoll "
59+
"shellcompletion",
60+
"complete",
61+
shell=shell,
62+
cmd_str=f"closepoll{self.typer} ",
5563
)
5664
result2 = StringIO()
5765
with contextlib.redirect_stdout(result2):
5866
call_command(
5967
"shellcompletion",
6068
"complete",
6169
shell=shell,
62-
cmd_str="./manage.py closepoll ",
70+
cmd_str=f"./manage.py closepoll{self.typer} ",
6371
)
6472

6573
result = result1.getvalue()
@@ -70,23 +78,23 @@ def test_poll_complete(self):
7078
self.assertTrue(q.question_text in result)
7179

7280
def test_tutorial1(self):
73-
result = run_command("closepoll_t1", str(self.q2.id))
81+
result = run_command(f"closepoll_t1{self.typer}", str(self.q2.id))
7482
self.assertFalse(result[1])
7583
self.assertTrue("Successfully closed poll" in result[0])
7684

7785
def test_tutorial2(self):
78-
result = run_command("closepoll_t2", str(self.q2.id))
86+
result = run_command(f"closepoll_t2{self.typer}", str(self.q2.id))
7987
self.assertFalse(result[1])
8088
self.assertTrue("Successfully closed poll" in result[0])
8189

8290
def test_tutorial_parser(self):
83-
result = run_command("closepoll_t3", str(self.q1.id))
91+
result = run_command(f"closepoll_t3{self.typer}", str(self.q1.id))
8492
self.assertFalse(result[1])
8593

8694
def test_tutorial_parser_cmd(self):
8795
log = StringIO()
88-
call_command("closepoll_t3", str(self.q1.id), stdout=log)
89-
cmd = get_command("closepoll_t3", stdout=log)
96+
call_command(f"closepoll_t3{self.typer}", str(self.q1.id), stdout=log)
97+
cmd = get_command(f"closepoll_t3{self.typer}", stdout=log)
9098
cmd([self.q1])
9199
cmd(polls=[self.q1])
92100
# these don't work, maybe revisit in future?
@@ -96,13 +104,18 @@ def test_tutorial_parser_cmd(self):
96104

97105
def test_tutorial_modelobjparser_cmd(self):
98106
log = StringIO()
99-
call_command("closepoll_t6", str(self.q1.id), stdout=log)
100-
cmd = get_command("closepoll_t6", stdout=log)
107+
call_command(f"closepoll_t6{self.typer}", str(self.q1.id), stdout=log)
108+
cmd = get_command(f"closepoll_t6{self.typer}", stdout=log)
101109
cmd([self.q1])
102110
cmd(polls=[self.q1])
103111
self.assertEqual(log.getvalue().count("Successfully"), 3)
104112

105113
def test_poll_ex(self):
106-
result = run_command("closepoll", str(self.q2.id))
114+
result = run_command(f"closepoll{self.typer}", str(self.q2.id))
107115
self.assertFalse(result[1])
108116
self.assertTrue("Successfully closed poll" in result[0])
117+
118+
119+
@pytest.mark.skipif(sys.version_info < (3, 9), reason="requires python 3.9+")
120+
class TestPollExampleTyper(SimpleTestCase):
121+
typer = "_typer"

doc/source/index.rst

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
Django Typer
66
============
77

8-
Use Typer_ to define the CLI for your Django_ management commands using the typer interface.
8+
Use Typer_ to define the CLI for your Django_ management commands using Python's static typing.
99
Optionally use the provided TyperCommand class that inherits from BaseCommand_. This class maps
1010
the typer interface onto a class based interface that Django developers will be familiar with.
1111
All of the BaseCommand functionality is preserved, so that TyperCommand can be a drop in
@@ -25,6 +25,7 @@ replacement.
2525
* Refactor existing management commands into TyperCommands because TyperCommand is interface
2626
compatible with BaseCommand.
2727
* Use either a class-based interface or the basic Typer style interface to define commands.
28+
* Add plugins to upstream commands.
2829

2930

3031
:big:`Installation`
@@ -59,9 +60,12 @@ replacement.
5960

6061
.. note::
6162

62-
This documentation shows all examples using both the function orientied Typer-style interface
63-
and the class based Django-style interface using tabs. Each interface is equivalent so the
64-
choice of which to use is a matter of preference.
63+
This documentation shows all examples using both the function oriented Typer-style interface
64+
and the class based Django-style interface in separate tabs. Each interface is functionally
65+
equivalent so the choice of which to use is a matter of preference and familiarity. All
66+
django-typer commands are instances of :class:`~django_typer.TyperCommand`, including commands
67+
defined in the Typer-style interface. **This means you may always specify a self argument to
68+
receive the instance of the command in your functions.**
6569

6670
:big:`Basic Example`
6771

@@ -141,8 +145,7 @@ command like so:
141145
142146
Any number of groups and subcommands and subgroups of other groups can be defined allowing for
143147
arbitrarily complex command hierarchies. The Typer-style interface builds a TyperCommand class for
144-
us. **This allows you to optionally accept the self argument in your commands.** We could define
145-
the above command using the typer interface like this:
148+
us **that allows you to optionally accept the self argument in your commands.**
146149

147150
.. tabs::
148151

doc/source/tutorial.rst

Lines changed: 63 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -94,19 +94,30 @@ looks like this:
9494
:replace:
9595
django_typer.tests.apps.examples.polls.models : polls.models
9696

97-
9897
Inherit from :class:`~django_typer.TyperCommand`
9998
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
10099

101100
We first need to change the inheritance to :class:`~django_typer.TyperCommand` and then move the
102101
argument and option definitions from add_arguments into the method signature of handle. A minimal
103102
conversion may look like this:
104103

105-
.. literalinclude:: ../../django_typer/tests/apps/examples/polls/management/commands/closepoll_t1.py
106-
:language: python
107-
:linenos:
108-
:replace:
109-
django_typer.tests.apps.examples.polls.models : polls.models
104+
.. tabs::
105+
106+
.. tab:: Django-style
107+
108+
.. literalinclude:: ../../django_typer/tests/apps/examples/polls/management/commands/closepoll_t1.py
109+
:language: python
110+
:linenos:
111+
:replace:
112+
django_typer.tests.apps.examples.polls.models : polls.models
113+
114+
.. tab:: Typer-style
115+
116+
.. literalinclude:: ../../django_typer/tests/apps/examples/polls/management/commands/closepoll_t1_typer.py
117+
:language: python
118+
:linenos:
119+
:replace:
120+
django_typer.tests.apps.examples.polls.models : polls.models
110121

111122
You'll note that we've removed add_arguments entirely and specified the arguments and options as
112123
parameters to the handle method. django-typer_ will interpret the parameters on the handle() method
@@ -139,12 +150,23 @@ with one of two Typer_ parameter types, either Argument or Option. Arguments_ ar
139150
parameters and Options_ are named parameters (i.e. `--delete`). In our polls example, the poll_ids
140151
are arguments and the delete flag is an option. Here is what that would look like:
141152

142-
.. literalinclude:: ../../django_typer/tests/apps/examples/polls/management/commands/closepoll_t2.py
143-
:language: python
144-
:lines: 17-29
145-
:replace:
146-
django_typer.tests.apps.examples.polls.models : polls.models
153+
.. tabs::
154+
155+
.. tab:: Django-style
156+
157+
.. literalinclude:: ../../django_typer/tests/apps/examples/polls/management/commands/closepoll_t2.py
158+
:language: python
159+
:lines: 11-23
160+
:replace:
161+
django_typer.tests.apps.examples.polls.models : polls.models
162+
163+
.. tab:: Typer-style
147164

165+
.. literalinclude:: ../../django_typer/tests/apps/examples/polls/management/commands/closepoll_t2_typer.py
166+
:language: python
167+
:lines: 13-23
168+
:replace:
169+
django_typer.tests.apps.examples.polls.models : polls.models
148170

149171
See that our help text now shows up in the command line interface. Also note, that
150172
`lazy translations <https://docs.djangoproject.com/en/stable/topics/i18n/translation/>`_ work for
@@ -176,11 +198,19 @@ duplicate our for loop that loads Poll objects from ids, but that wouldn't be ve
176198
Typer_ allows us to define custom parsers for arbitrary parameter types. Lets see what that would
177199
look like if we used the Poll class as our type hint:
178200

179-
.. literalinclude:: ../../django_typer/tests/apps/examples/polls/management/commands/closepoll_t3.py
180-
:language: python
181-
:linenos:
182-
:lines: 17-58
201+
.. tabs::
202+
203+
.. tab:: Django-style
204+
205+
.. literalinclude:: ../../django_typer/tests/apps/examples/polls/management/commands/closepoll_t3.py
206+
:language: python
207+
:lines: 11-53
183208

209+
.. tab:: Typer-style
210+
211+
.. literalinclude:: ../../django_typer/tests/apps/examples/polls/management/commands/closepoll_t3_typer.py
212+
:language: python
213+
:lines: 11-54
184214

185215
.. typer:: django_typer.tests.apps.examples.polls.management.commands.closepoll_t3.Command:typer_app
186216
:prog: manage.py closepoll
@@ -254,11 +284,24 @@ When we're using a :class:`~django_typer.parsers.ModelObjectParser` and
254284
of boiler plate. Let's put everything together and see what our full-featured refactored
255285
closepoll command looks like:
256286

257-
.. literalinclude:: ../../django_typer/tests/apps/examples/polls/management/commands/closepoll_t6.py
258-
:language: python
259-
:linenos:
260-
:replace:
261-
django_typer.tests.apps.examples.polls.models : polls.models
287+
.. tabs::
288+
289+
.. tab:: Django-style
290+
291+
.. literalinclude:: ../../django_typer/tests/apps/examples/polls/management/commands/closepoll_t6.py
292+
:language: python
293+
:linenos:
294+
:replace:
295+
django_typer.tests.apps.examples.polls.models : polls.models
296+
297+
.. tab:: Typer-style
298+
299+
.. literalinclude:: ../../django_typer/tests/apps/examples/polls/management/commands/closepoll_t6_typer.py
300+
:language: python
301+
:linenos:
302+
:replace:
303+
django_typer.tests.apps.examples.polls.models : polls.models
304+
262305

263306
.. only:: html
264307

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import typing as t
2+
3+
from django.core.management.base import CommandError
4+
5+
from django_typer import Typer
6+
from django_typer.tests.apps.examples.polls.models import Question as Poll
7+
8+
app = Typer(help="Closes the specified poll for voting")
9+
10+
11+
@app.command()
12+
def handle(
13+
self,
14+
poll_ids: t.List[int],
15+
delete: bool = False,
16+
):
17+
for poll_id in poll_ids:
18+
try:
19+
poll = Poll.objects.get(pk=poll_id)
20+
except Poll.DoesNotExist:
21+
raise CommandError(f'Poll "{poll_id}" does not exist')
22+
23+
poll.opened = False
24+
poll.save()
25+
26+
self.stdout.write(
27+
self.style.SUCCESS(f'Successfully closed poll "{poll.id}"')
28+
)
29+
30+
if delete:
31+
poll.delete()

examples/polls/management/commands/closepoll_t2.py

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,5 @@
1-
import sys
21
import typing as t
32

4-
if sys.version_info < (3, 9):
5-
from typing_extensions import Annotated
6-
else:
7-
from typing import Annotated
8-
93
from django.core.management.base import CommandError
104
from django.utils.translation import gettext_lazy as _
115
from typer import Argument, Option
@@ -19,11 +13,11 @@ class Command(TyperCommand):
1913

2014
def handle(
2115
self,
22-
poll_ids: Annotated[
16+
poll_ids: t.Annotated[
2317
t.List[int],
2418
Argument(help=_("The database IDs of the poll(s) to close.")),
2519
],
26-
delete: Annotated[
20+
delete: t.Annotated[
2721
bool, Option(help=_("Delete poll instead of closing it."))
2822
] = False,
2923
):
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import typing as t
2+
3+
from django.core.management.base import CommandError
4+
from django.utils.translation import gettext_lazy as _
5+
from typer import Argument, Option
6+
7+
from django_typer import Typer
8+
from django_typer.tests.apps.examples.polls.models import Question as Poll
9+
10+
app = Typer(help="Closes the specified poll for voting")
11+
12+
13+
@app.command()
14+
def handle(
15+
self,
16+
poll_ids: t.Annotated[
17+
t.List[int],
18+
Argument(help=_("The database IDs of the poll(s) to close.")),
19+
],
20+
delete: t.Annotated[
21+
bool, Option(help=_("Delete poll instead of closing it."))
22+
] = False,
23+
):
24+
for poll_id in poll_ids:
25+
try:
26+
poll = Poll.objects.get(pk=poll_id)
27+
except Poll.DoesNotExist:
28+
raise CommandError(f'Poll "{poll_id}" does not exist')
29+
30+
poll.opened = False
31+
poll.save()
32+
33+
self.stdout.write(
34+
self.style.SUCCESS(f'Successfully closed poll "{poll.id}"')
35+
)
36+
37+
if delete:
38+
poll.delete()

examples/polls/management/commands/closepoll_t3.py

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,5 @@
1-
import sys
21
import typing as t
32

4-
if sys.version_info < (3, 9):
5-
from typing_extensions import Annotated
6-
else:
7-
from typing import Annotated
8-
93
from django.core.management.base import CommandError
104
from django.utils.translation import gettext_lazy as _
115
from typer import Argument, Option
@@ -26,14 +20,14 @@ def get_poll_from_id(poll: t.Union[str, Poll]) -> Poll:
2620
class Command(TyperCommand):
2721
def handle(
2822
self,
29-
polls: Annotated[
23+
polls: t.Annotated[
3024
t.List[Poll],
3125
Argument(
3226
parser=get_poll_from_id,
3327
help=_("The database IDs of the poll(s) to close."),
3428
),
3529
],
36-
delete: Annotated[
30+
delete: t.Annotated[
3731
bool,
3832
Option(
3933
"--delete", # we can also get rid of that unnecessary --no-delete flag

0 commit comments

Comments
 (0)