django-typer
Use static typing to define the CLI for your Django management commands with Typer. Optionally use the provided TyperCommand class that inherits from BaseCommand. This class maps the Typer interface onto a class based interface that Django developers will be familiar with. All of the BaseCommand functionality is inherited, so that TyperCommand can be a drop in replacement.
django-typer makes it easy to:
- Define your command CLI interface in a clear, DRY and safe way using type hints
- Create subcommands and hierarchical groups of commands.
- Use the full power of Typer's parameter types to validate and parse command line inputs.
- Create beautiful and information dense help outputs.
- Configure the rendering of exception stack traces using rich.
- Install shell tab-completion support for bash, zsh, fish, and powershell.
- Create custom and portable shell tab-completions for your CLI parameters.
- Port existing commands (TyperCommand is interface compatible with BaseCommand).
- Use either a Django-style class-based interface or the Typer-style interface to define commands.
- Add plugins to upstream commands.
Please refer to the full documentation for more information.
🚨 Deprecation Notice
Imports from django_typer
have been deprecated and will be removed in 3.0! Imports have moved to django_typer.management
:
# old way
from django_typer import TyperCommand, command, group, initialize, Typer
# new way!
from django_typer.management import TyperCommand, command, group, initialize, Typer
Installation
- Clone django-typer from GitHub or install a release off PyPI:
bash
pip install django-typer
rich is a powerful library for rich text and beautiful formatting in the terminal. It is not required but highly recommended for the best experience:
bash
pip install "django-typer[rich]"
- Optionally add
django_typer
to yourINSTALLED_APPS
setting:
python
INSTALLED_APPS = [
...
'django_typer',
]
You only need to install django_typer as an app if you want to use the shell completion command to enable tab-completion or if you would like django-typer to install rich traceback rendering for you - which it does by default if rich is also installed.
Basic Example
TyperCommand is a drop in extension to BaseCommand. All of the documented features of BaseCommand work the same way!
from django_typer.management import TyperCommand
class Command(TyperCommand):
def handle(self, arg1: str, arg2: str, arg3: float = 0.5, arg4: int = 1):
"""
A basic command that uses Typer
"""
Or, you may also use an interface identical to Typer's. Simply import Typer from django_typer instead of typer.
from django_typer.management import Typer
app = Typer()
@app.command()
def main(arg1: str, arg2: str, arg3: float = 0.5, arg4: int = 1):
"""
A basic command that uses Typer
"""
Multiple Subcommands Example
Commands with multiple subcommands can be defined:
import typing as t
from django.utils.translation import gettext_lazy as _
from typer import Argument
from django_typer.management import TyperCommand, command
class Command(TyperCommand):
"""
A command that defines subcommands.
"""
@command()
def create(
self,
name: t.Annotated[str, Argument(help=_("The name of the object to create."))],
):
"""
Create an object.
"""
@command()
def delete(
self, id: t.Annotated[int, Argument(help=_("The id of the object to delete."))]
):
"""
Delete an object.
"""
Or using the typer-style interface this could be written:
from django_typer.management import Typer
import typing as t
from django.utils.translation import gettext_lazy as _
from typer import Argument
app = Typer(help="A command that defines subcommands.")
@app.command()
def create(
name: t.Annotated[str, Argument(help=_("The name of the object to create."))],
):
"""
Create an object.
"""
@app.command()
def delete(
id: t.Annotated[int, Argument(help=_("The id of the object to delete."))]
):
"""
Delete an object.
"""
Grouping and Hierarchies Example
More complex groups and subcommand hierarchies can be defined. For example, this command defines a group of commands called math, with subcommands divide and multiply. The group has a common initializer that optionally sets a float precision value. We would invoke this command like so:
./manage.py hierarchy math --precision 5 divide 10 2.1
./manage.py hierarchy math multiply 10 2
Using the class-based interface we could define the command like this:
import typing as t
from functools import reduce
from django.utils.translation import gettext_lazy as _
from typer import Argument, Option
from django_typer.management import TyperCommand, group
class Command(TyperCommand):
help = _("A more complex command that defines a hierarchy of subcommands.")
precision = 2
@group(help=_("Do some math at the given precision."))
def math(
self,
precision: t.Annotated[
int, Option(help=_("The number of decimal places to output."))
] = precision,
):
self.precision = precision
# helps can be passed to the decorators
@math.command(help=_("Multiply the given numbers."))
def multiply(
self,
numbers: t.Annotated[
t.List[float], Argument(help=_("The numbers to multiply"))
],
):
return f"{reduce(lambda x, y: x * y, [1, *numbers]):.{self.precision}f}"
# or if no help is supplied to the decorators, the docstring if present
# will be used!
@math.command()
def divide(
self,
numerator: t.Annotated[float, Argument(help=_("The numerator"))],
denominator: t.Annotated[float, Argument(help=_("The denominator"))],
floor: t.Annotated[bool, Option(help=_("Use floor division"))] = False,
):
"""
Divide the given numbers.
"""
if floor:
return str(numerator // denominator)
return f"{numerator / denominator:.{self.precision}f}"
The typer-style interface builds a TyperCommand class for us that allows you to optionally accept the self argument in your commands. We could define the above command using the typer interface like this:
import typing as t
from functools import reduce
from django.utils.translation import gettext_lazy as _
from typer import Argument, Option
from django_typer.management import Typer
app = Typer(help=_("A more complex command that defines a hierarchy of subcommands."))
math_grp = Typer(help=_("Do some math at the given precision."))
app.add_typer(math_grp)
@math_grp.callback()
def math(
self,
precision: t.Annotated[
int, Option(help=_("The number of decimal places to output."))
] = 2,
):
self.precision = precision
@math_grp.command(help=_("Multiply the given numbers."))
def multiply(
self,
numbers: t.Annotated[
t.List[float], Argument(help=_("The numbers to multiply"))
],
):
return f"{reduce(lambda x, y: x * y, [1, *numbers]):.{self.precision}f}"
@math_grp.command()
def divide(
self,
numerator: t.Annotated[float, Argument(help=_("The numerator"))],
denominator: t.Annotated[float, Argument(help=_("The denominator"))],
floor: t.Annotated[bool, Option(help=_("Use floor division"))] = False,
):
"""
Divide the given numbers.
"""
if floor:
return str(numerator // denominator)
return f"{numerator / denominator:.{self.precision}f}"