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. --settings<str>The 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.              --pythonpath<path>A directory to add to the Python path, e.g.     "/home/djangoprojects/myproject".               --tracebackRaise on CommandError exceptions --show-localsPrint local variables in tracebacks. --no-colorDon't colorize the command output. --force-colorForce colorization of the command output. --skip-checksSkip system checks. ╰──────────────────────────────────────────────────────────────────────────────╯ ╭─ Commands ───────────────────────────────────────────────────────────────────╮ subcommand2 subcommand3 subcommand1 ╰──────────────────────────────────────────────────────────────────────────────╯
management Usage: management [OPTIONS] COMMAND [ARGS]... ╭─ Options ────────────────────────────────────────────────────────────────────╮ --helpShow this message and exit. ╰──────────────────────────────────────────────────────────────────────────────╯ ╭─ Django ─────────────────────────────────────────────────────────────────────╮ --versionShow program's version number and exit. --settings<str>The 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.              --pythonpath<path>A directory to add to the Python path, e.g.     "/home/djangoprojects/myproject".               --tracebackRaise on CommandError exceptions --show-localsPrint local variables in tracebacks. --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()

Collect Results with @finalize

Typer and Click have a results_callback mechanism on MultiCommands that allow a function hook to be registered to operate on the results of subroutines before the command exits. You may use this same results_callback mechanism directly through the Typer interface, but django-typer offers a more convenient class-aware way to do this with the finalize() decorator.

For example lets say we have two subcommands that return strings, we could turn them into a csv string by registering a callback with finalize():

import typing as t
from django_typer.management import TyperCommand, command, finalize


# chain=True allows multiple subroutines to be called from the command line
class Command(TyperCommand, chain=True):
    @finalize()
    def to_csv(self, results: t.List[str]):
        return ", ".join(results)

    @command()
    def cmd1(self):
        return "result1"

    @command()
    def cmd2(self):
        return "result2"
$> ./manage.py finalizer cmd1 cmd1 cmd2
result1, result2, result3

Tip

@finalize() wrapped callbacks will be passed the CLI parameters on the current context if the function signature accepts them. While convenient, we recommend using command state to track these parameters instead. This will be more amenable to direct invocations of command object functions.

Use @finalize on groups

Finalizers are hierarchical. The finalize() decorator is available for use on subgroups. When used on a group, the callback will be invoked after the group’s subcommands have been executed and the return value of the finalizer will be passed up to any finalizers at higher levels in the command hierarchy.

import typing as t
from django_typer.management import TyperCommand, command, finalize, group


class Command(TyperCommand, chain=True):
    """
    Show that finalizers are hierarchical and results are collected and
    passed to the finalizer of the parent group if one exists.
    """

    @finalize()
    def to_csv(self, results: t.List[str]):
        return ", ".join(results)

    @command()
    def cmd1(self):
        return "result1"

    @command()
    def cmd2(self):
        return "result2"

    @group(chain=True)
    def grp(self):
        return "grp"

    @grp.finalize()
    def to_upper_csv(self, results):
        return ", ".join([result.upper() for result in results])

    @grp.command()
    def cmd3(self):
        return "result3"

    @grp.command()
    def cmd4(self):
        return "result4"
$> ./manage.py finalizer cmd1 cmd1 cmd2 grp cmd4 cmd3
result1, result2, result3, RESULT4, RESULT3

Tip

Finalizers can be overridden just like groups and initializers using the plugin pattern.

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.

Exit Codes, Errors and Aborts

A command function can end in a number of ways: by returning a value, by raising CommandError, typer.Exit or typer.Abort, by calling sys.exit(), by being interrupted, or by raising some other exception. What happens next depends on how the command was invoked, because each of the three invocation contexts has a different caller with different expectations:

  • From the command line (manage.py mycommand) the caller is the shell. The outcome is a process exit status and, where appropriate, a message on stderr.

  • From call_command() the caller is Python code that wants a return value or an exception it can catch. This is Django’s contract for management commands: failures are reported by raising CommandError, which carries the exit status as its returncode.

  • Called directly as a function (mycommand(), mycommand.subcommand() or get_command("mycommand", "subcommand")(), see calling commands from code) it is a plain Python call. django-typer does not interpose: whatever the function returns is returned and whatever it raises propagates unchanged.

