Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions tests/test_completion/path_typergroup_example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import click
import typer
import typer.core


class DynamicGroup(typer.core.TyperGroup):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.add_command(process_cmd, "process")


process_cmd = click.Command(
name="process",
callback=lambda **kw: print(kw),
params=[
click.Option(
["--input", "-i"],
type=click.Path(exists=False),
required=True,
help="Input file",
),
click.Option(
["--output-dir", "-o"],
type=click.Path(file_okay=False, dir_okay=True),
help="Output directory",
),
click.Option(
["--count", "-n"],
type=int,
default=1,
help="Number of items",
),
],
)

app = typer.Typer()
sub_app = typer.Typer(cls=DynamicGroup)
app.add_typer(sub_app, name="sub")

if __name__ == "__main__":
app()
223 changes: 223 additions & 0 deletions tests/test_completion/test_completion_path.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import sys

from . import path_example as mod
from . import path_typergroup_example as typergroup_mod


def test_script():
Expand All @@ -15,6 +16,25 @@ def test_script():
assert "deadpool" in result.stdout


def test_typergroup_script():
result = subprocess.run(
[
sys.executable,
"-m",
"coverage",
"run",
typergroup_mod.__file__,
"sub",
"process",
"--input",
"/tmp/test",
],
capture_output=True,
encoding="utf-8",
)
assert result.returncode == 0


def test_completion_path_bash():
result = subprocess.run(
[sys.executable, "-m", "coverage", "run", mod.__file__, " "],
Expand All @@ -28,3 +48,206 @@ def test_completion_path_bash():
},
)
assert result.returncode == 0


def test_completion_path_zsh_empty():
result = subprocess.run(
[sys.executable, "-m", "coverage", "run", mod.__file__, " "],
capture_output=True,
encoding="utf-8",
env={
**os.environ,
"_PATH_EXAMPLE.PY_COMPLETE": "complete_zsh",
"_TYPER_COMPLETE_ARGS": "path_example.py ",
},
)
assert result.returncode == 0
assert "_arguments" not in result.stdout


def test_completion_path_zsh_partial():
result = subprocess.run(
[sys.executable, "-m", "coverage", "run", mod.__file__, " "],
capture_output=True,
encoding="utf-8",
env={
**os.environ,
"_PATH_EXAMPLE.PY_COMPLETE": "complete_zsh",
"_TYPER_COMPLETE_ARGS": "path_example.py /tmp/some_part",
},
)
assert result.returncode == 0
assert "_arguments" not in result.stdout


def test_completion_typergroup_path_zsh_empty():
result = subprocess.run(
[sys.executable, "-m", "coverage", "run", typergroup_mod.__file__, " "],
capture_output=True,
encoding="utf-8",
env={
**os.environ,
"_PATH_TYPERGROUP_EXAMPLE.PY_COMPLETE": "complete_zsh",
"_TYPER_COMPLETE_ARGS": "path_typergroup_example.py sub process --input ",
},
)
assert result.returncode == 0
assert "_arguments" not in result.stdout


def test_completion_typergroup_path_zsh_partial():
result = subprocess.run(
[sys.executable, "-m", "coverage", "run", typergroup_mod.__file__, " "],
capture_output=True,
encoding="utf-8",
env={
**os.environ,
"_PATH_TYPERGROUP_EXAMPLE.PY_COMPLETE": "complete_zsh",
"_TYPER_COMPLETE_ARGS": "path_typergroup_example.py sub process --input /tmp/test",
},
)
assert result.returncode == 0
assert "_arguments" not in result.stdout


def test_completion_typergroup_dir_zsh():
result = subprocess.run(
[sys.executable, "-m", "coverage", "run", typergroup_mod.__file__, " "],
capture_output=True,
encoding="utf-8",
env={
**os.environ,
"_PATH_TYPERGROUP_EXAMPLE.PY_COMPLETE": "complete_zsh",
"_TYPER_COMPLETE_ARGS": "path_typergroup_example.py sub process --output-dir ",
},
)
assert result.returncode == 0
assert "_path_files -/" in result.stdout


def test_completion_typergroup_flags_zsh():
result = subprocess.run(
[sys.executable, "-m", "coverage", "run", typergroup_mod.__file__, " "],
capture_output=True,
encoding="utf-8",
env={
**os.environ,
"_PATH_TYPERGROUP_EXAMPLE.PY_COMPLETE": "complete_zsh",
"_TYPER_COMPLETE_ARGS": "path_typergroup_example.py sub process --",
},
)
assert result.returncode == 0
assert "_arguments" in result.stdout
assert "--input" in result.stdout
assert "--count" in result.stdout


def test_completion_typergroup_path_bash_empty():
result = subprocess.run(
[sys.executable, "-m", "coverage", "run", typergroup_mod.__file__, " "],
capture_output=True,
encoding="utf-8",
env={
**os.environ,
"_PATH_TYPERGROUP_EXAMPLE.PY_COMPLETE": "complete_bash",
"COMP_WORDS": "path_typergroup_example.py sub process --input ",
"COMP_CWORD": "4",
},
)
assert result.returncode == 0
assert result.stdout.strip() == ""


def test_completion_typergroup_path_bash_partial():
result = subprocess.run(
[sys.executable, "-m", "coverage", "run", typergroup_mod.__file__, " "],
capture_output=True,
encoding="utf-8",
env={
**os.environ,
"_PATH_TYPERGROUP_EXAMPLE.PY_COMPLETE": "complete_bash",
"COMP_WORDS": "path_typergroup_example.py sub process --input /tmp/test",
"COMP_CWORD": "4",
},
)
assert result.returncode == 0
assert result.stdout.strip() == ""


