Skip to content
Open
Show file tree
Hide file tree
Changes from 13 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
6 changes: 4 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -209,8 +209,10 @@ known-first-party = ["reigns", "towns", "lands", "items", "users"]
keep-runtime-typing = true

[tool.ruff.lint.flake8-tidy-imports]
# Import rich_utils from within functions (lazy), not at the module level (TID253)
banned-module-level-imports = ["typer.rich_utils"]
# Import rich_utils and shellingham from within functions (lazy),
# not at the module level (TID253)
banned-module-level-imports = ["typer.rich_utils", "shellingham"]

[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."
3 changes: 2 additions & 1 deletion tests/test_completion/test_completion_install.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
from pathlib import Path
from unittest import mock

import shellingham
import typer
from typer.testing import CliRunner

Expand Down Expand Up @@ -141,6 +140,8 @@ def test_completion_install_fish():

@requires_completion_permission
def test_completion_install_powershell():
import shellingham

completion_path: Path = (
Path.home() / ".config/powershell/Microsoft.PowerShell_profile.ps1"
)
Expand Down
3 changes: 2 additions & 1 deletion tests/test_completion/test_completion_show.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import sys
from unittest import mock

import shellingham
import typer
from typer.testing import CliRunner

Expand Down Expand Up @@ -142,6 +141,8 @@ def test_completion_source_pwsh():


def test_completion_show_invalid_shell():
import shellingham

with mock.patch.object(
shellingham, "detect_shell", return_value=("xshell", "/usr/bin/xshell")
):
Expand Down
3 changes: 2 additions & 1 deletion tests/test_others.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@

import click
import pytest
import shellingham
import typer
import typer.completion
from typer.core import _split_opt
Expand Down Expand Up @@ -78,6 +77,8 @@ def convert(

@requires_completion_permission
def test_install_invalid_shell():
import shellingham

app = typer.Typer()

@app.command()
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
34 changes: 26 additions & 8 deletions typer/_completion_shared.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,10 @@
import subprocess
from enum import Enum
from pathlib import Path
from typing import Optional, Tuple
from typing import Optional, Tuple, Union

import click

try:
import shellingham
except ImportError: # pragma: no cover
shellingham = None
from typer.core import HAS_SHELLINGHAM


class Shells(str, Enum):
Expand Down Expand Up @@ -213,8 +209,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 +234,25 @@ 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:
import 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