Tutorial: Shell Tab-Completions

_images/closepoll_example.gif

Shell completions are helpful suggestions that are displayed when you press the <TAB> key while typing a command in a shell. They are especially useful when you are not sure about the exact name of a command, its options, its arguments or the potential values of either.

Django has some support for bash completions, but it is not enabled by default and left to the user to install.

django-typer augments the upstream functionality of Typer and Click to provide both an easy way to define shell completions for your custom CLI options and arguments as well as a way to install them in your shell.

Tip

django-typer supports shell completion installation for bash, zsh, fish and powershell.

Installation

Each shell has its own mechanism for enabling completions and this is further complicated by how different shells are installed and configured on different platforms. All shells have the same basic process. Completion logic needs to be registered with the shell that will be invoked when tabs are pressed for a specific command or script. To install tab completions for django commands we need to register our completion logic for Django manage script with the shell. This process has two phases:

  1. Ensure that your shell is configured to support completions.

  2. Use the shellcompletion command to install the completion hook for your Django manage script. This usually entails adding a specifically named script to a certain directory or adding lines to an existing script. The shellcompletion command will handle this for you.

The goal of this guide is not to be an exhaustive list of how to enable completions for each supported shell on all possible platforms, but rather to provide general guidance on how to enable completions for the most common platforms and environments. If you encounter issues or have solutions, please report them on our issues page

Windows

powershell is now bundled with most modern versions of Windows. There should be no additional installation steps necessary, but please refer to the Windows documentation if powershell is not present on your system.

Linux

bash is the default shell on most Linux distributions. Completions should be enabled by default.

OSX

zsh is currently the default shell on OSX. Unfortunately completions are not supported out of the box. We recommend using homebrew to install the zsh-completions package.

After installing the package you will need to add some configuration to your .zshrc file. We have had luck with the following:

zstyle ':completion:*' menu select

if type brew &>/dev/null; then
    FPATH=~/.zfunc:$(brew --prefix)/share/zsh-completions:$FPATH

    autoload -Uz compinit
    compinit
fi

fpath+=~/.zfunc

Install the Completion Hook

django-typer comes with a management command called shellcompletion. To install completions for your Django project simply run the install command:

./manage.py shellcompletion install

Note

The manage script may be named differently in your project - this is fine. The only requirement is that you invoke the shellcompletion command in the same way you would invoke any commands you would like tab completions to work for.

The installation script should be able to automatically detect your shell and install the appropriate scripts. If it is unable to do so you may force it to install for a specific shell by passing the shell name as an argument. Refer to the shellcompletion for details.

After installation you will need to restart your shell or source the appropriate rc file.

Warning

In production environments it is recommended that your management script be installed as a command on your system path. This will produce the most reliable installation.

Enabling Completions in Development Environments

Most shells work best when the manage script is installed as an executable on the system path. This is not always the case, especially in development environments. In these scenarios completion installation should still work, but you may need to always invoke the script from the same path. Fish may not work at all in this mode.

Integrating with Other CLI Completion Libraries

When tab completion is requested for a command that is not a TyperCommand, django-typer will delegate that request to Django’s autocomplete function as a fallback. This means that using django-typer to install completion scripts will enable completions for Django BaseCommands in all supported shells.

However, if you are using a separate package to define custom tab completions for your commands you may use the –fallback parameter to supply a separate fallback hook that will invoke the appropriate completion function for your commands. If there are other popular completion libraries please consider letting us know or submitting a PR to support these libraries as a fallback out of the box.

The long-term solution here should be that Django itself manages completion installation and provides hooks for implementing libraries to provide completions for their own commands.

Defining Custom Completions

To define custom completion logic for your arguments and options pass the shell_completion parameter in your type hint annotations. django-typer comes with a few provided completers for common Django types. One of the provided completers completes Django app labels and names. We might build a similar completer that only works for Django app labels like this:

examples/completers/management/commands/app_labels.py
 1import typing as t
 2import typer
 3from click import Context, Parameter
 4from click.shell_completion import CompletionItem
 5from django.apps import apps
 6
 7from django_typer.management import TyperCommand, command
 8
 9
