How-To

Define an Argument

Positional arguments on a command are treated as positional command line arguments by Typer. For example to define an integer positional argument we could simply do:

def handle(self, int_arg: int):
    ...

You will likely want to add additional meta information to your arguments for Typer to render things like helps and usage strings. You can do this by annotating the type hint with the typer.Argument class:

import typing as t
from typer import Argument

# ...

def handle(self, int_arg: t.Annotated[int, Argument(help="An integer argument")]):
    ...

Tip

Refer to the Typer docs on arguments for more details.

Define an Option

Options are like arguments but are not position dependent and instead provided with a preceding identifier string (e.g. –name).

When a default value is provided for a parameter, Typer will treat it as an option. For example:

def handle(self, flag: bool = False):
    ...

Would be called like this:

$ mycommand --flag

If the type hint on the option is something other than a boolean it will accept a value:

def handle(self, name: str = "world"):
    ...

Would be called like this:

$ mycommand --name=world
$ mycommand --name world  # this also works

To add meta information, we annotate with the typer.Option class:

import typing as t
from typer import Option

# ...

def handle(self, name: t.Annotated[str, Option(help="The name of the thing")]):
    ...

Tip

Refer to the Typer docs on options for more details.

Define Multiple Subcommands

Commands with a single executable function should simply implement handle(), but if you would like have multiple subcommands you can define any number of functions decorated with command() or command():

from django_typer.management import TyperCommand, command

class Command(TyperCommand):

    @command()
    def subcommand1(self):
        ...

    @command()
    def subcommand2(self):
        ...

Note

When no handle() method is defined, you cannot invoke a command instance as a callable. instead you should invoke subcommands directly:

from django_typer.management import get_command

command = get_command("mycommand")
command.subcommand1()
command.subcommand2()

command() # this will raise an error

Define Multiple Subcommands w/ a Default

We can also implement a default subcommand by defining a handle() method, and we can rename it to whatever we want the command to be. For example to define three subcommands but have one as the default we can do this:

 1from django_typer.management import TyperCommand, command
 2
 3
 4class Command(TyperCommand):
 5    @command(name="subcommand1")
 6    def handle(self):
 7        return "handle"
 8
 9    @command()
10    def subcommand2(self):
11        return "subcommand2"
12
13    @command()
14    def subcommand3(self):
15        return "subcommand3"
from django_typer.management import get_command

command = get_command("mycommand")
assert command.subcommand2() == 'subcommand2'
assert command.subcommand3() == 'subcommand3'

# this will invoke handle (i.e. subcommand1)
assert command() == 'handle'
command()

# note - we *cannot* do this:
command.handle()

# but we can do this!
assert command.subcommand1() == 'handle'

Lets look at the help output:

management Usage: management [OPTIONS] COMMAND [ARGS]... ╭─ Options ────────────────────────────────────────────────────────────────────╮ --helpShow this message and exit. ╰──────────────────────────────────────────────────────────────────────────────╯ ╭─ Django ─────────────────────────────────────────────────────────────────────╮ --versionShow program's version number and exit. --settingsTEXTThe Python path to a settings module, e.g.        "myproject.settings.main". If this isn't          provided, the DJANGO_SETTINGS_MODULE environment  variable will be used.                            --pythonpathPATHA directory to add to the Python path, e.g.       "/home/djangoprojects/myproject".                 [default: None]                                   --tracebackRaise on CommandError exceptions --no-colorDon't colorize the command output. --force-colorForce colorization of the command output. --skip-checksSkip system checks. ╰──────────────────────────────────────────────────────────────────────────────╯ ╭─ Commands ───────────────────────────────────────────────────────────────────╮ subcommand2  subcommand3  subcommand1  ╰──────────────────────────────────────────────────────────────────────────────╯

Define Groups of Commands

Any depth of command tree can be defined. Use the group() or add_typer() decorator to define a group of subcommands:

 1from django_typer.management import TyperCommand, group
 2
 3
 4class Command(TyperCommand):
 5    @group()
 6    def group1(self, common_option: bool = False):
 7        # you can define common options that will be available to all
 8        # subcommands of the group, and implement common initialization
 9        # logic here.
10        ...
11
12    @group()
13    def group2(self): ...
14
15    # attach subcommands to groups by using the command decorator on the group
16    # function
17    @group1.command()
18    def grp1_subcommand1(self): ...
19
20    @group1.command()
21    def grp1_subcommand2(self): ...
22
23    # groups can have subgroups!
24    @group1.group()
25    def subgroup1(self): ...
26
27    @subgroup1.command()
28    def subgrp_command(self): ...

The hierarchy of groups and commands from the above example looks like this:

_images/howto_groups_app_tree.png

Define an Initialization Callback

