test_shell_completion.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416
  1. import pytest
  2. import click.shell_completion
  3. from click.core import Argument
  4. from click.core import Command
  5. from click.core import Group
  6. from click.core import Option
  7. from click.shell_completion import add_completion_class
  8. from click.shell_completion import CompletionItem
  9. from click.shell_completion import ShellComplete
  10. from click.types import Choice
  11. from click.types import File
  12. from click.types import Path
  13. def _get_completions(cli, args, incomplete):
  14. comp = ShellComplete(cli, {}, cli.name, "_CLICK_COMPLETE")
  15. return comp.get_completions(args, incomplete)
  16. def _get_words(cli, args, incomplete):
  17. return [c.value for c in _get_completions(cli, args, incomplete)]
  18. def test_command():
  19. cli = Command("cli", params=[Option(["-t", "--test"])])
  20. assert _get_words(cli, [], "") == []
  21. assert _get_words(cli, [], "-") == ["-t", "--test", "--help"]
  22. assert _get_words(cli, [], "--") == ["--test", "--help"]
  23. assert _get_words(cli, [], "--t") == ["--test"]
  24. # -t has been seen, so --test isn't suggested
  25. assert _get_words(cli, ["-t", "a"], "-") == ["--help"]
  26. def test_group():
  27. cli = Group("cli", params=[Option(["-a"])], commands=[Command("x"), Command("y")])
  28. assert _get_words(cli, [], "") == ["x", "y"]
  29. assert _get_words(cli, [], "-") == ["-a", "--help"]
  30. def test_group_command_same_option():
  31. cli = Group(
  32. "cli", params=[Option(["-a"])], commands=[Command("x", params=[Option(["-a"])])]
  33. )
  34. assert _get_words(cli, [], "-") == ["-a", "--help"]
  35. assert _get_words(cli, ["-a", "a"], "-") == ["--help"]
  36. assert _get_words(cli, ["-a", "a", "x"], "-") == ["-a", "--help"]
  37. assert _get_words(cli, ["-a", "a", "x", "-a", "a"], "-") == ["--help"]
  38. def test_chained():
  39. cli = Group(
  40. "cli",
  41. chain=True,
  42. commands=[
  43. Command("set", params=[Option(["-y"])]),
  44. Command("start"),
  45. Group("get", commands=[Command("full")]),
  46. ],
  47. )
  48. assert _get_words(cli, [], "") == ["get", "set", "start"]
  49. assert _get_words(cli, [], "s") == ["set", "start"]
  50. assert _get_words(cli, ["set", "start"], "") == ["get"]
  51. # subcommands and parent subcommands
  52. assert _get_words(cli, ["get"], "") == ["full", "set", "start"]
  53. assert _get_words(cli, ["get", "full"], "") == ["set", "start"]
  54. assert _get_words(cli, ["get"], "s") == ["set", "start"]
  55. def test_help_option():
  56. cli = Group("cli", commands=[Command("with"), Command("no", add_help_option=False)])
  57. assert _get_words(cli, ["with"], "--") == ["--help"]
  58. assert _get_words(cli, ["no"], "--") == []
  59. def test_argument_order():
  60. cli = Command(
  61. "cli",
  62. params=[
  63. Argument(["plain"]),
  64. Argument(["c1"], type=Choice(["a1", "a2", "b"])),
  65. Argument(["c2"], type=Choice(["c1", "c2", "d"])),
  66. ],
  67. )
  68. # first argument has no completions
  69. assert _get_words(cli, [], "") == []
  70. assert _get_words(cli, [], "a") == []
  71. # first argument filled, now completion can happen
  72. assert _get_words(cli, ["x"], "a") == ["a1", "a2"]
  73. assert _get_words(cli, ["x", "b"], "d") == ["d"]
  74. def test_argument_default():
  75. cli = Command(
  76. "cli",
  77. add_help_option=False,
  78. params=[
  79. Argument(["a"], type=Choice(["a"]), default="a"),
  80. Argument(["b"], type=Choice(["b"]), default="b"),
  81. ],
  82. )
  83. assert _get_words(cli, [], "") == ["a"]
  84. assert _get_words(cli, ["a"], "b") == ["b"]
  85. # ignore type validation
  86. assert _get_words(cli, ["x"], "b") == ["b"]
  87. def test_type_choice():
  88. cli = Command("cli", params=[Option(["-c"], type=Choice(["a1", "a2", "b"]))])
  89. assert _get_words(cli, ["-c"], "") == ["a1", "a2", "b"]
  90. assert _get_words(cli, ["-c"], "a") == ["a1", "a2"]
  91. assert _get_words(cli, ["-c"], "a2") == ["a2"]
  92. def test_choice_special_characters():
  93. cli = Command("cli", params=[Option(["-c"], type=Choice(["!1", "!2", "+3"]))])
  94. assert _get_words(cli, ["-c"], "") == ["!1", "!2", "+3"]
  95. assert _get_words(cli, ["-c"], "!") == ["!1", "!2"]
  96. assert _get_words(cli, ["-c"], "!2") == ["!2"]
  97. def test_choice_conflicting_prefix():
  98. cli = Command(
  99. "cli",
  100. params=[
  101. Option(["-c"], type=Choice(["!1", "!2", "+3"])),
  102. Option(["+p"], is_flag=True),
  103. ],
  104. )
  105. assert _get_words(cli, ["-c"], "") == ["!1", "!2", "+3"]
  106. assert _get_words(cli, ["-c"], "+") == ["+p"]
  107. def test_option_count():
  108. cli = Command("cli", params=[Option(["-c"], count=True)])
  109. assert _get_words(cli, ["-c"], "") == []
  110. assert _get_words(cli, ["-c"], "-") == ["--help"]
  111. def test_option_optional():
  112. cli = Command(
  113. "cli",
  114. add_help_option=False,
  115. params=[
  116. Option(["--name"], is_flag=False, flag_value="value"),
  117. Option(["--flag"], is_flag=True),
  118. ],
  119. )
  120. assert _get_words(cli, ["--name"], "") == []
  121. assert _get_words(cli, ["--name"], "-") == ["--flag"]
  122. assert _get_words(cli, ["--name", "--flag"], "-") == []
  123. @pytest.mark.parametrize(
  124. ("type", "expect"),
  125. [(File(), "file"), (Path(), "file"), (Path(file_okay=False), "dir")],
  126. )
  127. def test_path_types(type, expect):
  128. cli = Command("cli", params=[Option(["-f"], type=type)])
  129. out = _get_completions(cli, ["-f"], "ab")
  130. assert len(out) == 1
  131. c = out[0]
  132. assert c.value == "ab"
  133. assert c.type == expect
  134. def test_absolute_path():
  135. cli = Command("cli", params=[Option(["-f"], type=Path())])
  136. out = _get_completions(cli, ["-f"], "/ab")
  137. assert len(out) == 1
  138. c = out[0]
  139. assert c.value == "/ab"
  140. def test_option_flag():
  141. cli = Command(
  142. "cli",
  143. add_help_option=False,
  144. params=[
  145. Option(["--on/--off"]),
  146. Argument(["a"], type=Choice(["a1", "a2", "b"])),
  147. ],
  148. )
  149. assert _get_words(cli, [], "--") == ["--on", "--off"]
  150. # flag option doesn't take value, use choice argument
  151. assert _get_words(cli, ["--on"], "a") == ["a1", "a2"]
  152. def test_option_custom():
  153. def custom(ctx, param, incomplete):
  154. return [incomplete.upper()]
  155. cli = Command(
  156. "cli",
  157. params=[
  158. Argument(["x"]),
  159. Argument(["y"]),
  160. Argument(["z"], shell_complete=custom),
  161. ],
  162. )
  163. assert _get_words(cli, ["a", "b"], "") == [""]
  164. assert _get_words(cli, ["a", "b"], "c") == ["C"]
  165. def test_option_multiple():
  166. cli = Command(
  167. "type",
  168. params=[Option(["-m"], type=Choice(["a", "b"]), multiple=True), Option(["-f"])],
  169. )
  170. assert _get_words(cli, ["-m"], "") == ["a", "b"]
  171. assert "-m" in _get_words(cli, ["-m", "a"], "-")
  172. assert _get_words(cli, ["-m", "a", "-m"], "") == ["a", "b"]
  173. # used single options aren't suggested again
  174. assert "-c" not in _get_words(cli, ["-c", "f"], "-")
  175. def test_option_nargs():
  176. cli = Command("cli", params=[Option(["-c"], type=Choice(["a", "b"]), nargs=2)])
  177. assert _get_words(cli, ["-c"], "") == ["a", "b"]
  178. assert _get_words(cli, ["-c", "a"], "") == ["a", "b"]
  179. assert _get_words(cli, ["-c", "a", "b"], "") == []
  180. def test_argument_nargs():
  181. cli = Command(
  182. "cli",
  183. params=[
  184. Argument(["x"], type=Choice(["a", "b"]), nargs=2),
  185. Argument(["y"], type=Choice(["c", "d"]), nargs=-1),
  186. Option(["-z"]),
  187. ],
  188. )
  189. assert _get_words(cli, [], "") == ["a", "b"]
  190. assert _get_words(cli, ["a"], "") == ["a", "b"]
  191. assert _get_words(cli, ["a", "b"], "") == ["c", "d"]
  192. assert _get_words(cli, ["a", "b", "c"], "") == ["c", "d"]
  193. assert _get_words(cli, ["a", "b", "c", "d"], "") == ["c", "d"]
  194. assert _get_words(cli, ["a", "-z", "1"], "") == ["a", "b"]
  195. assert _get_words(cli, ["a", "-z", "1", "b"], "") == ["c", "d"]
  196. def test_double_dash():
  197. cli = Command(
  198. "cli",
  199. add_help_option=False,
  200. params=[
  201. Option(["--opt"]),
  202. Argument(["name"], type=Choice(["name", "--", "-o", "--opt"])),
  203. ],
  204. )
  205. assert _get_words(cli, [], "-") == ["--opt"]
  206. assert _get_words(cli, ["value"], "-") == ["--opt"]
  207. assert _get_words(cli, [], "") == ["name", "--", "-o", "--opt"]
  208. assert _get_words(cli, ["--"], "") == ["name", "--", "-o", "--opt"]
  209. def test_hidden():
  210. cli = Group(
  211. "cli",
  212. commands=[
  213. Command(
  214. "hidden",
  215. add_help_option=False,
  216. hidden=True,
  217. params=[
  218. Option(["-a"]),
  219. Option(["-b"], type=Choice(["a", "b"]), hidden=True),
  220. ],
  221. )
  222. ],
  223. )
  224. assert "hidden" not in _get_words(cli, [], "")
  225. assert "hidden" not in _get_words(cli, [], "hidden")
  226. assert _get_words(cli, ["hidden"], "-") == ["-a"]
  227. assert _get_words(cli, ["hidden", "-b"], "") == ["a", "b"]
  228. def test_add_different_name():
  229. cli = Group("cli", commands={"renamed": Command("original")})
  230. words = _get_words(cli, [], "")
  231. assert "renamed" in words
  232. assert "original" not in words
  233. def test_completion_item_data():
  234. c = CompletionItem("test", a=1)
  235. assert c.a == 1
  236. assert c.b is None
  237. @pytest.fixture()
  238. def _patch_for_completion(monkeypatch):
  239. monkeypatch.setattr(
  240. "click.shell_completion.BashComplete._check_version", lambda self: True
  241. )
  242. @pytest.mark.parametrize("shell", ["bash", "zsh", "fish"])
  243. @pytest.mark.usefixtures("_patch_for_completion")
  244. def test_full_source(runner, shell):
  245. cli = Group("cli", commands=[Command("a"), Command("b")])
  246. result = runner.invoke(cli, env={"_CLI_COMPLETE": f"{shell}_source"})
  247. assert f"_CLI_COMPLETE={shell}_complete" in result.output
  248. @pytest.mark.parametrize(
  249. ("shell", "env", "expect"),
  250. [
  251. ("bash", {"COMP_WORDS": "", "COMP_CWORD": "0"}, "plain,a\nplain,b\n"),
  252. ("bash", {"COMP_WORDS": "a b", "COMP_CWORD": "1"}, "plain,b\n"),
  253. ("zsh", {"COMP_WORDS": "", "COMP_CWORD": "0"}, "plain\na\n_\nplain\nb\nbee\n"),
  254. ("zsh", {"COMP_WORDS": "a b", "COMP_CWORD": "1"}, "plain\nb\nbee\n"),
  255. ("fish", {"COMP_WORDS": "", "COMP_CWORD": ""}, "plain,a\nplain,b\tbee\n"),
  256. ("fish", {"COMP_WORDS": "a b", "COMP_CWORD": "b"}, "plain,b\tbee\n"),
  257. ],
  258. )
  259. @pytest.mark.usefixtures("_patch_for_completion")
  260. def test_full_complete(runner, shell, env, expect):
  261. cli = Group("cli", commands=[Command("a"), Command("b", help="bee")])
  262. env["_CLI_COMPLETE"] = f"{shell}_complete"
  263. result = runner.invoke(cli, env=env)
  264. assert result.output == expect
  265. @pytest.mark.usefixtures("_patch_for_completion")
  266. def test_context_settings(runner):
  267. def complete(ctx, param, incomplete):
  268. return ctx.obj["choices"]
  269. cli = Command("cli", params=[Argument("x", shell_complete=complete)])
  270. result = runner.invoke(
  271. cli,
  272. obj={"choices": ["a", "b"]},
  273. env={"COMP_WORDS": "", "COMP_CWORD": "0", "_CLI_COMPLETE": "bash_complete"},
  274. )
  275. assert result.output == "plain,a\nplain,b\n"
  276. @pytest.mark.parametrize(("value", "expect"), [(False, ["Au", "al"]), (True, ["al"])])
  277. def test_choice_case_sensitive(value, expect):
  278. cli = Command(
  279. "cli",
  280. params=[Option(["-a"], type=Choice(["Au", "al", "Bc"], case_sensitive=value))],
  281. )
  282. completions = _get_words(cli, ["-a"], "a")
  283. assert completions == expect
  284. @pytest.fixture()
  285. def _restore_available_shells(tmpdir):
  286. prev_available_shells = click.shell_completion._available_shells.copy()
  287. click.shell_completion._available_shells.clear()
  288. yield
  289. click.shell_completion._available_shells.clear()
  290. click.shell_completion._available_shells.update(prev_available_shells)
  291. @pytest.mark.usefixtures("_restore_available_shells")
  292. def test_add_completion_class():
  293. # At first, "mysh" is not in available shells
  294. assert "mysh" not in click.shell_completion._available_shells
  295. class MyshComplete(ShellComplete):
  296. name = "mysh"
  297. source_template = "dummy source"
  298. # "mysh" still not in available shells because it is not registered
  299. assert "mysh" not in click.shell_completion._available_shells
  300. # Adding a completion class should return that class
  301. assert add_completion_class(MyshComplete) is MyshComplete
  302. # Now, "mysh" is finally in available shells
  303. assert "mysh" in click.shell_completion._available_shells
  304. assert click.shell_completion._available_shells["mysh"] is MyshComplete
  305. @pytest.mark.usefixtures("_restore_available_shells")
  306. def test_add_completion_class_with_name():
  307. # At first, "mysh" is not in available shells
  308. assert "mysh" not in click.shell_completion._available_shells
  309. assert "not_mysh" not in click.shell_completion._available_shells
  310. class MyshComplete(ShellComplete):
  311. name = "not_mysh"
  312. source_template = "dummy source"
  313. # "mysh" and "not_mysh" are still not in available shells because
  314. # it is not registered yet
  315. assert "mysh" not in click.shell_completion._available_shells
  316. assert "not_mysh" not in click.shell_completion._available_shells
  317. # Adding a completion class should return that class.
  318. # Because we are using the "name" parameter, the name isn't taken
  319. # from the class.
  320. assert add_completion_class(MyshComplete, name="mysh") is MyshComplete
  321. # Now, "mysh" is finally in available shells
  322. assert "mysh" in click.shell_completion._available_shells
  323. assert "not_mysh" not in click.shell_completion._available_shells
  324. assert click.shell_completion._available_shells["mysh"] is MyshComplete
  325. @pytest.mark.usefixtures("_restore_available_shells")
  326. def test_add_completion_class_decorator():
  327. # At first, "mysh" is not in available shells
  328. assert "mysh" not in click.shell_completion._available_shells
  329. @add_completion_class
  330. class MyshComplete(ShellComplete):
  331. name = "mysh"
  332. source_template = "dummy source"
  333. # Using `add_completion_class` as a decorator adds the new shell immediately
  334. assert "mysh" in click.shell_completion._available_shells
  335. assert click.shell_completion._available_shells["mysh"] is MyshComplete