The command …

Command line

call_command()

Direct call

returns a value

exit status 0, nothing printed unless Toggle on/off result printing is set

value returned

value returned

raises CommandError(msg, returncode=n)

CommandError: msg on stderr, exit status n

CommandError raised

CommandError raised

raises typer.Exit(0)

exit status 0, nothing printed

None returned

typer.Exit raised

raises typer.Exit(n)

exit status n, nothing printed

CommandError raised with returncode=n, chained from the Exit

typer.Exit raised

raises typer.Abort() (a declined confirmation, or input ending at a prompt)

Aborted! on stderr, exit status 1

CommandError("Aborted!") raised with returncode=1

typer.Abort raised

calls sys.exit(n)

exit status n

SystemExit raised

SystemExit raised

is interrupted (KeyboardInterrupt)

exit status 130, nothing printed

KeyboardInterrupt raised

KeyboardInterrupt raised

writes to a pipe whose reader has gone away (| head)

exit status 1, nothing printed

BrokenPipeError raised

BrokenPipeError raised

raises any other exception

traceback on stderr (see Configure rich Stack Traces), exit status 1

the exception raised

the exception raised

Some guidance that follows from the table:

  • typer.Exit carries nothing but a status. If there is something to say, say it first, for example with typer.echo(..., err=True) or self.stderr.write(...), then raise Exit. From the command line nothing else is printed.

  • To fail with a message, prefer raise CommandError(msg, returncode=n). It is the Django idiom, it behaves the same way from every context, and other Django code already knows how to handle it.

  • Exit(0) means success. From call_command() it is indistinguishable from returning None.

  • --help and --version end the process during argument parsing, from the command line and from call_command() alike, just as they do for any BaseCommand.

Wrap a Command in a Transaction

Django’s BaseCommand has no option to run a command inside a database transaction (its output_transaction attribute only decorates SQL that a command prints). A TyperCommand invocation may run an initializer, several chained subcommands and a finalizer, so it is useful to be able to make the whole invocation atomic. Set the class attribute atomic and everything execute() runs is wrapped in django.db.transaction.atomic():

 1from django.contrib.auth.models import Group
 2
 3from django_typer.management import TyperCommand, command
 4
 5
 6class Command(TyperCommand, chain=True):
 7    # the initializer, every chained subcommand and the finalizer run in one
 8    # transaction - if any of them fails nothing is committed
 9    atomic = True
10
11    @command()
12    def create(self, name: str):
13        Group.objects.create(name=name)
14
15    @command()
16    def rename(self, old: str, new: str):
17        Group.objects.filter(name=old).update(name=new)
18
19    @command()
20    def delete(self, name: str):
21        # raises Group.DoesNotExist if there is no such group
22        Group.objects.get(name=name).delete()

Running manage.py atomic create a create b delete c creates nothing: the last subcommand raises and rolls back the first two.

atomic accepts:

True

The database named by the command’s database option when it declares one (on the initializer or on handle()), otherwise the default database. This mirrors how Django’s own commands resolve their --database option.

"other"

The database with that alias.

("default", "other")

A nested atomic() block for each alias, entered in that order.

"__all__"

Every database in DATABASES.

The transaction applies when the command is run from the command line or through call_command(), the two contexts in which django-typer drives the command. Command functions called directly from Python are plain calls and are not wrapped. The transaction commits when the command succeeds and rolls back on every other outcome in the exit table:

The command …

call_command()

Command line

Transaction

returns a value

value returned

normal return

commit

raises typer.Exit(0)

None returned

normal return

commit

raises typer.Exit(n)

CommandError, returncode=n

SystemExit(n)

rollback

raises typer.Abort()

CommandError("Aborted!")

SystemExit(1)

rollback

calls sys.exit(0)

SystemExit(0)

SystemExit(0)

commit

calls sys.exit(n)

SystemExit(n)

SystemExit(n)

rollback

is interrupted (KeyboardInterrupt)

KeyboardInterrupt

SystemExit(130)

rollback

raises CommandError(returncode=n)

CommandError