You can define an initializer function that takes arguments and options that will be invoked before your handle() command or subcommands using the initialize() decorator. This is like defining a group at the command root and is an extension of the typer callback mechanism.

 1from django_typer.management import TyperCommand, command, initialize
 2
 3
 4class Command(TyperCommand):
 5    @initialize()
 6    def init(self, common_option: bool = False):
 7        # you can define common options that will be available to all
 8        # subcommands of the command, and implement common initialization
 9        # logic here. This will be invoked before the chosen command
10        self.common_option = common_option
11
12    @command()
13    def subcommand1(self):
14        return self.common_option
15
16    @command()
17    def subcommand2(self):
18        return self.common_option
$> ./manage initializer --common-option subcommand1
True
$> ./manage.py initializer --no-common-option subcommand2
False
from django_typer.management import get_command

command = get_command("initializer")
command.init(common_option=True)
assert command.subcommand1()
command.init(False)
assert not command.subcommand2()

Call Commands from Code

There are two options for invoking a TyperCommand from code without spawning off a subprocess. The first is to use Django’s builtin call_command function. This function will work exactly as it does for normal BaseCommand derived commands. django-typer however adds another mechanism that can be more efficient, especially if your options and arguments are already of the correct type and require no parsing:

Say we have this command, called mycommand:

from django_typer.management import TyperCommand, command

class Command(TyperCommand):

    def handle(self, count: int=5):
        return count
from django.core.management import call_command
from django_typer.management import get_command

# we can use use call_command like with any Django command
call_command("mycommand", count=10)
call_command("mycommand", '--count=10')  # this will work too

# or we can use the get_command function to get a command instance and invoke it directly
mycommand = get_command("mycommand")
mycommand(count=10)
mycommand(10) # this will work too

# return values are also available
assert mycommand(10) == 10

The rule of thumb is this:

  • Use call_command if your options and arguments need parsing.

  • Use get_command() and invoke the command functions directly if your options and arguments are already of the correct type.

If the second argument is a type, static type checking will assume the return value of get_command to be of that type:

from django_typer.management import get_command
from myapp.management.commands.math import Command as Math

math = get_command("math", Math)
math.add(10, 5)  # type checkers will resolve add parameters correctly

You may also fetch a subcommand function directly by passing its path:

get_command("math", "add")(10, 5)

Tip

Also refer to the get_command() docs and here and here for the nuances of calling commands when handle() is and is not implemented.

Change Default Django Options

TyperCommand classes preserve all of the functionality of BaseCommand derivatives. This means that you can still use class members like suppressed_base_arguments to suppress default options.

By default TyperCommand suppresses --verbosity. You can add it back by setting suppressed_base_arguments to an empty list. If you want to use verbosity you can simply redefine it or use one of django-typer’s provided type hints for the default BaseCommand options:

1from django_typer.management import TyperCommand
2from django_typer.types import Verbosity
3
4
5class Command(TyperCommand):
6    suppressed_base_arguments = ["--settings"]  # remove the --settings option
7
8    def handle(self, verbosity: Verbosity = 1): ...

Configure Typer Options

Typer apps can be configured using a number of parameters. These parameters are usually passed to the Typer class constructor when the application is created. django-typer provides a way to pass these options upstream to Typer by supplying them as keyword arguments to the TyperCommand class inheritance:

configure.py
 1from django_typer.management import TyperCommand, command
 2
 3
 4# here we pass chain=True to typer telling it to allow invocation of
 5# multiple subcommands
 6class Command(TyperCommand, chain=True):
 7    @command()
 8    def cmd1(self):
 9        self.stdout.write("cmd1")
10
11    @command()
12    def cmd2(self):
13        self.stdout.write("cmd2")
$> ./manage.py configure cmd1 cmd2
cmd1
cmd2

$> ./manage.py configure cmd2 cmd1
cmd2
cmd1

Tip

See TyperCommandMeta or Typer for a list of available parameters. Also refer to the Typer docs for more details.

Define Shell Tab Completions for Parameters

See the section on defining shell completions.

Debug Shell Tab Completers

See the section on debugging shell completers.

Inherit/Override Commands

You can extend typer commands by subclassing them. All of the normal inheritance rules apply. You can either subclass an existing command from an upstream app and leave its module the same name to override the command or you can subclass and rename the module to provide an adapted version of the upstream command with a different name. For example:

Say we have a command that looks like:

management/commands/upstream.py
 1from django_typer.management import TyperCommand, command, group, initialize
 2
 3
 4class Command(TyperCommand):
 5    @initialize()
 6    def init(self):
 7        return "upstream:init"
 8
 9    @command()
10    def sub1(self):
11        return "upstream:sub1"
12
13    @command()
14    def sub2(self):
15        return "upstream:sub2"
16
17    @group()
18    def grp1(self):
19        return "upstream:grp1"
20
21    @grp1.command()
22    def grp1_cmd1(self):
23        return "upstream:grp1_cmd1"

