Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
87aad96
Refactor the handling of shellingham lazy-loading
nathanjmcdougall Sep 20, 2025
9a8a4e6
🎨 [pre-commit.ci] Auto format from pre-commit.com hooks
pre-commit-ci[bot] Sep 20, 2025
aa61b96
Refactor to mypy compliance
nathanjmcdougall Sep 20, 2025
c1d6710
Merge branch 'config/lazy-load-shellingham-via-ruff' of https://githu…
nathanjmcdougall Sep 20, 2025
2b82a1b
🎨 [pre-commit.ci] Auto format from pre-commit.com hooks
pre-commit-ci[bot] Sep 20, 2025
f7e89c8
Remove backticks from comment
nathanjmcdougall Sep 20, 2025
2f0fb63
Rename variable for consistency
nathanjmcdougall Sep 20, 2025
d7f84ba
Ban calls to `shellingham.detect_shell`
nathanjmcdougall Sep 20, 2025
988148b
Merge branch 'config/lazy-load-shellingham-via-ruff' of https://githu…
nathanjmcdougall Sep 20, 2025
9fab46b
Move `_get_shell_name` to `._completion_shared` to avoid circular imp…
nathanjmcdougall Sep 20, 2025
f61dece
🎨 [pre-commit.ci] Auto format from pre-commit.com hooks
pre-commit-ci[bot] Sep 20, 2025
d599732
Use pre-3.10 style type unions
nathanjmcdougall Sep 20, 2025
81a8134
Merge branch 'config/lazy-load-shellingham-via-ruff' of https://githu…
nathanjmcdougall Sep 20, 2025
c39bbe0
Revert to non-lazy loading of shellingham and targetted import ban on…
nathanjmcdougall Oct 5, 2025
fdba4c7
🎨 [pre-commit.ci] Auto format from pre-commit.com hooks
pre-commit-ci[bot] Oct 5, 2025
b62412b
Fix broken reference in mock
nathanjmcdougall Oct 5, 2025
055d18f
Merge branch 'config/lazy-load-shellingham-via-ruff' of https://githu…
nathanjmcdougall Oct 5, 2025
c7cbe11
🎨 [pre-commit.ci] Auto format from pre-commit.com hooks
pre-commit-ci[bot] Oct 5, 2025
2d8be24
Remove unused import
nathanjmcdougall Oct 5, 2025
88295c1
Merge branch 'config/lazy-load-shellingham-via-ruff' of https://githu…
nathanjmcdougall Oct 5, 2025
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
4 changes: 4 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -214,3 +214,7 @@ banned-module-level-imports = ["typer.rich_utils"]

[tool.ruff.lint.flake8-tidy-imports.banned-api]
"rich".msg = "Use 'typer.rich_utils' instead of importing from 'rich' directly."
"shellingham.detect_shell".msg = """\
Use 'typer._completion_shared._get_shell_name' instead of using \
'shellingham.detect_shell' directly.
"""
6 changes: 2 additions & 4 deletions tests/test_completion/test_completion_show.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
import sys
from unittest import mock

import shellingham
import typer
import typer.completion
from typer.testing import CliRunner

from docs_src.commands.index import tutorial001 as mod
Expand Down Expand Up @@ -142,8 +142,6 @@ def test_completion_source_pwsh():


def test_completion_show_invalid_shell():
with mock.patch.object(
shellingham, "detect_shell", return_value=("xshell", "/usr/bin/xshell")
):
with mock.patch.object(typer.completion, "_get_shell_name", return_value="xshell"):
result = runner.invoke(app, ["--show-completion"])
assert "Shell xshell not supported" in result.output
4 changes: 2 additions & 2 deletions tests/test_others.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@

import click
import pytest
import shellingham
import typer
import typer._completion_shared
import typer.completion
from typer.core import _split_opt
from typer.main import solve_typer_info_defaults, solve_typer_info_help
Expand Down Expand Up @@ -85,7 +85,7 @@ def main():
print("Hello World")

with mock.patch.object(
shellingham, "detect_shell", return_value=("xshell", "/usr/bin/xshell")
typer._completion_shared, "_get_shell_name", return_value="xshell"
):
result = runner.invoke(app, ["--install-completion"])
assert "Shell xshell is not supported." in result.stdout
Expand Down
16 changes: 3 additions & 13 deletions tests/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,7 @@
from os import getenv

import pytest

try:
import shellingham
from shellingham import ShellDetectionFailure

shell = shellingham.detect_shell()[0]
except ImportError: # pragma: no cover
shellingham = None
shell = None
except ShellDetectionFailure: # pragma: no cover
shell = None

