diff --git a/tests/test_completion/test_completion_install.py b/tests/test_completion/test_completion_install.py index f6194695fe..bb02c6530c 100644 --- a/tests/test_completion/test_completion_install.py +++ b/tests/test_completion/test_completion_install.py @@ -110,6 +110,47 @@ def test_completion_install_zsh(): ) +@requires_completion_permission +def test_completion_install_zsh_zdotdir(tmp_path: Path): + """Test that ZDOTDIR is respected for zsh completion installation.""" + zdotdir = tmp_path / "custom_zsh" + zdotdir.mkdir() + zshrc_path = zdotdir / ".zshrc" + zshrc_path.write_text('echo "custom .zshrc"\n') + 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", + "ZDOTDIR": str(zdotdir), + }, + ) + assert "completion installed in" in result.stdout + assert "Completion will take effect once you restart the terminal" in result.stdout + # .zshrc should be modified in ZDOTDIR, not HOME + new_text = zshrc_path.read_text() + assert "compinit" in new_text + assert "$ZDOTDIR/.zfunc" in new_text + # Completion file should be installed under ZDOTDIR/.zfunc/ + install_source_path = zdotdir / ".zfunc/_tutorial001_py310.py" + assert install_source_path.is_file() + install_content = install_source_path.read_text() + assert ( + "compdef _tutorial001_py310py_completion tutorial001_py310.py" + in install_content + ) + + @requires_completion_permission def test_completion_install_fish(): script_path = Path(mod.__file__) diff --git a/typer/_completion_shared.py b/typer/_completion_shared.py index 5a81dcf68c..c4f61fa911 100644 --- a/typer/_completion_shared.py +++ b/typer/_completion_shared.py @@ -118,12 +118,17 @@ 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" + # Respect ZDOTDIR if set, as zsh uses it to locate .zshrc + zdotdir = os.environ.get("ZDOTDIR") + zsh_home = Path(zdotdir) if zdotdir else Path.home() + zshrc_path = zsh_home / ".zshrc" zshrc_path.parent.mkdir(parents=True, exist_ok=True) zshrc_content = "" if zshrc_path.is_file(): zshrc_content = zshrc_path.read_text() - completion_line = "fpath+=~/.zfunc; autoload -Uz compinit; compinit" + zfunc_path = zsh_home / ".zfunc" + zfunc_str = f"$ZDOTDIR/.zfunc" if zdotdir else "~/.zfunc" + completion_line = f"fpath+={zfunc_str}; autoload -Uz compinit; compinit" if completion_line not in zshrc_content: zshrc_content += f"\n{completion_line}\n" style_line = "zstyle ':completion:*' menu select" @@ -134,8 +139,8 @@ def install_zsh(*, prog_name: str, complete_var: str, shell: str) -> Path: zshrc_content += f"\n{style_line}\n" zshrc_content = f"{zshrc_content.strip()}\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 = zsh_home / f".zfunc/_{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