diff --git a/pyproject.toml b/pyproject.toml index 62acd7d5cb..e7f7b995b2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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. +""" diff --git a/tests/test_completion/test_completion_show.py b/tests/test_completion/test_completion_show.py index 10d8b6ff39..a0f0fdff91 100644 --- a/tests/test_completion/test_completion_show.py +++ b/tests/test_completion/test_completion_show.py @@ -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 @@ -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 diff --git a/tests/test_others.py b/tests/test_others.py index 93f8728072..b770000d92 100644 --- a/tests/test_others.py +++ b/tests/test_others.py @@ -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 @@ -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 diff --git a/tests/utils.py b/tests/utils.py index 019b006fa0..ee190d8438 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -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+" @@ -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( diff --git a/typer/_completion_classes.py b/typer/_completion_classes.py index 5980248afe..b726865e93 100644 --- a/typer/_completion_classes.py +++ b/typer/_completion_classes.py @@ -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""" diff --git a/typer/_completion_shared.py b/typer/_completion_shared.py index cc0add992c..21cc98bc5c 100644 --- a/typer/_completion_shared.py +++ b/typer/_completion_shared.py @@ -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): @@ -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 @@ -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 diff --git a/typer/completion.py b/typer/completion.py index c355baa781..b2080c05b1 100644 --- a/typer/completion.py +++ b/typer/completion.py @@ -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( @@ -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 ) diff --git a/typer/core.py b/typer/core.py index 048f28c137..7f555539b5 100644 --- a/typer/core.py +++ b/typer/core.py @@ -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"