Skip to content

Commit

Permalink
fix #163
Browse files Browse the repository at this point in the history
  • Loading branch information
bckohan committed Feb 3, 2025
1 parent 4b5c82d commit ad5c42c
Show file tree
Hide file tree
Showing 28 changed files with 662 additions and 592 deletions.
115 changes: 115 additions & 0 deletions django_typer/completers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
"""
Typer_ and click_ provide tab-completion hooks for individual parameters. As with
:mod:`~django_typer.parsers` custom completion logic can be implemented for custom
parameter types and added to the annotation of the parameter. Previous versions of
Typer_ supporting click_ 7 used the autocompletion argument to provide completion
logic, Typer_ still supports this, but passing ``shell_complete`` to the annotation is
the preferred way to do this.
This package provides some completer functions and classes that work with common Django_
types:
- **models**: Complete model object field strings using :class:`~django_typer.completers.model.ModelObjectCompleter`.
- **django apps**: Complete app labels or names using :func:`~django_typer.completers.apps.app_labels`.
- **commands**: Complete Django command names using :func:`~django_typer.completers.cmd.commands`.
- **databases**: Complete Django database names using :func:`~django_typer.completers.db.databases`.
- **import paths**: Complete Django database names using :func:`~django_typer.completers.path.import_paths`.
- **paths**: Complete Django database names using :func:`~django_typer.completers.path.paths`.
- **directories**: Complete Django database names using :func:`~django_typer.completers.path.directories`.
"""

import typing as t

from click import Context, Parameter
from click.core import ParameterSource
from click.shell_completion import CompletionItem

Completer = t.Callable[[Context, Parameter, str], t.List[CompletionItem]]
Strings = t.Union[t.Sequence[str], t.KeysView[str], t.Generator[str, None, None]]


def these_strings(
strings: t.Union[t.Callable[[], Strings], Strings],
allow_duplicates: bool = False,
):
"""
Get a completer that provides completion logic that matches the allowed strings.
:param strings: A sequence of allowed strings or a callable that generates a sequence of
allowed strings.
:param allow_duplicates: Whether or not to allow duplicate values. Defaults to False.
:return: A completer function.
"""

def complete(ctx: Context, param: Parameter, incomplete: str):
present = []
if (
not allow_duplicates
and param.name
and ctx.get_parameter_source(param.name) is not ParameterSource.DEFAULT
):
present = [value for value in (ctx.params.get(param.name) or [])]
return [
CompletionItem(item)
for item in (strings() if callable(strings) else strings)
if item.startswith(incomplete) and item not in present
]

return complete


def chain(
completer: Completer,
*completers: Completer,
first_match: bool = False,
allow_duplicates: bool = False,
):
"""
Run through the given completers and return the items from the first one, or all
of them if first_match is False.
.. note::
This function is also useful for filtering out previously supplied duplicate
values for completers that do not natively support that:
.. code-block:: python
shell_complete=chain(
complete_import_path,
allow_duplicates=False
)
:param completer: The first completer to use (must be at least one!)
:param completers: The completers to use
:param first_match: If true, return only the matches from the first completer that
finds completions. Default: False
:param allow_duplicates: If False (default) remove completions from previously provided
values.
"""

def complete(ctx: Context, param: Parameter, incomplete: str):
completions = []
present = []
if (
not allow_duplicates
and param.name
and ctx.get_parameter_source(param.name) is not ParameterSource.DEFAULT
):
present = [value for value in (ctx.params.get(param.name) or [])]
for cmpltr in [completer, *completers]:
completions.extend(cmpltr(ctx, param, incomplete))
if first_match and completions:
break

# eliminate duplicates
return list(
{
ci.value: ci
for ci in completions
if ci.value
if ci.value not in present
}.values()
)

return complete
64 changes: 64 additions & 0 deletions django_typer/completers/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import typing as t

from click import Context, Parameter
from click.core import ParameterSource
from click.shell_completion import CompletionItem
from django.apps import apps


def app_labels(
ctx: Context, param: Parameter, incomplete: str
) -> t.List[CompletionItem]:
"""
A case-sensitive completer for Django app labels or names. The completer
prefers labels but names will also work.
.. code-block:: python
import typing as t
import typer
from django_typer.management import TyperCommand
from django_typer.parsers import parse_app_label
from django_typer.completers import complete_app_label
class Command(TyperCommand):
def handle(
self,
django_apps: t.Annotated[
t.List[AppConfig],
typer.Argument(
parser=parse_app_label,
shell_complete=complete_app_label,
help=_("One or more application labels.")
)
]
):
...
:param ctx: The click context.
:param param: The click parameter.
:param incomplete: The incomplete string.
:return: A list of matching app labels or names. Labels already present for the
parameter on the command line will be filtered out.
"""
present = []
if (
param.name
and ctx.get_parameter_source(param.name) is not ParameterSource.DEFAULT
):
present = [app.label for app in (ctx.params.get(param.name) or [])]
ret = [
CompletionItem(app.label)
for app in apps.get_app_configs()
if app.label.startswith(incomplete) and app.label not in present
]
if not ret and incomplete:
ret = [
CompletionItem(app.name)
for app in apps.get_app_configs()
if app.name.startswith(incomplete)
and app.name not in present
and app.label not in present
]
return ret
13 changes: 13 additions & 0 deletions django_typer/completers/cmd.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from functools import partial

from django.core.management import get_commands

from . import these_strings

commands = partial(these_strings, lambda: get_commands().keys())
"""
A completer that completes management command names.
:param allow_duplicates: Whether or not to allow duplicate values. Defaults to False.
:return: A completer function.
"""
14 changes: 14 additions & 0 deletions django_typer/completers/db.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from functools import partial

from django.conf import settings

from . import these_strings

# use a function that returns a generator because we should not access settings on import
databases = partial(these_strings, lambda: settings.DATABASES.keys())
"""
A completer that completes Django database aliases configured in settings.DATABASES.
:param allow_duplicates: Whether or not to allow duplicate values. Defaults to False.
:return: A completer function.
"""
Loading

0 comments on commit ad5c42c

Please sign in to comment.