Skip to content

Commit

Permalink
Support ZDOTDIR in zsh completion
Browse files Browse the repository at this point in the history
zsh supports placing its startup files in a directory other than $HOME, [when the user specifies $ZDOTDIR in ~/.zshenv](https://zsh.sourceforge.io/Intro/intro_3.html).

typer currently always writes to `~/.zshrc` and `~/.zfunc`. This means that for users that have $ZDOTDIR set to something other than $HOME, `typer --install-completion` has no effect.

Update `install_zsh` to install completion to `$ZDOTDIR/.zshrc` and `$ZDOTDIR/.zfunc/` when set.
  • Loading branch information
Chris Hepner committed Jun 24, 2024
1 parent 04eba6b commit 6a40926
Show file tree
Hide file tree
Showing 2 changed files with 55 additions and 40 deletions.
82 changes: 47 additions & 35 deletions tests/test_completion/test_completion_install.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from pathlib import Path
from unittest import mock

import pytest
import shellingham
import typer
from typer.testing import CliRunner
Expand Down Expand Up @@ -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():
Expand Down
13 changes: 8 additions & 5 deletions typer/_completion_shared.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,25 +122,28 @@ 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()
completion_init_lines = [
"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
Expand Down

0 comments on commit 6a40926

Please sign in to comment.