Skip to content

Commit 8ce9f5a

Browse files
committed
add typer versions of extensions tutorial
1 parent 91d2eb0 commit 8ce9f5a

File tree

9 files changed

+346
-38
lines changed

9 files changed

+346
-38
lines changed

django_typer/tests/test_backup_example.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
class TestBackupExample(SimpleTestCase):
1717
databases = {"default"}
1818

19+
typer = ""
20+
1921
def setUp(self):
2022
if BACKUP_DIRECTORY.exists():
2123
shutil.rmtree(BACKUP_DIRECTORY)
@@ -28,7 +30,7 @@ def tearDown(self) -> None:
2830

2931
def test_base_backup(self):
3032
stdout, stderr, retcode = run_command(
31-
"backup",
33+
f"backup{self.typer}",
3234
"--no-color",
3335
"--settings",
3436
"django_typer.tests.settings.backup",
@@ -43,7 +45,7 @@ def test_base_backup(self):
4345
)
4446

4547
stdout, stderr, retcode = run_command(
46-
"backup",
48+
f"backup{self.typer}",
4749
"--settings",
4850
"django_typer.tests.settings.backup",
4951
"-o",
@@ -56,7 +58,7 @@ def test_base_backup(self):
5658

5759
def test_inherit_backup(self):
5860
stdout, stderr, retcode = run_command(
59-
"backup",
61+
f"backup{self.typer}",
6062
"--no-color",
6163
"--settings",
6264
"django_typer.tests.settings.backup_inherit",
@@ -71,7 +73,7 @@ def test_inherit_backup(self):
7173
self.assertTrue("media(filename=media.tar.gz)" in lines)
7274

7375
stdout, stderr, retcode = run_command(
74-
"backup",
76+
f"backup{self.typer}",
7577
"--settings",
7678
"django_typer.tests.settings.backup_inherit",
7779
"-o",
@@ -85,7 +87,7 @@ def test_inherit_backup(self):
8587

8688
def test_extend_backup(self):
8789
stdout, stderr, retcode = run_command(
88-
"backup",
90+
f"backup{self.typer}",
8991
"--no-color",
9092
"--settings",
9193
"django_typer.tests.settings.backup_ext",
@@ -99,7 +101,7 @@ def test_extend_backup(self):
99101
self.assertTrue("media(filename=media.tar.gz)" in lines)
100102

101103
stdout, stderr, retcode = run_command(
102-
"backup",
104+
f"backup{self.typer}",
103105
"--settings",
104106
"django_typer.tests.settings.backup_ext",
105107
"-o",
@@ -113,3 +115,8 @@ def test_extend_backup(self):
113115
self.assertTrue((BACKUP_DIRECTORY / "media.tar.gz").exists())
114116
self.assertTrue((BACKUP_DIRECTORY / "requirements.txt").exists())
115117
self.assertTrue(len(os.listdir(BACKUP_DIRECTORY)) == 3)
118+
119+
120+
@pytest.mark.skipif(sys.version_info < (3, 9), reason="requires python3.9 or higher")
121+
class TestBackupTyperExample(TestBackupExample):
122+
typer = "_typer"

doc/source/extensions.rst

Lines changed: 72 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,26 @@ to determine if a subcommand was called in our root initializer callback and we
3030
subroutines added by extensions at runtime using :func:`~django_typer.TyperCommand.get_subcommand`.
3131
Our command might look like this:
3232

33+
.. tabs::
34+
35+
.. tab:: Django-style
36+
37+
.. literalinclude:: ../../django_typer/tests/apps/examples/extensions/backup/management/commands/backup.py
38+
:language: python
39+
:caption: backup/management/commands/backup.py
40+
:linenos:
41+
:replace:
42+
django_typer.tests.apps.examples.extensions.backup : backup
43+
44+
.. tab:: Typer-style
45+
46+
.. literalinclude:: ../../django_typer/tests/apps/examples/extensions/backup/management/commands/backup_typer.py
47+
:language: python
48+
:caption: backup/management/commands/backup.py
49+
:linenos:
50+
:replace:
51+
django_typer.tests.apps.examples.extensions.backup : backup
3352

34-
.. literalinclude:: ../../django_typer/tests/apps/examples/extensions/backup/management/commands/backup.py
35-
:language: python
36-
:caption: Base Backup Command
37-
:linenos:
3853

3954
.. typer:: django_typer.tests.apps.examples.extensions.backup.management.commands.backup.Command:typer_app
4055
:prog: manage.py backup
@@ -92,11 +107,23 @@ Say our app tree looks like this:
92107
93108
Our backup.py implementation in the media app might look like this:
94109

95-
.. literalinclude:: ../../django_typer/tests/apps/examples/extensions/media1/management/commands/backup.py
96-
:language: python
97-
:caption: Media Backup Extension
98-
:replace:
99-
django_typer.tests.apps.examples.extensions.backup : media
110+
.. tabs::
111+
112+
.. tab:: Django-style
113+
114+
.. literalinclude:: ../../django_typer/tests/apps/examples/extensions/media1/management/commands/backup.py
115+
:language: python
116+
:caption: media/management/commands/backup.py
117+
:replace:
118+
django_typer.tests.apps.examples.extensions.media1 : media
119+
120+
.. tab:: Typer-style
121+
122+
.. literalinclude:: ../../django_typer/tests/apps/examples/extensions/media1/management/commands/backup_typer.py
123+
:language: python
124+
:caption: media/management/commands/backup.py
125+
:replace:
126+
django_typer.tests.apps.examples.extensions.media1 : media
100127

101128
Now you'll see we have another command called media available:
102129

@@ -221,19 +248,45 @@ this:
221248
For plugins to work, we'll need to re-mplement media from above as a composed extension
222249
and that would look like this:
223250

224-
.. literalinclude:: ../../django_typer/tests/apps/examples/extensions/media2/management/extensions/backup.py
225-
:language: python
226-
:caption: Media Backup Extension
227-
:replace:
228-
django_typer.tests.apps.examples.extensions.backup : backup
251+
.. tabs::
252+
253+
.. tab:: django-typer
254+
255+
.. literalinclude:: ../../django_typer/tests/apps/examples/extensions/media2/management/extensions/backup.py
256+
:language: python
257+
:caption: media/management/extensions/backup.py
258+
:replace:
259+
django_typer.tests.apps.examples.extensions.media2 : media
260+
261+
.. tab:: Typer-style
262+
263+
.. literalinclude:: ../../django_typer/tests/apps/examples/extensions/media2/management/extensions/backup_typer.py
264+
:language: python
265+
:caption: media/management/extensions/backup.py
266+
:replace:
267+
django_typer.tests.apps.examples.extensions.media2 : media
268+
269+
229270

230271
And our my_app extension might look like this:
231272

232-
.. literalinclude:: ../../django_typer/tests/apps/examples/extensions/my_app/management/extensions/backup.py
233-
:language: python
234-
:caption: MyApp Backup Extension
235-
:replace:
236-
django_typer.tests.apps.examples.extensions.backup : backup
273+
.. tabs::
274+
275+
.. tab:: Django-style
276+
277+
.. literalinclude:: ../../django_typer/tests/apps/examples/extensions/my_app/management/extensions/backup.py
278+
:language: python
279+
:caption: my_app/management/extensions/backup.py
280+
:replace:
281+
django_typer.tests.apps.examples.extensions.my_app : my_app
282+
283+
.. tab:: Typer-style
284+
285+
.. literalinclude:: ../../django_typer/tests/apps/examples/extensions/my_app/management/extensions/backup_typer.py
286+
:language: python
287+
:caption: my_app/management/extensions/backup.py
288+
:replace:
289+
django_typer.tests.apps.examples.extensions.my_app : my_app
237290

238291
Note that we now have a new environment command available:
239292

doc/source/tutorial.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ look like if we used the Poll class as our type hint:
204204

205205
.. literalinclude:: ../../django_typer/tests/apps/examples/polls/management/commands/closepoll_t3.py
206206
:language: python
207-
:lines: 11-53
207+
:lines: 11-52
208208

209209
.. tab:: Typer-style
210210

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import inspect
2+
import os
3+
import typing as t
4+
from pathlib import Path
5+
6+
import typer
7+
from django.conf import settings
8+
from django.core.management import CommandError, call_command
9+
10+
from django_typer import CommandNode, Typer, completers
11+
12+
app = Typer()
13+
14+
Command.suppressed_base_arguments = {"verbosity", "skip_checks"}
15+
Command.requires_migrations_checks = False
16+
Command.requires_system_checks = []
17+
18+
databases = [alias for alias in settings.DATABASES.keys()]
19+
20+
21+
@app.callback(invoke_without_command=True)
22+
def init_or_run_all(
23+
self,
24+
# if we add a context argument Typer will provide it
25+
context: typer.Context,
26+
output_directory: t.Annotated[
27+
Path,
28+
typer.Option(
29+
"-o",
30+
"--output",
31+
shell_complete=completers.complete_directory,
32+
help="The directory to write backup artifacts to.",
33+
),
34+
] = Path(os.getcwd()),
35+
):
36+
"""
37+
Backup the website! This command groups backup routines together.
38+
Each routine may be run individually, but if no routine is specified,
39+
the default run of all routines will be executed.
40+
"""
41+
self.output_directory = output_directory
42+
43+
if not self.output_directory.exists():
44+
self.output_directory.mkdir(parents=True)
45+
46+
if not self.output_directory.is_dir():
47+
raise CommandError(f"{self.output_directory} is not a directory.")
48+
49+
# here we use the context to determine if a subcommand was invoked and
50+
# if it was not we run all the backup routines
51+
if not context.invoked_subcommand:
52+
for cmd in get_backup_routines(self):
53+
cmd()
54+
55+
56+
@app.command()
57+
def list(self):
58+
"""
59+
List the default backup routines in the order they will be run.
60+
"""
61+
self.echo("Default backup routines:")
62+
for cmd in get_backup_routines(self):
63+
sig = {
64+
name: param.default
65+
for name, param in inspect.signature(
66+
cmd.callback
67+
).parameters.items()
68+
if not name == "self"
69+
}
70+
params = ", ".join([f"{k}={v}" for k, v in sig.items()])
71+
self.secho(f" {cmd.name}({params})", fg="green")
72+
73+
74+
@app.command()
75+
def database(
76+
self,
77+
filename: t.Annotated[
78+
str,
79+
typer.Option(
80+
"-f",
81+
"--filename",
82+
help=(
83+
"The name of the file to use for the backup fixture. The "
84+
"filename may optionally contain a {database} formatting "
85+
"placeholder."
86+
),
87+
),
88+
] = "{database}.json",
89+
databases: t.Annotated[
90+
t.Optional[t.List[str]],
91+
typer.Option(
92+
"-d",
93+
"--database",
94+
help=(
95+
"The name of the database(s) to backup. If not provided, "
96+
"all databases will be backed up."
97+
),
98+
shell_complete=completers.databases,
99+
),
100+
] = databases,
101+
):
102+
"""
103+
Backup database(s) to a json fixture file.
104+
"""
105+
for db in databases or self.databases:
106+
output = self.output_directory / filename.format(database=db)
107+
self.echo(f"Backing up database [{db}] to: {output}")
108+
call_command(
109+
"dumpdata",
110+
output=output,
111+
database=db,
112+
format="json",
113+
)
114+
115+
116+
def get_backup_routines(command) -> t.List[CommandNode]:
117+
"""
118+
Return the list of backup subcommands. This is every registered command
119+
except for the list command.
120+
"""
121+
# fetch all the command names at the top level of our command tree,
122+
# except for list, which we know to not be a backup routine
123+
return [
124+
cmd
125+
for name, cmd in command.get_subcommand().children.items()
126+
if name != "list"
127+
]
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import tarfile
2+
import typing as t
3+
from pathlib import Path
4+
5+
import typer
6+
from django.conf import settings
7+
8+
from django_typer import Typer
9+
from django_typer.tests.apps.examples.extensions.backup.management.commands import (
10+
backup_typer,
11+
)
12+
13+
app = Typer(backup_typer.app)
14+
15+
16+
@app.command()
17+
def media(
18+
self,
19+
filename: t.Annotated[
20+
str,
21+
typer.Option(
22+
"-f",
23+
"--filename",
24+
help=("The name of the file to use for the media backup tar."),
25+
),
26+
] = "media.tar.gz",
27+
):
28+
"""
29+
Backup the media files (i.e. those files in MEDIA_ROOT).
30+
"""
31+
media_root = Path(settings.MEDIA_ROOT)
32+
output_file = self.output_directory / filename
33+
34+
# backup the media directory into the output file as a gzipped tar
35+
typer.echo(f"Backing up {media_root} to {output_file}")
36+
with tarfile.open(output_file, "w:gz") as tar:
37+
tar.add(media_root, arcname=media_root.name)
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import tarfile
2+
import typing as t
3+
from pathlib import Path
4+
5+
import typer
6+
from django.conf import settings
7+
8+
from django_typer.tests.apps.examples.extensions.backup.management.commands import (
9+
backup_typer,
10+
)
11+
12+
13+
@backup_typer.app.command()
14+
def media(
15+
# self is optional, but if you want to access the command instance, you
16+
# can specify it
17+
self,
18+
filename: t.Annotated[
19+
str,
20+
typer.Option(
21+
"-f",
22+
"--filename",
23+
help=("The name of the file to use for the media backup tar."),
24+
),
25+
] = "media.tar.gz",
26+
):
27+
"""
28+
Backup the media files (i.e. those files in MEDIA_ROOT).
29+
"""
30+
media_root = Path(settings.MEDIA_ROOT)
31+
output_file = self.output_directory / filename
32+
33+
# backup the media directory into the output file as a gzipped tar
34+
typer.echo(f"Backing up {media_root} to {output_file}")
35+
with tarfile.open(output_file, "w:gz") as tar:
36+
tar.add(media_root, arcname=media_root.name)

0 commit comments

Comments
 (0)