SystemExit(n)

rollback

raises any other exception

the exception

the exception

rollback

Note

Separate databases are separate transactions. A sequence of aliases or "__all__" rolls every database back when the command fails, but the commits at the end are still made one database at a time.

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,               # default is False
    "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.

Configure the Manage Script Name

The usage line in command help, and the command name shell completions are installed for, are derived from the script that was invoked. When the script is available as a command on the path (e.g. an installed entry point, or a shim or wrapper of the same name that launches it) only its name is shown. Otherwise the path to the script relative to the current working directory is shown:

$ manage basic --help
Usage: manage basic [OPTIONS] ARG1 ARG2

$ python manage.py basic --help
Usage: ./manage.py basic [OPTIONS] ARG1 ARG2

If this detection does not produce the name you want, for example when your script is launched by a wrapper with a different name, set DT_MANAGE_SCRIPT. It is used verbatim in place of the detected name for command help and by shellcompletion when no --manage-script is given.

settings.py
DT_MANAGE_SCRIPT = "mycli"

Tip

If you run your manage script through another command, like a just recipe or poetry run, see Completions for Wrapped Invocations for how to get help output and tab completion working with the wrapper.

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 typer 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. --settings<str>The 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.              --pythonpath<path>A directory to add to the Python path, e.g.     "/home/djangoprojects/myproject".               --tracebackRaise on CommandError exceptions --show-localsPrint local variables in tracebacks. --no-colorDon't colorize the command output. --force-colorForce colorization of the command output. --skip-checksSkip system checks. ╰──────────────────────────────────────────────────────────────────────────────╯ ╭─ Commands ───────────────────────────────────────────────────────────────────╮ b a d c ╰──────────────────────────────────────────────────────────────────────────────╯
order Usage: manage.py order [OPTIONS] COMMAND [ARGS]... ╭─ Options ────────────────────────────────────────────────────────────────────╮ --helpShow this message and exit. ╰──────────────────────────────────────────────────────────────────────────────╯ ╭─ Django ─────────────────────────────────────────────────────────────────────╮ --versionShow program's version number and exit. --settings<str>The 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.              --pythonpath<path>A directory to add to the Python path, e.g.     "/home/djangoprojects/myproject".               --tracebackRaise on CommandError exceptions --show-localsPrint local variables in tracebacks. --no-colorDon't colorize the command output. --force-colorForce colorization of the command output. --skip-checksSkip system checks. ╰──────────────────────────────────────────────────────────────────────────────╯ ╭─ Commands ───────────────────────────────────────────────────────────────────╮ b a d c ╰──────────────────────────────────────────────────────────────────────────────╯
manage.py order d Usage: manage.py order d [OPTIONS] COMMAND [ARGS]... ╭─ Options ────────────────────────────────────────────────────────────────────╮ --helpShow this message and exit. ╰──────────────────────────────────────────────────────────────────────────────╯ ╭─ Commands ───────────────────────────────────────────────────────────────────╮ f e ╰──────────────────────────────────────────────────────────────────────────────╯
manage.py order d Usage: manage.py order d [OPTIONS] COMMAND [ARGS]... ╭─ Options ────────────────────────────────────────────────────────────────────╮ --helpShow this message and exit. ╰──────────────────────────────────────────────────────────────────────────────╯ ╭─ Commands ───────────────────────────────────────────────────────────────────╮ f e ╰──────────────────────────────────────────────────────────────────────────────╯

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

Toggle on/off result printing

Django’s BaseCommand writes any truthy value returned from handle() to stdout. TyperCommand does not: return values are for callers (see Call Commands from Code) and output is whatever the command chooses to print. To get Django’s behavior back, set the class field print_result to True and truthy return values will be written to stdout:

1from django_typer.management import TyperCommand
2
3
4class Command(TyperCommand):
5    print_result = True
6
7    def handle(self):
8        return "This will be printed"

The default can also be changed for a whole project with the DT_PRINT_RESULT setting. A command’s own print_result field, when set, takes precedence over it.

Note

Prior to version 4.0 results were printed by default. If your commands rely on their return values reaching stdout, set print_result = True on them or DT_PRINT_RESULT = True in your settings.