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():

from django_Typer 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 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:

from django_typer import TyperCommand, command

class Command(TyperCommand):

    @command(name='subcommand1')
    def handle(self):
        ...

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

    @command()
    def subcommand3(self):
        ...
from django_typer import get_command

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

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

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

# or this:
command.subcommand1()

Lets look at the help output:

django_typer Usage: django_typer [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 ───────────────────────────────────────────────────────────────────╮ subcommand1  subcommand2  subcommand3  ╰──────────────────────────────────────────────────────────────────────────────╯

Define Groups of Commands

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

from django_Typer import TyperCommand, command, group

class Command(TyperCommand):

    @group()
    def group1(self, common_option: bool = False):
        # you can define common options that will be available to all subcommands of
        # the group, and implement common initialization logic here.

    @group()
    def group2(self):
        ...

    # attach subcommands to groups by using the command decorator on the group function
    @group1.command()
    def grp1_subcommand1(self):
        ...

    @group1.command()
    def grp1_subcommand1(self):
        ...

    # groups can have subgroups!
    @group1.group()
    def subgroup1(self):
        ...

    @subgroup1.command()
    def subgrp_command(self):
        ...

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.

from django_Typer import TyperCommand, initialize, command

class Command(TyperCommand):

    @initialize()
    def init(self, common_option: bool = False):
        # you can define common options that will be available to all subcommands of
        # the command, and implement common initialization logic here. This will be
        # invoked before the chosen command
        ...

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

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

Call TyperCommands 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 import TyperCommand, command

class Command(TyperCommand):

    def handle(self, count: int=5):
        return count
from django.core.management import call_command
from django_typer 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 the 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 them 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.

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:

from django_typer import TyperCommand
from django_typer.types import Verbosity

class Command(TyperCommand):

    suppressed_base_arguments = ['--settings']  # remove the --settings option

    def handle(self, verbosity: Verbosity=1):
        ...

Configure the Typer Application

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:

from django_typer import TyperCommand

class Command(TyperCommand, chain=True):
    # here we pass chain=True to typer telling it to allow invocation of
    # multiple subcommands
    ...

Tip

See TyperCommandMeta for a list of available parameters. Also refer to the Typer docs for more details. Note that not all of these parameters make sense in the context of a Django management command, so behavior for some is undefined.

Define Shell Tab Completions for Parameters

See the section on defining shell completions.

Debug Shell Tab Completers

See the section on debugging shell completers.

Extend/Override TyperCommands

You can extend typer commands simply 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 extend and override the command or you can subclass and rename the module to provide an adapted version of the upstream command with a different name.

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":               # default is 10
    "locals_max_string":               # 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:

from django_typer import TyperCommand, command
from django.utils.translation import gettext_lazy as _

class Command(TyperCommand, help=_('2')):

    help = _("3")

    @command(help=_("1"))
    def handle(self):
        """
        Docstring is last priority and is not subject to translation.
        """

Document Commands w/Sphinx

Checkout this Sphinx extension that can be used to render your rich helps to Sphinx docs.

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()