diff --git a/tests/test_completion/test_completion_install.py b/tests/test_completion/test_completion_install.py index d1a4695bc8..ca0f689d84 100644 --- a/tests/test_completion/test_completion_install.py +++ b/tests/test_completion/test_completion_install.py @@ -4,6 +4,7 @@ from pathlib import Path from unittest import mock +import pytest import shellingham import typer from typer.testing import CliRunner @@ -67,41 +68,52 @@ def test_completion_install_bash(): ) -def test_completion_install_zsh(): - completion_path: Path = Path.home() / ".zshrc" - text = "" - if not completion_path.is_file(): # pragma: no cover - completion_path.write_text('echo "custom .zshrc"') - if completion_path.is_file(): - text = completion_path.read_text() - result = subprocess.run( - [ - sys.executable, - "-m", - "coverage", - "run", - mod.__file__, - "--install-completion", - "zsh", - ], - capture_output=True, - encoding="utf-8", - env={ - **os.environ, - "_TYPER_COMPLETE_TEST_DISABLE_SHELL_DETECTION": "True", - }, - ) - new_text = completion_path.read_text() - completion_path.write_text(text) - zfunc_fragment = "fpath+=~/.zfunc" - assert zfunc_fragment in new_text - assert "completion installed in" in result.stdout - assert "Completion will take effect once you restart the terminal" in result.stdout - install_source_path = Path.home() / ".zfunc/_tutorial001.py" - assert install_source_path.is_file() - install_content = install_source_path.read_text() - install_source_path.unlink() - assert "compdef _tutorial001py_completion tutorial001.py" in install_content +@pytest.mark.parametrize( + ("environ_override", "expected_zsh_dir"), + ( + ({}, Path.home()), + ({"ZDOTDIR": "~/.config/zsh"}, Path.home() / ".config/zsh"), + ), + ids=("ZDOTDIR unset", "ZDOTDIR set"), +) +def test_completion_install_zsh(environ_override, expected_zsh_dir): + with mock.patch.dict(os.environ, environ_override, clear=True): + completion_path: Path = expected_zsh_dir / ".zshrc" + text = "" + if not completion_path.is_file(): # pragma: no cover + completion_path.write_text('echo "custom .zshrc"') + if completion_path.is_file(): + text = completion_path.read_text() + result = subprocess.run( + [ + sys.executable, + "-m", + "coverage", + "run", + mod.__file__, + "--install-completion", + "zsh", + ], + capture_output=True, + encoding="utf-8", + env={ + **os.environ, + "_TYPER_COMPLETE_TEST_DISABLE_SHELL_DETECTION": "True", + }, + ) + new_text = completion_path.read_text() + completion_path.write_text(text) + zfunc_fragment = f"fpath+={expected_zsh_dir}/.zfunc" + assert zfunc_fragment in new_text + assert "completion installed in" in result.stdout + assert ( + "Completion will take effect once you restart the terminal" in result.stdout + ) + install_source_path = expected_zsh_dir / ".zfunc/_tutorial001.py" + assert install_source_path.is_file() + install_content = install_source_path.read_text() + install_source_path.unlink() + assert "compdef _tutorial001py_completion tutorial001.py" in install_content def test_completion_install_fish(): diff --git a/typer/_completion_shared.py b/typer/_completion_shared.py index 10de54420d..67be5c628c 100644 --- a/typer/_completion_shared.py +++ b/typer/_completion_shared.py @@ -122,9 +122,12 @@ def install_bash(*, prog_name: str, complete_var: str, shell: str) -> Path: def install_zsh(*, prog_name: str, complete_var: str, shell: str) -> Path: - # Setup Zsh and load ~/.zfunc - zshrc_path = Path.home() / ".zshrc" + # Setup Zsh and load .zfunc + # support ZDOTDIR for custom zsh config location if set (see https://zsh.sourceforge.io/Intro/intro_3.html): + zdotdir_path = Path(os.getenv("ZDOTDIR", default=Path.home())) + zshrc_path = zdotdir_path / ".zshrc" zshrc_path.parent.mkdir(parents=True, exist_ok=True) + zfunc_path = zdotdir_path / ".zfunc" zshrc_content = "" if zshrc_path.is_file(): zshrc_content = zshrc_path.read_text() @@ -132,15 +135,15 @@ def install_zsh(*, prog_name: str, complete_var: str, shell: str) -> Path: "autoload -Uz compinit", "compinit", "zstyle ':completion:*' menu select", - "fpath+=~/.zfunc", + f"fpath+={zfunc_path}", ] for line in completion_init_lines: if line not in zshrc_content: # pragma: no cover zshrc_content += f"\n{line}" zshrc_content += "\n" zshrc_path.write_text(zshrc_content) - # Install completion under ~/.zfunc/ - path_obj = Path.home() / f".zfunc/_{prog_name}" + # Install completion under .zfunc/ + path_obj = zfunc_path / f"_{prog_name}" path_obj.parent.mkdir(parents=True, exist_ok=True) script_content = get_completion_script( prog_name=prog_name, complete_var=complete_var, shell=shell