from typer._completion_shared import _get_shell_name

needs_py310 = pytest.mark.skipif(
sys.version_info < (3, 10), reason="requires python3.10+"
Expand All @@ -23,8 +12,9 @@
not sys.platform.startswith("linux"), reason="Test requires Linux"
)

shell = _get_shell_name()
needs_bash = pytest.mark.skipif(
not shellingham or not shell or "bash" not in shell, reason="Test requires Bash"
shell is None or "bash" not in shell, reason="Test requires Bash"
)

requires_completion_permission = pytest.mark.skipif(
Expand Down
5 changes: 0 additions & 5 deletions typer/_completion_classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,6 @@
split_arg_string as click_split_arg_string,
)

try:
import shellingham
except ImportError: # pragma: no cover
shellingham = None


def _sanitize_help_text(text: str) -> str:
"""Sanitizes the help text by removing rich tags"""
Expand Down
31 changes: 25 additions & 6 deletions typer/_completion_shared.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,13 @@
import subprocess
from enum import Enum
from pathlib import Path
from typing import Optional, Tuple
from typing import Optional, Tuple, Union

import click
from typer.core import HAS_SHELLINGHAM

try:
if HAS_SHELLINGHAM:
import shellingham
except ImportError: # pragma: no cover
shellingham = None


class Shells(str, Enum):
Expand Down Expand Up @@ -213,8 +212,8 @@ def install(
if complete_var is None:
complete_var = "_{}_COMPLETE".format(prog_name.replace("-", "_").upper())
test_disable_detection = os.getenv("_TYPER_COMPLETE_TEST_DISABLE_SHELL_DETECTION")
if shell is None and shellingham is not None and not test_disable_detection:
shell, _ = shellingham.detect_shell()
if shell is None and not test_disable_detection:
shell = _get_shell_name()
if shell == "bash":
installed_path = install_bash(
prog_name=prog_name, complete_var=complete_var, shell=shell
Expand All @@ -238,3 +237,23 @@ def install(
else:
click.echo(f"Shell {shell} is not supported.")
raise click.exceptions.Exit(1)


def _get_shell_name() -> Union[str, None]:
"""Get the current shell name, if available.

The name will always be lowercase. If the shell cannot be detected, None is
returned.
"""
name: Union[str, None] # N.B. shellingham is untyped
if HAS_SHELLINGHAM:
try:
# N.B. detect_shell returns a tuple of (shell name, shell command).
# We only need the name.
name, _cmd = shellingham.detect_shell() # noqa: TID251
except shellingham.ShellDetectionFailure: # pragma: no cover
name = None
else:
name = None # pragma: no cover

return name
17 changes: 7 additions & 10 deletions typer/completion.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,19 @@
import click

from ._completion_classes import completion_init
from ._completion_shared import Shells, get_completion_script, install
from ._completion_shared import Shells, _get_shell_name, get_completion_script, install
from .core import HAS_SHELLINGHAM
from .models import ParamMeta
from .params import Option
from .utils import get_params_from_function

try:
import shellingham
except ImportError: # pragma: no cover
shellingham = None


_click_patched = False


def get_completion_inspect_parameters() -> Tuple[ParamMeta, ParamMeta]:
completion_init()
test_disable_detection = os.getenv("_TYPER_COMPLETE_TEST_DISABLE_SHELL_DETECTION")
if shellingham and not test_disable_detection:
if HAS_SHELLINGHAM and not test_disable_detection:
parameters = get_params_from_function(_install_completion_placeholder_function)
else:
parameters = get_params_from_function(
Expand Down Expand Up @@ -54,8 +49,10 @@ def show_callback(ctx: click.Context, param: click.Parameter, value: Any) -> Any
test_disable_detection = os.getenv("_TYPER_COMPLETE_TEST_DISABLE_SHELL_DETECTION")
if isinstance(value, str):
shell = value
elif shellingham and not test_disable_detection:
shell, _ = shellingham.detect_shell()
elif not test_disable_detection:
detected_shell = _get_shell_name()
if detected_shell is not None:
shell = detected_shell
script_content = get_completion_script(
prog_name=prog_name, complete_var=complete_var, shell=shell
)
Expand Down
1 change: 1 addition & 0 deletions typer/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
MarkupMode = Literal["markdown", "rich", None]

HAS_RICH = importlib.util.find_spec("rich") is not None
HAS_SHELLINGHAM = importlib.util.find_spec("shellingham") is not None

if HAS_RICH:
DEFAULT_MARKUP_MODE: MarkupMode = "rich"
Expand Down
Loading