We can inherit and override or add additional commands and groups like so:

management/commands/downstream.py
 1from django_typer.management import command, initialize
 2
 3from .upstream import Command as Upstream
 4
 5
 6# inherit directly from the upstream command class
 7class Command(Upstream):
 8    # override init
 9    @initialize()
10    def init(self):
11        return "downstream:init"
12
13    # override sub1
14    @command()
15    def sub1(self):
16        return "downstream:sub1"
17
18    # add a 3rd top level command
19    @command()
20    def sub3(self):
21        return "downstream:sub3"
22
23    # add a new subcommand to grp1
24    @Upstream.grp1.command()
25    def grp1_cmd2(self):
26        return "downstream:grp1_cmd2"

Notice that if we are adding to a group from the parent class, we have to use the group directly (i.e. @ParentClass.group_name). Since we named our command downstream it does not override upstream. upstream is not affected and may be invoked in the same way as if downstream was not present.

Note

For more information on extension patterns see the tutorial on Extending Commands.

Plugin to Existing Commands

You may add additional subcommands and command groups to existing commands by using django-typer’s plugin pattern. This allows apps that do not know anything about each other to attach additional CLI behavior to an upstream command and can be convenient for grouping loosely related behavior into a single command namespace.

To use our example from above, lets add and override the same behavior of upstream we did in downstream using this pattern instead:

First in other_app we need to create a new package under management. It can be called anything, but for clarity lets call it plugins:

site/
├── my_app/
│   ├── __init__.py
│   ├── apps.py
│   ├── management/
│   │   ├── __init__.py
│   │   └── commands/
│   │       ├── __init__.py
│   │       └── upstream.py
└── other_app/
    ├── __init__.py
    ├── apps.py
    └── management/
        ├── __init__.py
        ├── plugins/
        │   ├── __init__.py
        │   └── upstream.py <---- put your plugins to upstream here
        └── commands/
            └── __init__.py

Now we need to make sure our plugins are loaded. We do this by using the provided register_command_plugins() convenience function in our app’s ready() method:

other_app/apps.py
from django.apps import AppConfig
from django_typer.utils import register_command_plugins


class OtherAppConfig(AppConfig):
    name = "other_app"

    def ready(self):
        from .management import plugins

        register_command_plugins(extensions)

Now we can add our plugins:

management/plugins/upstream.py
 1from my_app.management.commands.upstream import Command as Upstream
 2
 3# When plugging into an existing command we do not create
 4# a new class, but instead work directly with the commands
 5# and groups on the upstream class
 6
 7
 8# override init
 9@Upstream.initialize()
10def init(self):
11    return "plugin:init"
12
13
14# override sub1
15@Upstream.command()
16def sub1(self):
17    return "plugin:sub1"
18
19
20# add a 3rd top level command
21@Upstream.command()
22def sub3(self):
23    return "plugin:sub3"
24
25
26# add a new subcommand to grp1
27@Upstream.grp1.command()
28def grp1_cmd2(self):
29    return "plugin:grp1_cmd2"

The main difference here from normal inheritance is that we do not declare a new class, instead we use the classmethod decorators on the class of the command we are extending. These extension functions will also be added to the class. The self argument is always optional in django-typer and if it is not provided the function will be treated as a staticmethod.

Note

Conflicting extensions are resolved in INSTALLED_APPS order. For a detailed discussion about the utility of this pattern, see the tutorial on Extending Commands.

Warning

Take care not to import any extension code during or before Django’s bootstrap procedure. This may result in conflict override behavior that does not honor INSTALLED_APPS order.

Configure rich Stack Traces

When rich is installed it may be configured to display rendered stack traces for unhandled exceptions. These stack traces are information dense and can be very helpful for debugging. By default, if rich is installed django-typer will configure it to render stack traces. You can disable this behavior by setting the DT_RICH_TRACEBACK_CONFIG config to False. You may also set DT_RICH_TRACEBACK_CONFIG to a dictionary holding the parameters to pass to rich.traceback.install.

This provides a common hook for configuring rich that you can control on a per-deployment basis:

settings.py
# refer to the rich docs for details
DT_RICH_TRACEBACK_CONFIG = {
    "console": rich.console.Console(), # create a custom Console object for rendering
    "width": 100,                      # default is 100
    "extra_lines": 3,                  # default is 3
    "theme": None,                     # predefined themes
    "word_wrap": False,                # default is False
    "show_locals": True,               # rich default is False, but we turn this on
    "locals_max_length": 10            # default is 10
    "locals_max_string": 80            # default is 80
    "locals_hide_dunder": True,        # default is True
    "locals_hide_sunder": False,       # default is None
    "indent_guides": True,             # default is True
    "suppress": [],                    # suppress frames from these module import paths
    "max_frames": 100                  # default is 100
}

