Skip to content

Commit 578f5da

Browse files
committed
touch up extensions tutorial and provide typer style example code
1 parent 62a1496 commit 578f5da

File tree

6 files changed

+104
-37
lines changed

6 files changed

+104
-37
lines changed

doc/source/extensions.rst

Lines changed: 96 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,12 @@ Our command might look like this:
6060

6161
|
6262
63+
.. code-block:: console
64+
65+
$> python manage.py backup list
66+
Default backup routines:
67+
database(filename={database}.json, databases=['default'])
68+
6369
.. _inheritance:
6470

6571
Inheritance
@@ -72,8 +78,8 @@ files to our site. This means we'll want to add a media backup routine to the ba
7278
.. note::
7379

7480
Inheritance also works for commands defined using the Typer-style function based interface.
75-
Just import Command from the upstream command module and subclass it just as you would if you
76-
had defined it using the class based interface.
81+
Import the root Typer_ app from the upstream command module and pass it as an argument
82+
to Typer_ when you create the root app in your overriding command module.
7783

7884
Say our app tree looks like this:
7985

@@ -147,10 +153,18 @@ backup batch:
147153

148154
.. code-block:: bash
149155
156+
$> python manage.py backup list
157+
Default backup routines:
158+
database(filename={database}.json, databases=['default'])
159+
media(filename=media.tar.gz)
150160
# backup media only
151-
$ python manage.py backup media
161+
$> python manage.py backup media
162+
Backing up ./media to ./media.tar.gz
152163
# or backup database and media
153-
$ python manage.py backup
164+
$> python manage.py backup
165+
Backing up database [default] to: ./default.json
166+
[.............................................]
167+
Backing up ./media to ./media.tar.gz
154168
155169
.. warning::
156170

@@ -327,41 +341,93 @@ Overriding Groups
327341
~~~~~~~~~~~~~~~~~
328342

329343
Some commands might have deep nesting of subcommands and groups. If you want to override a
330-
group or subcommand of a group down a chain of commands you simply need to access the
331-
:class:`~django_typer.CommandGroup` instance of the group you want to override or extend:
344+
group or subcommand of a group down a chain of commands you would need to access the
345+
:class:`~django_typer.Typer` instance of the group you want to override or extend:
332346

333-
.. code-block:: python
347+
.. tabs::
348+
349+
.. tab:: Django-style
350+
351+
.. code-block:: python
352+
353+
from somewhere.upstream.management.commands.command import Command
354+
355+
# add a command to grp2 which is a subgroup of grp1
356+
@Command.grp1.grp2.command()
357+
def my_command(): # remember self is optional
358+
pass
359+
360+
# add a subgroup to grp2 which is a subgroup of grp1
361+
@Command.grp1.grp2.group()
362+
def grp3():
363+
pass
364+
365+
.. tab:: Typer-style
366+
367+
.. code-block:: python
334368
335-
from somewhere.upstream.management.commands.command import Command
369+
from somewhere.upstream.management.commands.command import app
336370
337-
# add a command to grp2 which is a subgroup of grp1
338-
@Command.grp1.grp2.command()
339-
def my_command(): # remember self is optional
340-
pass
371+
# add a command to grp2 which is a subgroup of grp1
372+
@app.grp1.grp2.command()
373+
def my_command(): # remember self is optional
374+
pass
341375
342-
# add a subgroup to grp2 which is a subgroup of grp1
343-
@Command.grp1.grp2.group()
344-
def grp3():
345-
pass
376+
# add a subgroup to grp2 which is a subgroup of grp1
377+
@app.grp1.grp2.group()
378+
def grp3():
379+
pass
346380
347381
You may even override the initializer of a predefined group:
348382

