test_commands.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543
  1. import re
  2. import pytest
  3. import click
  4. def test_other_command_invoke(runner):
  5. @click.command()
  6. @click.pass_context
  7. def cli(ctx):
  8. return ctx.invoke(other_cmd, arg=42)
  9. @click.command()
  10. @click.argument("arg", type=click.INT)
  11. def other_cmd(arg):
  12. click.echo(arg)
  13. result = runner.invoke(cli, [])
  14. assert not result.exception
  15. assert result.output == "42\n"
  16. def test_other_command_forward(runner):
  17. cli = click.Group()
  18. @cli.command()
  19. @click.option("--count", default=1)
  20. def test(count):
  21. click.echo(f"Count: {count:d}")
  22. @cli.command()
  23. @click.option("--count", default=1)
  24. @click.pass_context
  25. def dist(ctx, count):
  26. ctx.forward(test)
  27. ctx.invoke(test, count=42)
  28. result = runner.invoke(cli, ["dist"])
  29. assert not result.exception
  30. assert result.output == "Count: 1\nCount: 42\n"
  31. def test_forwarded_params_consistency(runner):
  32. cli = click.Group()
  33. @cli.command()
  34. @click.option("-a")
  35. @click.pass_context
  36. def first(ctx, **kwargs):
  37. click.echo(f"{ctx.params}")
  38. @cli.command()
  39. @click.option("-a")
  40. @click.option("-b")
  41. @click.pass_context
  42. def second(ctx, **kwargs):
  43. click.echo(f"{ctx.params}")
  44. ctx.forward(first)
  45. result = runner.invoke(cli, ["second", "-a", "foo", "-b", "bar"])
  46. assert not result.exception
  47. assert result.output == "{'a': 'foo', 'b': 'bar'}\n{'a': 'foo', 'b': 'bar'}\n"
  48. def test_auto_shorthelp(runner):
  49. @click.group()
  50. def cli():
  51. pass
  52. @cli.command()
  53. def short():
  54. """This is a short text."""
  55. @cli.command()
  56. def special_chars():
  57. """Login and store the token in ~/.netrc."""
  58. @cli.command()
  59. def long():
  60. """This is a long text that is too long to show as short help
  61. and will be truncated instead."""
  62. result = runner.invoke(cli, ["--help"])
  63. assert (
  64. re.search(
  65. r"Commands:\n\s+"
  66. r"long\s+This is a long text that is too long to show as short help"
  67. r"\.\.\.\n\s+"
  68. r"short\s+This is a short text\.\n\s+"
  69. r"special-chars\s+Login and store the token in ~/.netrc\.\s*",
  70. result.output,
  71. )
  72. is not None
  73. )
  74. def test_no_args_is_help(runner):
  75. @click.command(no_args_is_help=True)
  76. def cli():
  77. pass
  78. result = runner.invoke(cli, [])
  79. assert result.exit_code == 0
  80. assert "Show this message and exit." in result.output
  81. def test_default_maps(runner):
  82. @click.group()
  83. def cli():
  84. pass
  85. @cli.command()
  86. @click.option("--name", default="normal")
  87. def foo(name):
  88. click.echo(name)
  89. result = runner.invoke(cli, ["foo"], default_map={"foo": {"name": "changed"}})
  90. assert not result.exception
  91. assert result.output == "changed\n"
  92. @pytest.mark.parametrize(
  93. ("args", "exit_code", "expect"),
  94. [
  95. (["obj1"], 2, "Error: Missing command."),
  96. (["obj1", "--help"], 0, "Show this message and exit."),
  97. (["obj1", "move"], 0, "obj=obj1\nmove\n"),
  98. ([], 0, "Show this message and exit."),
  99. ],
  100. )
  101. def test_group_with_args(runner, args, exit_code, expect):
  102. @click.group()
  103. @click.argument("obj")
  104. def cli(obj):
  105. click.echo(f"obj={obj}")
  106. @cli.command()
  107. def move():
  108. click.echo("move")
  109. result = runner.invoke(cli, args)
  110. assert result.exit_code == exit_code
  111. assert expect in result.output
  112. def test_base_command(runner):
  113. import optparse
  114. @click.group()
  115. def cli():
  116. pass
  117. class OptParseCommand(click.BaseCommand):
  118. def __init__(self, name, parser, callback):
  119. super().__init__(name)
  120. self.parser = parser
  121. self.callback = callback
  122. def parse_args(self, ctx, args):
  123. try:
  124. opts, args = parser.parse_args(args)
  125. except Exception as e:
  126. ctx.fail(str(e))
  127. ctx.args = args
  128. ctx.params = vars(opts)
  129. def get_usage(self, ctx):
  130. return self.parser.get_usage()
  131. def get_help(self, ctx):
  132. return self.parser.format_help()
  133. def invoke(self, ctx):
  134. ctx.invoke(self.callback, ctx.args, **ctx.params)
  135. parser = optparse.OptionParser(usage="Usage: foo test [OPTIONS]")
  136. parser.add_option(
  137. "-f", "--file", dest="filename", help="write report to FILE", metavar="FILE"
  138. )
  139. parser.add_option(
  140. "-q",
  141. "--quiet",
  142. action="store_false",
  143. dest="verbose",
  144. default=True,
  145. help="don't print status messages to stdout",
  146. )
  147. def test_callback(args, filename, verbose):
  148. click.echo(" ".join(args))
  149. click.echo(filename)
  150. click.echo(verbose)
  151. cli.add_command(OptParseCommand("test", parser, test_callback))
  152. result = runner.invoke(cli, ["test", "-f", "f.txt", "-q", "q1.txt", "q2.txt"])
  153. assert result.exception is None
  154. assert result.output.splitlines() == ["q1.txt q2.txt", "f.txt", "False"]
  155. result = runner.invoke(cli, ["test", "--help"])
  156. assert result.exception is None
  157. assert result.output.splitlines() == [
  158. "Usage: foo test [OPTIONS]",
  159. "",
  160. "Options:",
  161. " -h, --help show this help message and exit",
  162. " -f FILE, --file=FILE write report to FILE",
  163. " -q, --quiet don't print status messages to stdout",
  164. ]
  165. def test_object_propagation(runner):
  166. for chain in False, True:
  167. @click.group(chain=chain)
  168. @click.option("--debug/--no-debug", default=False)
  169. @click.pass_context
  170. def cli(ctx, debug):
  171. if ctx.obj is None:
  172. ctx.obj = {}
  173. ctx.obj["DEBUG"] = debug
  174. @cli.command()
  175. @click.pass_context
  176. def sync(ctx):
  177. click.echo(f"Debug is {'on' if ctx.obj['DEBUG'] else 'off'}")
  178. result = runner.invoke(cli, ["sync"])
  179. assert result.exception is None
  180. assert result.output == "Debug is off\n"
  181. def test_other_command_invoke_with_defaults(runner):
  182. @click.command()
  183. @click.pass_context
  184. def cli(ctx):
  185. return ctx.invoke(other_cmd)
  186. @click.command()
  187. @click.option("-a", type=click.INT, default=42)
  188. @click.option("-b", type=click.INT, default="15")
  189. @click.option("-c", multiple=True)
  190. @click.pass_context
  191. def other_cmd(ctx, a, b, c):
  192. return ctx.info_name, a, b, c
  193. result = runner.invoke(cli, standalone_mode=False)
  194. # invoke should type cast default values, str becomes int, empty
  195. # multiple should be empty tuple instead of None
  196. assert result.return_value == ("other-cmd", 42, 15, ())
  197. def test_invoked_subcommand(runner):
  198. @click.group(invoke_without_command=True)
  199. @click.pass_context
  200. def cli(ctx):
  201. if ctx.invoked_subcommand is None:
  202. click.echo("no subcommand, use default")
  203. ctx.invoke(sync)
  204. else:
  205. click.echo("invoke subcommand")
  206. @cli.command()
  207. def sync():
  208. click.echo("in subcommand")
  209. result = runner.invoke(cli, ["sync"])
  210. assert not result.exception
  211. assert result.output == "invoke subcommand\nin subcommand\n"
  212. result = runner.invoke(cli)
  213. assert not result.exception
  214. assert result.output == "no subcommand, use default\nin subcommand\n"
  215. def test_aliased_command_canonical_name(runner):
  216. class AliasedGroup(click.Group):
  217. def get_command(self, ctx, cmd_name):
  218. return push
  219. def resolve_command(self, ctx, args):
  220. _, command, args = super().resolve_command(ctx, args)
  221. return command.name, command, args
  222. cli = AliasedGroup()
  223. @cli.command()
  224. def push():
  225. click.echo("push command")
  226. result = runner.invoke(cli, ["pu", "--help"])
  227. assert not result.exception
  228. assert result.output.startswith("Usage: root push [OPTIONS]")
  229. def test_group_add_command_name(runner):
  230. cli = click.Group("cli")
  231. cmd = click.Command("a", params=[click.Option(["-x"], required=True)])
  232. cli.add_command(cmd, "b")
  233. # Check that the command is accessed through the registered name,
  234. # not the original name.
  235. result = runner.invoke(cli, ["b"], default_map={"b": {"x": 3}})
  236. assert result.exit_code == 0
  237. @pytest.mark.parametrize(
  238. ("invocation_order", "declaration_order", "expected_order"),
  239. [
  240. # Non-eager options.
  241. ([], ["-a"], ["-a"]),
  242. (["-a"], ["-a"], ["-a"]),
  243. ([], ["-a", "-c"], ["-a", "-c"]),
  244. (["-a"], ["-a", "-c"], ["-a", "-c"]),
  245. (["-c"], ["-a", "-c"], ["-c", "-a"]),
  246. ([], ["-c", "-a"], ["-c", "-a"]),
  247. (["-a"], ["-c", "-a"], ["-a", "-c"]),
  248. (["-c"], ["-c", "-a"], ["-c", "-a"]),
  249. (["-a", "-c"], ["-a", "-c"], ["-a", "-c"]),
  250. (["-c", "-a"], ["-a", "-c"], ["-c", "-a"]),
  251. # Eager options.
  252. ([], ["-b"], ["-b"]),
  253. (["-b"], ["-b"], ["-b"]),
  254. ([], ["-b", "-d"], ["-b", "-d"]),
  255. (["-b"], ["-b", "-d"], ["-b", "-d"]),
  256. (["-d"], ["-b", "-d"], ["-d", "-b"]),
  257. ([], ["-d", "-b"], ["-d", "-b"]),
  258. (["-b"], ["-d", "-b"], ["-b", "-d"]),
  259. (["-d"], ["-d", "-b"], ["-d", "-b"]),
  260. (["-b", "-d"], ["-b", "-d"], ["-b", "-d"]),
  261. (["-d", "-b"], ["-b", "-d"], ["-d", "-b"]),
  262. # Mixed options.
  263. ([], ["-a", "-b", "-c", "-d"], ["-b", "-d", "-a", "-c"]),
  264. (["-a"], ["-a", "-b", "-c", "-d"], ["-b", "-d", "-a", "-c"]),
  265. (["-b"], ["-a", "-b", "-c", "-d"], ["-b", "-d", "-a", "-c"]),
  266. (["-c"], ["-a", "-b", "-c", "-d"], ["-b", "-d", "-c", "-a"]),
  267. (["-d"], ["-a", "-b", "-c", "-d"], ["-d", "-b", "-a", "-c"]),
  268. (["-a", "-b"], ["-a", "-b", "-c", "-d"], ["-b", "-d", "-a", "-c"]),
  269. (["-b", "-a"], ["-a", "-b", "-c", "-d"], ["-b", "-d", "-a", "-c"]),
  270. (["-d", "-c"], ["-a", "-b", "-c", "-d"], ["-d", "-b", "-c", "-a"]),
  271. (["-c", "-d"], ["-a", "-b", "-c", "-d"], ["-d", "-b", "-c", "-a"]),
  272. (["-a", "-b", "-c", "-d"], ["-a", "-b", "-c", "-d"], ["-b", "-d", "-a", "-c"]),
  273. (["-b", "-d", "-a", "-c"], ["-a", "-b", "-c", "-d"], ["-b", "-d", "-a", "-c"]),
  274. ([], ["-b", "-d", "-e", "-a", "-c"], ["-b", "-d", "-e", "-a", "-c"]),
  275. (["-a", "-d"], ["-b", "-d", "-e", "-a", "-c"], ["-d", "-b", "-e", "-a", "-c"]),
  276. (["-c", "-d"], ["-b", "-d", "-e", "-a", "-c"], ["-d", "-b", "-e", "-c", "-a"]),
  277. ],
  278. )
  279. def test_iter_params_for_processing(
  280. invocation_order, declaration_order, expected_order
  281. ):
  282. parameters = {
  283. "-a": click.Option(["-a"]),
  284. "-b": click.Option(["-b"], is_eager=True),
  285. "-c": click.Option(["-c"]),
  286. "-d": click.Option(["-d"], is_eager=True),
  287. "-e": click.Option(["-e"], is_eager=True),
  288. }
  289. invocation_params = [parameters[opt_id] for opt_id in invocation_order]
  290. declaration_params = [parameters[opt_id] for opt_id in declaration_order]
  291. expected_params = [parameters[opt_id] for opt_id in expected_order]
  292. assert (
  293. click.core.iter_params_for_processing(invocation_params, declaration_params)
  294. == expected_params
  295. )
  296. def test_help_param_priority(runner):
  297. """Cover the edge-case in which the eagerness of help option was not
  298. respected, because it was internally generated multiple times.
  299. See: https://github.com/pallets/click/pull/2811
  300. """
  301. def print_and_exit(ctx, param, value):
  302. if value:
  303. click.echo(f"Value of {param.name} is: {value}")
  304. ctx.exit()
  305. @click.command(context_settings={"help_option_names": ("--my-help",)})
  306. @click.option("-a", is_flag=True, expose_value=False, callback=print_and_exit)
  307. @click.option(
  308. "-b", is_flag=True, expose_value=False, callback=print_and_exit, is_eager=True
  309. )
  310. def cli():
  311. pass
  312. # --my-help is properly called and stop execution.
  313. result = runner.invoke(cli, ["--my-help"])
  314. assert "Value of a is: True" not in result.stdout
  315. assert "Value of b is: True" not in result.stdout
  316. assert "--my-help" in result.stdout
  317. assert result.exit_code == 0
  318. # -a is properly called and stop execution.
  319. result = runner.invoke(cli, ["-a"])
  320. assert "Value of a is: True" in result.stdout
  321. assert "Value of b is: True" not in result.stdout
  322. assert "--my-help" not in result.stdout
  323. assert result.exit_code == 0
  324. # -a takes precedence over -b and stop execution.
  325. result = runner.invoke(cli, ["-a", "-b"])
  326. assert "Value of a is: True" not in result.stdout
  327. assert "Value of b is: True" in result.stdout
  328. assert "--my-help" not in result.stdout
  329. assert result.exit_code == 0
  330. # --my-help is eager by default so takes precedence over -a and stop
  331. # execution, whatever the order.
  332. for args in [["-a", "--my-help"], ["--my-help", "-a"]]:
  333. result = runner.invoke(cli, args)
  334. assert "Value of a is: True" not in result.stdout
  335. assert "Value of b is: True" not in result.stdout
  336. assert "--my-help" in result.stdout
  337. assert result.exit_code == 0
  338. # Both -b and --my-help are eager so they're called in the order they're
  339. # invoked by the user.
  340. result = runner.invoke(cli, ["-b", "--my-help"])
  341. assert "Value of a is: True" not in result.stdout
  342. assert "Value of b is: True" in result.stdout
  343. assert "--my-help" not in result.stdout
  344. assert result.exit_code == 0
  345. # But there was a bug when --my-help is called before -b, because the
  346. # --my-help option created by click via help_option_names is internally
  347. # created twice and is not the same object, breaking the priority order
  348. # produced by iter_params_for_processing.
  349. result = runner.invoke(cli, ["--my-help", "-b"])
  350. assert "Value of a is: True" not in result.stdout
  351. assert "Value of b is: True" not in result.stdout
  352. assert "--my-help" in result.stdout
  353. assert result.exit_code == 0
  354. def test_unprocessed_options(runner):
  355. @click.command(context_settings=dict(ignore_unknown_options=True))
  356. @click.argument("args", nargs=-1, type=click.UNPROCESSED)
  357. @click.option("--verbose", "-v", count=True)
  358. def cli(verbose, args):
  359. click.echo(f"Verbosity: {verbose}")
  360. click.echo(f"Args: {'|'.join(args)}")
  361. result = runner.invoke(cli, ["-foo", "-vvvvx", "--muhaha", "x", "y", "-x"])
  362. assert not result.exception
  363. assert result.output.splitlines() == [
  364. "Verbosity: 4",
  365. "Args: -foo|-x|--muhaha|x|y|-x",
  366. ]
  367. @pytest.mark.parametrize("doc", ["CLI HELP", None])
  368. def test_deprecated_in_help_messages(runner, doc):
  369. @click.command(deprecated=True, help=doc)
  370. def cli():
  371. pass
  372. result = runner.invoke(cli, ["--help"])
  373. assert "(Deprecated)" in result.output
  374. def test_deprecated_in_invocation(runner):
  375. @click.command(deprecated=True)
  376. def deprecated_cmd():
  377. pass
  378. result = runner.invoke(deprecated_cmd)
  379. assert "DeprecationWarning:" in result.output
  380. def test_command_parse_args_collects_option_prefixes():
  381. @click.command()
  382. @click.option("+p", is_flag=True)
  383. @click.option("!e", is_flag=True)
  384. def test(p, e):
  385. pass
  386. ctx = click.Context(test)
  387. test.parse_args(ctx, [])
  388. assert ctx._opt_prefixes == {"-", "--", "+", "!"}
  389. def test_group_parse_args_collects_base_option_prefixes():
  390. @click.group()
  391. @click.option("~t", is_flag=True)
  392. def group(t):
  393. pass
  394. @group.command()
  395. @click.option("+p", is_flag=True)
  396. def command1(p):
  397. pass
  398. @group.command()
  399. @click.option("!e", is_flag=True)
  400. def command2(e):
  401. pass
  402. ctx = click.Context(group)
  403. group.parse_args(ctx, ["command1", "+p"])
  404. assert ctx._opt_prefixes == {"-", "--", "~"}
  405. def test_group_invoke_collects_used_option_prefixes(runner):
  406. opt_prefixes = set()
  407. @click.group()
  408. @click.option("~t", is_flag=True)
  409. def group(t):
  410. pass
  411. @group.command()
  412. @click.option("+p", is_flag=True)
  413. @click.pass_context
  414. def command1(ctx, p):
  415. nonlocal opt_prefixes
  416. opt_prefixes = ctx._opt_prefixes
  417. @group.command()
  418. @click.option("!e", is_flag=True)
  419. def command2(e):
  420. pass
  421. runner.invoke(group, ["command1"])
  422. assert opt_prefixes == {"-", "--", "~", "+"}
  423. @pytest.mark.parametrize("exc", (EOFError, KeyboardInterrupt))
  424. def test_abort_exceptions_with_disabled_standalone_mode(runner, exc):
  425. @click.command()
  426. def cli():
  427. raise exc("catch me!")
  428. rv = runner.invoke(cli, standalone_mode=False)
  429. assert rv.exit_code == 1
  430. assert isinstance(rv.exception.__cause__, exc)
  431. assert rv.exception.__cause__.args == ("catch me!",)