# or turn off rich traceback rendering
DT_RICH_TRACEBACK_CONFIG = False

Tip

There are traceback configuration options that can be supplied as configuration parameters to the Typer application. It is best to not set these and allow users to configure tracebacks via the DT_RICH_TRACEBACK_CONFIG setting.

Add Help Text to Commands

There are multiple places to add help text to your commands. There is however a precedence order, and while lazy translation is supported in help texts, if you use docstrings as the helps they will not be translated.

The precedence order, for a simple command is as follows:

 1from django.utils.translation import gettext_lazy as _
 2
 3from django_typer.management import TyperCommand, command
 4
 5
 6class Command(TyperCommand, help=_("2")):
 7    """
 8    5: Command class docstrings are the last resort for
 9    the upper level command help string.
10    """
11
12    help = _("3")
13
14    # if an initializer is present it's help will be used for the command
15    # level help
16
17    @command(help=_("1"))
18    def handle(self):
19        """
20        4: Function docstring is last priority and is not subject to
21           translation.
22        """

The rule for how helps are resolved when inheriting from other commands is that higher precedence helps in base classes will be chosen over lower priority helps in deriving classes. However, if you would like to use a docstring as the help in a derived class instead of the high priority help in a base class you can set the equivalent priority help in the deriving class to a falsy value:

class Command(TyperCommand, help=_("High precedence help defined in base class.")):
    ...

...

from upstream.management.commands.command import Command as BaseCommand
class Command(BaseCommand, help=None):
    """
    Docstring will be used as help.
    """

Order Commands in Help Text

By default commands are listed in the order they appear in the class. You can override this by using a custom click group.

For example, to change the order of commands to be in alphabetical order you could define a custom group and override the list_commands method. Custom group and command classes may be provided like below, but they must extend from django-typer’s classes:

import typing as t
from django_typer.management import TyperCommand, DTGroup, command, group
from click import Context


class AlphabetizeCommands(DTGroup):
    def list_commands(self, ctx: Context) -> t.List[str]:
        return list(sorted(self.commands.keys()))


class Command(TyperCommand, cls=AlphabetizeCommands):
    @command()
    def b(self):
        print("b")

    @command()
    def a(self):
        print("a")

    @group(cls=AlphabetizeCommands)
    def d(self):
        print("d")

    @d.command()
    def f(self):
        print("f")

    @d.command()
    def e(self):
        print("e")

    @command()
    def c(self):
        print("c")
order Usage: manage.py order [OPTIONS] COMMAND [ARGS]... ╭─ Options ────────────────────────────────────────────────────────────────────╮ --helpShow this message and exit. ╰──────────────────────────────────────────────────────────────────────────────╯ ╭─ Django ─────────────────────────────────────────────────────────────────────╮ --versionShow program's version number and exit. --settingsTEXTThe Python path to a settings module, e.g.        "myproject.settings.main". If this isn't          provided, the DJANGO_SETTINGS_MODULE environment  variable will be used.                            --pythonpathPATHA directory to add to the Python path, e.g.       "/home/djangoprojects/myproject".                 [default: None]                                   --tracebackRaise on CommandError exceptions --no-colorDon't colorize the command output. --force-colorForce colorization of the command output. --skip-checksSkip system checks. ╰──────────────────────────────────────────────────────────────────────────────╯ ╭─ Commands ───────────────────────────────────────────────────────────────────╮ ╰──────────────────────────────────────────────────────────────────────────────╯
 manage.py order d Usage: manage.py order d [OPTIONS] COMMAND [ARGS]... ╭─ Options ────────────────────────────────────────────────────────────────────╮ --helpShow this message and exit. ╰──────────────────────────────────────────────────────────────────────────────╯ ╭─ Commands ───────────────────────────────────────────────────────────────────╮ ╰──────────────────────────────────────────────────────────────────────────────╯

Document Commands w/Sphinx

sphinxcontrib-typer can be used to render your rich helps to Sphinx docs and is used extensively in this documentation.

For example, to document a TyperCommand with sphinxcontrib-typer, you would do something like this:

.. typer:: django_typer.management.commands.shellcompletion.Command:typer_app
    :prog: ./manage.py shellcompletion
    :show-nested:
    :width: 80

The Typer application object is a property of the command class and is named typer_app. The typer directive simply needs to be given the fully qualified import path of the application object.

Or we could render the helps for individual subcommands as well:

.. typer:: django_typer.management.commands.shellcompletion.Command:typer_app:install
    :prog: ./manage.py shellcompletion
    :width: 80

You’ll also need to make sure that Django is bootstrapped in your conf.py file:

conf.py
import django

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'path.to.your.settings')
django.setup()