10# the completer function signature must match this exactly
11def complete_app_label(
12    ctx: Context, param: Parameter, incomplete: str
13) -> t.List[CompletionItem]:
14    # don't offer apps that are already present as completion suggestions
15    present = [app.label for app in (ctx.params.get(param.name or "") or [])]
16    return [
17        CompletionItem(app.label)
18        for app in apps.get_app_configs()
19        if app.label.startswith(incomplete) and app.label not in present
20    ]
21
22
23class Command(TyperCommand):
24    @command()
25    def handle(
26        self,
27        apps: t.Annotated[
28            t.List[str],
29            typer.Argument(
30                help="The app label",
31                # pass the completer function here
32                shell_complete=complete_app_label,
33            ),
34        ],
35    ):
36        print(apps)

Tip

See the ModelObjectCompleter for a completer that works for many Django model field types.

Debugging Tab Completers

Debugging tab completion code can be tricky because when invoked in situ in the shell the completer code is run as a subprocess and it’s output is captured by the shell. This means you can’t set a breakpoint and enter into the debugger easily.

To help with this django-typer provides a debug mode that will enter into the tab-completion logic flow. Use the shellcompletion complete() command, to pass the command line string that you would like to debug. For example:

./manage.py shellcompletion complete "mycommand --"

Provided Completers

Django Apps

import typing as t
import typer
from django.apps import AppConfig
from django_typer import completers, parsers

...

def handle(
    self,
    django_app: t.Annotated[
        AppConfig,
        typer.Argument(
            parser=parsers.parse_app_label,
            shell_complete=completers.complete_app_label,
        ),
    ],
)

Constant Strings

import typing as t
import typer
from enum import StrEnum
from django_typer.completers import these_strings

...

class Shells(StrEnum):

    zsh = 'zsh',
    bash = 'bash'
    fish = 'fish',
    powershell = 'pswh'

def handle(
    self,
    shell: t.Annotated[
        Shells,
        typer.Argument(
            shell_complete=these_strings(list(Shells)),
        ),
    ],
)

Database Aliases

import typing as t
import typer
from django_typer.completers import databases

...

def handle(
    self,
    database: t.Annotated[
        str,
        typer.Argument(
            shell_complete=databases,
        ),
    ],
)

Management Commands

import typing as t
import typer
from django_typer.completers import commands

...

def handle(
    self,
    command: t.Annotated[
        str,
        typer.Argument(
            shell_complete=commands,
        ),
    ],
)

Paths (Files & Directories)

import typing as t
import typer
from pathlib import Path
from django_typer.completers import complete_path

...

def handle(
    self,
    path: t.Annotated[
        Path,
        typer.Argument(
            shell_complete=complete_path,
        ),
    ],
)

Directories

import typing as t
import typer
from pathlib import Path
from django_typer.completers import complete_directory

...

def handle(
    self,
    directory: t.Annotated[
        Path,
        typer.Argument(
            shell_complete=complete_directory,
        ),
    ],
)

Import Paths

Complete python.import.paths - uses sys.path. This completer is used for –settings

import typing as t
import typer
from django_typer.completers import complete_import_path

...

def handle(
    self,
    import_path: t.Annotated[
        str,
        typer.Argument(
            shell_complete=complete_import_path,
        ),
    ],
)

Model Objects

This completer/parser pairing provides the ability to fetch a model object from one of its fields. Most field types are supported. Additionally any other field can be set as the help text that some shells support. Refer to the reference documentation and the polls tutorial for more information.

Warning

If the model table is large you’ll want to make sure the completable field is indexed. When your users hit tab, they’ll have to wait to the matching query to complete. The queries have been optimized to take advantage of indexes for each supported field type.

import typing as t
import typer
from django_typer.management import model_parser_completer

...

def handle(
    self,
    obj: t.Annotated[
        ModelClass,
        typer.Argument(
            **model_parser_completer(
                ModelClass,  # may also accept a QuerySet for pre-filtering
                'field_name',  # the field that should be matched (defaults to id)
                help_field='other_field'  # optionally provide some additional help text
            ),
            help=_("Fetch objects by their field_names.")
        ),
    ]
):
    ...

Completer Chains

Multiple completers can be chained together in a sequence. All completions can be returned, or only the first completer that generates matches.

import typing as t
import typer
from django_typer import completers

...

def handle(
    self,
    command: t.Annotated[
        str,
        typer.Argument(
            # allow commands to be specified by name or import path
            shell_complete=completers.chain(
                completers.complete_import_path,
                completers.commands
            )
        ),
    ],
)