def test_completion_typergroup_flags_bash():
result = subprocess.run(
[sys.executable, "-m", "coverage", "run", typergroup_mod.__file__, " "],
capture_output=True,
encoding="utf-8",
env={
**os.environ,
"_PATH_TYPERGROUP_EXAMPLE.PY_COMPLETE": "complete_bash",
"COMP_WORDS": "path_typergroup_example.py sub process --",
"COMP_CWORD": "3",
},
)
assert result.returncode == 0
assert "--input" in result.stdout
assert "--count" in result.stdout


def test_completion_typergroup_path_fish_is_args():
result = subprocess.run(
[sys.executable, "-m", "coverage", "run", typergroup_mod.__file__, " "],
capture_output=True,
encoding="utf-8",
env={
**os.environ,
"_PATH_TYPERGROUP_EXAMPLE.PY_COMPLETE": "complete_fish",
"_TYPER_COMPLETE_ARGS": "path_typergroup_example.py sub process --input /tmp/test",
"_TYPER_COMPLETE_FISH_ACTION": "is-args",
},
)
assert result.returncode != 0


def test_completion_typergroup_path_fish_get_args():
result = subprocess.run(
[sys.executable, "-m", "coverage", "run", typergroup_mod.__file__, " "],
capture_output=True,
encoding="utf-8",
env={
**os.environ,
"_PATH_TYPERGROUP_EXAMPLE.PY_COMPLETE": "complete_fish",
"_TYPER_COMPLETE_ARGS": "path_typergroup_example.py sub process --input /tmp/test",
"_TYPER_COMPLETE_FISH_ACTION": "get-args",
},
)
assert result.stdout.strip() == ""


def test_completion_typergroup_path_powershell_empty():
result = subprocess.run(
[sys.executable, "-m", "coverage", "run", typergroup_mod.__file__, " "],
capture_output=True,
encoding="utf-8",
env={
**os.environ,
"_PATH_TYPERGROUP_EXAMPLE.PY_COMPLETE": "complete_powershell",
"_TYPER_COMPLETE_ARGS": "path_typergroup_example.py sub process --input ",
"_TYPER_COMPLETE_WORD_TO_COMPLETE": "",
},
)
assert result.returncode == 0
assert result.stdout.strip() == ""


def test_completion_typergroup_path_powershell_partial():
result = subprocess.run(
[sys.executable, "-m", "coverage", "run", typergroup_mod.__file__, " "],
capture_output=True,
encoding="utf-8",
env={
**os.environ,
"_PATH_TYPERGROUP_EXAMPLE.PY_COMPLETE": "complete_powershell",
"_TYPER_COMPLETE_ARGS": "path_typergroup_example.py sub process --input /tmp/test",
"_TYPER_COMPLETE_WORD_TO_COMPLETE": "/tmp/test",
},
)
assert result.returncode == 0
assert result.stdout.strip() == ""
39 changes: 39 additions & 0 deletions typer/_completion_classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,15 @@ def _sanitize_help_text(text: str) -> str:
return rich_utils.rich_render_text(text)


def _is_path_completion(
completions: list[click.shell_completion.CompletionItem],
) -> bool:
"""Check if completions are all file/dir type (Click's Path type)."""
return bool(completions) and all(
item.type in ("file", "dir") for item in completions
)


class BashComplete(click.shell_completion.BashComplete):
name = Shells.bash.value
source_template = COMPLETION_SCRIPT_BASH
Expand Down Expand Up @@ -59,6 +68,12 @@ def format_completion(self, item: click.shell_completion.CompletionItem) -> str:
def complete(self) -> str:
args, incomplete = self.get_completion_args()
completions = self.get_completions(args, incomplete)

# Return empty so bash falls back to native file completion
# via the "complete -o default" registration.
if _is_path_completion(completions):
return ""

out = [self.format_completion(item) for item in completions]
return "\n".join(out)

Expand Down Expand Up @@ -106,6 +121,13 @@ def escape(s: str) -> str:
def complete(self) -> str:
args, incomplete = self.get_completion_args()
completions = self.get_completions(args, incomplete)

# Emit native zsh path completion instead of wrapping in _arguments.
if _is_path_completion(completions):
if any(item.type == "dir" for item in completions):
return "_path_files -/"
return "_path_files -f"

res = [self.format_completion(item) for item in completions]
if res:
args_str = "\n".join(res)
Expand Down Expand Up @@ -153,6 +175,12 @@ def complete(self) -> str:
complete_action = os.getenv("_TYPER_COMPLETE_FISH_ACTION", "")
args, incomplete = self.get_completion_args()
completions = self.get_completions(args, incomplete)

# Treat path completions as empty so fish falls back to native
# file completion (is-args exits 1, get-args returns nothing).
if _is_path_completion(completions):
completions = []

show_args = [self.format_completion(item) for item in completions]
if complete_action == "get-args":
if show_args:
Expand Down Expand Up @@ -188,6 +216,17 @@ def get_completion_args(self) -> tuple[list[str], str]:
def format_completion(self, item: click.shell_completion.CompletionItem) -> str:
return f"{item.value}:::{_sanitize_help_text(item.help) if item.help else ' '}"

def complete(self) -> str:
args, incomplete = self.get_completion_args()
completions = self.get_completions(args, incomplete)

# Return empty so PowerShell falls back to native file completion.
if _is_path_completion(completions):
return ""

out = [self.format_completion(item) for item in completions]
return "\n".join(out)


def completion_init() -> None:
click.shell_completion.add_completion_class(BashComplete, Shells.bash.value)
Expand Down
Loading