349-
.. code-block:: python
383+
.. tabs::
384+
385+
.. tab:: Django-style
386+
387+
.. code-block:: python
388+
389+
from somewhere.upstream.management.commands.command import Command
390+
391+
# override the initializer (typer callback) of grp1 on Command,
392+
# this will not alter the child groups of grp1 (grp2, grp3, etc.)
393+
@Command.grp1.initialize()
394+
def grp1_init(self):
395+
pass
396+
397+
@Command.group()
398+
def grp1(self):
399+
"""
400+
This would override grp1 entirely and remove all subcommands
401+
and groups.
402+
"""
403+
404+
.. tab:: Typer-style
405+
406+
.. code-block:: python
407+
408+
from somewhere.upstream.management.commands.command import app
409+
410+
# override the initializer (typer callback) of grp1 on app,
411+
# this will not alter the child groups of grp1 (grp2, grp3, etc.)
412+
@app.grp1.initialize()
413+
def grp1_init():
414+
pass
350415
351-
from somewhere.upstream.management.commands.command import Command
416+
@app.group()
417+
def grp1():
418+
"""
419+
This would override grp1 entirely and remove all subcommands
420+
and groups.
421+
"""
352422
353-
# override the initializer (typer callback) of grp1 on Command,
354-
# this will not alter the child groups of grp1 (grp2, grp3, etc.)
355-
@Command.grp1.initialize()
356-
def grp1_init(self):
357-
pass
423+
.. tip::
358424

359-
@Command.group()
360-
def grp1(self):
361-
"""
362-
This would override grp1 entirely and remove all subcommands
363-
and groups.
364-
"""
425+
If a group or command has not been directly defined on a Command class, django-typer will do
426+
a `breadth first search <https://en.wikipedia.org/wiki/Breadth-first_search>`_ of the command
427+
tree and fetch the first group or subcommand that matches the name of the attribute. This
428+
means that you do not necessarily have to walk the command hierarchy
429+
(i.e. ``Command.grp1.grp2.grp3.cmd``), if there is only one cmd you can simply write
430+
``Command.cmd``. However, using the strict hierarchy will be robust to future changes.
365431

366432
When Do Plugins Make Sense?
367433
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -377,4 +443,4 @@ projects its often a good idea to break your code into apps that are as self con
377443
Plugins can be a good way to organize commands in a code base that follows this pattern. It
378444
also allows for deployments that install a subset of those apps and is therefore a good way to
379445
organize commands in code bases that serve as a framework for a particular kind of site or that
380-
support selecting the features to install.
446+
support selecting the features to install by the inclusion or exclusion of specific apps.

doc/source/tutorial.rst

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -60,12 +60,6 @@ Install django-typer_
6060
rich_ is a powerful library for rich text and beautiful formatting in the terminal.
6161
It is not required, but highly recommended for the best experience:
6262

63-
.. note::
64-
65-
If you install rich_, `traceback rendering <https://rich.readthedocs.io/en/stable/traceback.html>`_
66-
will be enabled by default. Refer to the :ref:`how-to <configure-rich-exception-tracebacks>` if
67-
you would like to disable it.
68-
6963

7064
2. Add ``django_typer`` to your ``INSTALLED_APPS`` setting:
7165

@@ -80,7 +74,7 @@ Install django-typer_
8074

8175
Adding django_typer to INSTALLED_APPS is not strictly necessary if you do not wish to use shell
8276
tab completions or configure `rich traceback rendering <https://rich.readthedocs.io/en/stable/traceback.html>`_.
83-
77+
Refer to the :ref:`how-to <configure-rich-exception-tracebacks>` if you would like to disable it.
8478

8579
Convert the closepoll command to a :class:`~django_typer.TyperCommand`
8680
----------------------------------------------------------------------

examples/extensions/backup/management/commands/backup.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ class Command(TyperCommand):
3636
def init_or_run_all(
3737
self,
3838
# if we add a context argument Typer will provide it
39+
# the context is a click object that contains additional
40+
# information about the broader CLI invocation
3941
context: typer.Context,
4042
output_directory: t.Annotated[
4143
Path,

examples/extensions/backup/management/commands/backup_typer.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
def init_or_run_all(
2323
self,
2424
# if we add a context argument Typer will provide it
25+
# the context is a click object that contains additional
26+
# information about the broader CLI invocation
2527
context: typer.Context,
2628
output_directory: t.Annotated[
2729
Path,

examples/extensions/media1/management/commands/backup_typer.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
backup_typer,
1111
)
1212

13+
# to inherit all commands defined upstream pass the root typer app from upstream
14+
# to our root app here. (This is not a standard typer interface.)
1315
app = Typer(backup_typer.app)
1416

1517

examples/extensions/media2/management/extensions/backup_typer.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
)
1111

1212

13+
# for typer-style plugins we use the decorators on the root Typer app directly
1314
@backup_typer.app.command()
1415
def media(
1516
# self is optional, but if you want to access the command instance, you

0 commit comments

Comments
 (0)