From 775fe157a624a261264e279fd94f270ee8e12d61 Mon Sep 17 00:00:00 2001 From: Brian Kohan Date: Wed, 18 Dec 2024 13:40:33 -0800 Subject: [PATCH] zshell tests working again #156 --- .../management/commands/shellcompletion.py | 23 ++++++++++++------- django_typer/templates/shell_complete/zsh.sh | 22 +++++++----------- django_typer/utils.py | 2 +- tests/shellcompletion/__init__.py | 8 ++++--- tests/shellcompletion/test_zsh.py | 7 +++++- tests/utils.py | 15 ------------ 6 files changed, 35 insertions(+), 42 deletions(-) diff --git a/django_typer/management/commands/shellcompletion.py b/django_typer/management/commands/shellcompletion.py index 0530694..0787f91 100644 --- a/django_typer/management/commands/shellcompletion.py +++ b/django_typer/management/commands/shellcompletion.py @@ -25,7 +25,6 @@ import re import sys import typing as t -from functools import cached_property from importlib.resources import files from pathlib import Path from types import ModuleType @@ -311,6 +310,7 @@ class Command(TyperCommand): ANSI_ESCAPE_RE = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])") _fallback: t.Optional[t.Callable[[t.List[str], str], t.List[CompletionItem]]] = None + _manage_script: t.Optional[t.Union[str, Path]] = None @property def fallback( @@ -337,7 +337,7 @@ def fallback_import(self) -> t.Optional[str]: else None ) - @cached_property + @property def manage_script(self) -> t.Union[str, Path]: """ Returns the name of the manage command as a string if it is available as a command @@ -354,17 +354,22 @@ def manage_script(self) -> t.Union[str, Path]: # with a manage.py script being invoked directly as a script. Completion should work in # this case as well, but it does complicate the installation for some shell's so we must # first figure out which mode we are in. - script = get_usage_script() - if isinstance(script, Path): - return script.absolute() - return script + if not self._manage_script: + self._manage_script = get_usage_script() + return self._manage_script + + @manage_script.setter + def manage_script(self, script: t.Optional[str]): + self._manage_script = get_usage_script(script) - @cached_property + @property def manage_script_name(self) -> str: """ Get the name of the manage script as a command available from the shell's path. """ - return str(getattr(self.manage_script, "name", self.manage_script)) + if isinstance(self.manage_script, Path): + return self.manage_script.name + return self.manage_script @property def shell(self) -> str: @@ -486,6 +491,7 @@ def install( :convert-png: latex """ self.fallback = fallback # type: ignore[assignment] + self.manage_script = manage_script # type: ignore[assignment] if isinstance(self.manage_script, Path): if not self.shell_class.supports_scripts: raise CommandError( @@ -559,6 +565,7 @@ def uninstall( :convert-png: latex """ + self.manage_script = manage_script # type: ignore[assignment] self.shell_class( prog_name=str(manage_script or self.manage_script_name), command=self, diff --git a/django_typer/templates/shell_complete/zsh.sh b/django_typer/templates/shell_complete/zsh.sh index 3bfbde8..66e87ee 100644 --- a/django_typer/templates/shell_complete/zsh.sh +++ b/django_typer/templates/shell_complete/zsh.sh @@ -9,9 +9,13 @@ # we need to pass these to the complete script - they may be necessary to find the command! local settings_option="" local pythonpath_option="" - - {% if is_installed %} - (( ! $+commands[%(prog_name)s] )) && return 1 + local manage="{% if is_installed %}{{ manage_script_name }}{% endif %}" + {% if not is_installed %} + if [[ ${words[2]} == *{{manage_script_name}} ]]; then + manage="${words[1]} ${words[2]}" + else + manage="${words[1]}" + fi {% endif %} for ((i=1; i<$CURRENT; i++)); do @@ -31,17 +35,7 @@ esac done - {% if not is_installed %} - if [[ ${words[2]} == *{{manage_script_name}} ]]; then - cmd="${words[1]} ${words[2]}" - else - cmd="${words[1]}" - fi - {% else %} - cmd = "{{ manage_script_name }}" - {% endif %} - - response=("${(@f)$("${cmd}" {{ django_command }} --shell zsh ${settings_option:+${settings_option}} ${pythonpath_option:+${pythonpath_option}} {{ color }} complete "${words[*]}")}") + response=("${(@f)$("${manage}" {{ django_command }} --shell zsh ${settings_option:+${settings_option}} ${pythonpath_option:+${pythonpath_option}} {{ color }} complete "${words[*]}")}") for type key descr in ${response}; do if [[ "$type" == "plain" ]]; then diff --git a/django_typer/utils.py b/django_typer/utils.py index b2dda44..d3f4740 100644 --- a/django_typer/utils.py +++ b/django_typer/utils.py @@ -41,7 +41,7 @@ def get_usage_script(script: t.Optional[str] = None) -> t.Union[Path, str]: if shutil.which(cmd_pth.name): return cmd_pth.name try: - return cmd_pth.absolute().relative_to(Path(os.getcwd())) + return cmd_pth.absolute().relative_to(Path(os.getcwd())).absolute() except ValueError: return cmd_pth.absolute() diff --git a/tests/shellcompletion/__init__.py b/tests/shellcompletion/__init__.py index 6ff88bf..5087f39 100644 --- a/tests/shellcompletion/__init__.py +++ b/tests/shellcompletion/__init__.py @@ -148,10 +148,11 @@ def read(fd): print(read(master_fd)) # Send a command with a tab character for completion - os.write(master_fd, (" ".join(cmds)).encode()) + cmd = " ".join(cmds) + os.write(master_fd, cmd.encode()) time.sleep(0.5) - print(f'"{(" ".join(cmds))}"') + print(f'"{cmd}"') os.write(master_fd, b"\t\t") time.sleep(0.5) @@ -159,6 +160,7 @@ def read(fd): # Read the output output = read_all_from_fd_with_timeout(master_fd, 3) + # todo - avoid large output because this can mess things up if "do you wish" in output or "Display all" in output: os.write(master_fd, b"y\n") time.sleep(0.5) @@ -194,7 +196,7 @@ def run_command_completion(self): # annoingly in CI there are some spaces inserted between the incomplete phrase # and the completion on linux in bash specifically self.assertTrue(re.match(r".*complet\s*ion.*", completions)) - completions = self.get_completions(self.launch_script, " ") + completions = self.get_completions(self.launch_script) self.assertIn("adapted", completions) self.assertIn("help_precedence", completions) self.assertIn("closepoll", completions) diff --git a/tests/shellcompletion/test_zsh.py b/tests/shellcompletion/test_zsh.py index f973759..9c79711 100644 --- a/tests/shellcompletion/test_zsh.py +++ b/tests/shellcompletion/test_zsh.py @@ -4,7 +4,7 @@ import pytest from django.test import TestCase -from tests.shellcompletion import _DefaultCompleteTestCase +from tests.shellcompletion import _DefaultCompleteTestCase, _InstalledScriptTestCase @pytest.mark.skipif(shutil.which("zsh") is None, reason="Z-Shell not available") @@ -21,3 +21,8 @@ def verify_remove(self, script=None): if not script: script = self.manage_script self.assertFalse((self.directory / f"_{script}").exists()) + + +@pytest.mark.skipif(shutil.which("zsh") is None, reason="Z-Shell not available") +class ZshExeShellTests(_InstalledScriptTestCase, ZshShellTests, TestCase): + shell = "zsh" diff --git a/tests/utils.py b/tests/utils.py index 73a53a2..fc36a6b 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -248,18 +248,3 @@ def read_django_parameters(): def to_platform_str(path: str) -> str: return path.replace("/", os.path.sep) - - -def install_manage_script(name="manage"): - """ - We don't want to deliver bogus entry_points so we manually install - manage.py to our distro's bin directory for shell complete testing. - """ - bin_dir = Path(sys.executable).parent - with open(bin_dir / name, "wt") as ms: - ms.write(f"#!{sys.executable}{os.linesep}") - ms.write(f"import sys{os.linesep}") - ms.write(f"from tests.manage import main{os.linesep}") - ms.write(f"{os.linesep}") - ms.write(f"if __name__ == '__main__':{os.linesep}") - ms.write(f" sys.exit(main()){os.linesep}")