diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2644ff1a..cc36b5fb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,6 +21,9 @@ jobs: run: | [[ $(uname) == Linux ]] && sudo apt-get install --yes rpm tcsh fish zsh [[ $(uname) == Darwin ]] && brew install bash tcsh fish + # Some runners have python-argcomplete preinstalled + # as a dependency of pipx, which interferes with the tests. + [[ $(uname) == Darwin ]] && brew uninstall --ignore-dependencies python-argcomplete || true python -m pip install --quiet --upgrade codecov - run: make install - run: make lint diff --git a/argcomplete/bash_completion.d/_python-argcomplete b/argcomplete/bash_completion.d/_python-argcomplete index 1e4c66ce..cdb60a96 100644 --- a/argcomplete/bash_completion.d/_python-argcomplete +++ b/argcomplete/bash_completion.d/_python-argcomplete @@ -138,12 +138,6 @@ _python_argcomplete_global() { req_argv=( "" "${COMP_WORDS[@]:1}" ) __python_argcomplete_expand_tilde_by_ref executable else - if [[ "$service" != "-default-" ]]; then - # TODO: this may not be sufficient - see https://zsh.sourceforge.io/Doc/Release/Completion-System.html - # May need to call _complete with avoid-completer=_python-argcomplete or something like that - _default - return - fi executable="${words[1]}" req_argv=( "${words[@]:1}" ) fi @@ -208,7 +202,15 @@ _python_argcomplete_global() { _ARGCOMPLETE_SHELL="zsh" \ _ARGCOMPLETE_SUPPRESS_SPACE=1 \ __python_argcomplete_run "$executable" "${(@)req_argv[1, ${ARGCOMPLETE}-1]}")) - _describe "$executable" completions + local nosort=() + local nospace=() + if is-at-least 5.8; then + nosort=(-o nosort) + fi + if [[ "${completions-}" =~ ([^\\]): && "${BASH_REMATCH[2]}" =~ [=/:] ]]; then + nospace=(-S '') + fi + _describe "$executable" completions "${nosort[@]}" "${nospace[@]}" else COMPREPLY=($(IFS="$IFS" \ COMP_LINE="$COMP_LINE" \ @@ -234,5 +236,9 @@ _python_argcomplete_global() { if [[ -z "${ZSH_VERSION-}" ]]; then complete -o default -o bashdefault -D -F _python_argcomplete_global else - compdef _python_argcomplete_global -P '*' + autoload is-at-least + # Replace only the default completer (_default). + # There are many other special contexts we don't want to override. + # https://zsh.sourceforge.io/Doc/Release/Completion-System.html + compdef _python_argcomplete_global -default- fi diff --git a/argcomplete/shell_integration.py b/argcomplete/shell_integration.py index 53b8e182..73214bb6 100644 --- a/argcomplete/shell_integration.py +++ b/argcomplete/shell_integration.py @@ -42,7 +42,15 @@ _ARGCOMPLETE_SHELL="zsh" \ _ARGCOMPLETE_SUPPRESS_SPACE=1 \ __python_argcomplete_run ${script:-${words[1]}})) - _describe "${words[1]}" completions -o nosort + local nosort=() + local nospace=() + if is-at-least 5.8; then + nosort=(-o nosort) + fi + if [[ "${completions-}" =~ ([^\\]): && "${match[1]}" =~ [=/:] ]]; then + nospace=(-S '') + fi + _describe "${words[1]}" completions "${nosort[@]}" "${nospace[@]}" else local SUPPRESS_SPACE=0 if compopt +o nospace 2> /dev/null; then @@ -67,6 +75,7 @@ if [[ -z "${ZSH_VERSION-}" ]]; then complete %(complete_opts)s -F _python_argcomplete%(function_suffix)s %(executables)s else + autoload is-at-least compdef _python_argcomplete%(function_suffix)s %(executables)s fi """ diff --git a/test/test.py b/test/test.py index d94e55ac..b741a585 100755 --- a/test/test.py +++ b/test/test.py @@ -77,6 +77,8 @@ def bash_repl(command="bash"): def zsh_repl(command="zsh"): sh = _repl_sh(command, ["--no-rcs", "-V"], non_printable_insert="%(!..)") sh.run_command("autoload compinit; compinit -u") + # Require two tabs to print all options (some tests rely on this). + sh.run_command("setopt BASH_AUTO_LIST") return sh @@ -1256,9 +1258,6 @@ def setUp(self): path = ":".join([os.path.join(BASE_DIR, "scripts"), TEST_DIR, "$PATH"]) sh.run_command("export PATH={0}".format(path)) sh.run_command("export PYTHONPATH={0}".format(BASE_DIR)) - if self.repl_provider == bash_repl: - # Disable the "python" module provided by bash-completion - sh.run_command("complete -r python python2 python3") output = sh.run_command(self.install_cmd) self.assertEqual(output, "") # Register a dummy completion with an external argcomplete script @@ -1313,18 +1312,24 @@ class TestZsh(TestBashZshBase, unittest.TestCase): "test_parse_special_characters_dollar", "test_comp_point", # FIXME "test_completion_environment", # FIXME - "test_continuation", # FIXME - "test_wordbreak_chars", # FIXME ] def repl_provider(self): return zsh_repl() -@unittest.skipIf(BASH_MAJOR_VERSION < 4, "complete -D not supported") -class TestBashGlobal(TestBash): +class TestBashZshGlobalBase(TestBashZshBase): install_cmd = 'eval "$(activate-global-python-argcomplete --dest=-)"' + def test_redirection_completion(self): + with TempDir(prefix="test_dir_py", dir="."): + self.sh.run_command("cd " + os.getcwd()) + self.sh.run_command("echo failure > ./foo.txt") + self.sh.run_command("echo success > ./foo.\t") + with open("foo.txt") as f: + msg = f.read() + self.assertEqual(msg, "success\n") + def test_python_completion(self): self.sh.run_command("cd " + TEST_DIR) self.assertEqual(self.sh.run_command("python3 ./prog basic f\t"), "foo\r\n") @@ -1364,9 +1369,6 @@ def _test_console_script(self, package=False, wheel=False): command = "pip install {} --target .".format(test_package) if not wheel: command += " --no-binary :all:" - if sys.platform == "darwin": - # Work around https://stackoverflow.com/questions/24257803 - command += ' --install-option="--prefix="' install_output = self.sh.run_command(command) self.assertEqual(self.sh.run_command("echo $?"), "0\r\n", install_output) command = "test-module" @@ -1375,27 +1377,32 @@ def _test_console_script(self, package=False, wheel=False): command += " a\t" self.assertEqual(self.sh.run_command(command), "arg\r\n") - @unittest.skipIf(os.uname()[0] == "Darwin", "Skip test that fails on MacOS") def test_console_script_module(self): """Test completing a console_script for a module.""" self._test_console_script() - @unittest.skipIf(os.uname()[0] == "Darwin", "Skip test that fails on MacOS") def test_console_script_package(self): """Test completing a console_script for a package.""" self._test_console_script(package=True) - @unittest.skipIf(os.uname()[0] == "Darwin", "Skip test that fails on MacOS") def test_console_script_module_wheel(self): """Test completing a console_script for a module from a wheel.""" self._test_console_script(wheel=True) - @unittest.skipIf(os.uname()[0] == "Darwin", "Skip test that fails on MacOS") def test_console_script_package_wheel(self): """Test completing a console_script for a package from a wheel.""" self._test_console_script(package=True, wheel=True) +@unittest.skipIf(BASH_MAJOR_VERSION < 4, "complete -D not supported") +class TestBashGlobal(TestBash, TestBashZshGlobalBase): + pass + + +class TestZshGlobal(TestZsh, TestBashZshGlobalBase): + pass + + class Shell: def __init__(self, shell): self.child = pexpect.spawn(shell, encoding="utf-8")