test_utils.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510
  1. import os
  2. import pathlib
  3. import stat
  4. import sys
  5. from io import StringIO
  6. import pytest
  7. import click._termui_impl
  8. import click.utils
  9. from click._compat import WIN
  10. def test_echo(runner):
  11. with runner.isolation() as outstreams:
  12. click.echo("\N{SNOWMAN}")
  13. click.echo(b"\x44\x44")
  14. click.echo(42, nl=False)
  15. click.echo(b"a", nl=False)
  16. click.echo("\x1b[31mx\x1b[39m", nl=False)
  17. bytes = outstreams[0].getvalue().replace(b"\r\n", b"\n")
  18. assert bytes == b"\xe2\x98\x83\nDD\n42ax"
  19. # if wrapped, we expect bytes to survive.
  20. @click.command()
  21. def cli():
  22. click.echo(b"\xf6")
  23. result = runner.invoke(cli, [])
  24. assert result.stdout_bytes == b"\xf6\n"
  25. # Ensure we do not strip for bytes.
  26. with runner.isolation() as outstreams:
  27. click.echo(bytearray(b"\x1b[31mx\x1b[39m"), nl=False)
  28. assert outstreams[0].getvalue() == b"\x1b[31mx\x1b[39m"
  29. def test_echo_custom_file():
  30. f = StringIO()
  31. click.echo("hello", file=f)
  32. assert f.getvalue() == "hello\n"
  33. def test_echo_no_streams(monkeypatch, runner):
  34. """echo should not fail when stdout and stderr are None with pythonw on Windows."""
  35. with runner.isolation():
  36. sys.stdout = None
  37. sys.stderr = None
  38. click.echo("test")
  39. click.echo("test", err=True)
  40. @pytest.mark.parametrize(
  41. ("styles", "ref"),
  42. [
  43. ({"fg": "black"}, "\x1b[30mx y\x1b[0m"),
  44. ({"fg": "red"}, "\x1b[31mx y\x1b[0m"),
  45. ({"fg": "green"}, "\x1b[32mx y\x1b[0m"),
  46. ({"fg": "yellow"}, "\x1b[33mx y\x1b[0m"),
  47. ({"fg": "blue"}, "\x1b[34mx y\x1b[0m"),
  48. ({"fg": "magenta"}, "\x1b[35mx y\x1b[0m"),
  49. ({"fg": "cyan"}, "\x1b[36mx y\x1b[0m"),
  50. ({"fg": "white"}, "\x1b[37mx y\x1b[0m"),
  51. ({"bg": "black"}, "\x1b[40mx y\x1b[0m"),
  52. ({"bg": "red"}, "\x1b[41mx y\x1b[0m"),
  53. ({"bg": "green"}, "\x1b[42mx y\x1b[0m"),
  54. ({"bg": "yellow"}, "\x1b[43mx y\x1b[0m"),
  55. ({"bg": "blue"}, "\x1b[44mx y\x1b[0m"),
  56. ({"bg": "magenta"}, "\x1b[45mx y\x1b[0m"),
  57. ({"bg": "cyan"}, "\x1b[46mx y\x1b[0m"),
  58. ({"bg": "white"}, "\x1b[47mx y\x1b[0m"),
  59. ({"bg": 91}, "\x1b[48;5;91mx y\x1b[0m"),
  60. ({"bg": (135, 0, 175)}, "\x1b[48;2;135;0;175mx y\x1b[0m"),
  61. ({"bold": True}, "\x1b[1mx y\x1b[0m"),
  62. ({"dim": True}, "\x1b[2mx y\x1b[0m"),
  63. ({"underline": True}, "\x1b[4mx y\x1b[0m"),
  64. ({"overline": True}, "\x1b[53mx y\x1b[0m"),
  65. ({"italic": True}, "\x1b[3mx y\x1b[0m"),
  66. ({"blink": True}, "\x1b[5mx y\x1b[0m"),
  67. ({"reverse": True}, "\x1b[7mx y\x1b[0m"),
  68. ({"strikethrough": True}, "\x1b[9mx y\x1b[0m"),
  69. ({"bold": False}, "\x1b[22mx y\x1b[0m"),
  70. ({"dim": False}, "\x1b[22mx y\x1b[0m"),
  71. ({"underline": False}, "\x1b[24mx y\x1b[0m"),
  72. ({"overline": False}, "\x1b[55mx y\x1b[0m"),
  73. ({"italic": False}, "\x1b[23mx y\x1b[0m"),
  74. ({"blink": False}, "\x1b[25mx y\x1b[0m"),
  75. ({"reverse": False}, "\x1b[27mx y\x1b[0m"),
  76. ({"strikethrough": False}, "\x1b[29mx y\x1b[0m"),
  77. ({"fg": "black", "reset": False}, "\x1b[30mx y"),
  78. ],
  79. )
  80. def test_styling(styles, ref):
  81. assert click.style("x y", **styles) == ref
  82. assert click.unstyle(ref) == "x y"
  83. @pytest.mark.parametrize(("text", "expect"), [("\x1b[?25lx y\x1b[?25h", "x y")])
  84. def test_unstyle_other_ansi(text, expect):
  85. assert click.unstyle(text) == expect
  86. def test_filename_formatting():
  87. assert click.format_filename(b"foo.txt") == "foo.txt"
  88. assert click.format_filename(b"/x/foo.txt") == "/x/foo.txt"
  89. assert click.format_filename("/x/foo.txt") == "/x/foo.txt"
  90. assert click.format_filename("/x/foo.txt", shorten=True) == "foo.txt"
  91. assert click.format_filename(b"/x/\xff.txt", shorten=True) == "�.txt"
  92. def test_prompts(runner):
  93. @click.command()
  94. def test():
  95. if click.confirm("Foo"):
  96. click.echo("yes!")
  97. else:
  98. click.echo("no :(")
  99. result = runner.invoke(test, input="y\n")
  100. assert not result.exception
  101. assert result.output == "Foo [y/N]: y\nyes!\n"
  102. result = runner.invoke(test, input="\n")
  103. assert not result.exception
  104. assert result.output == "Foo [y/N]: \nno :(\n"
  105. result = runner.invoke(test, input="n\n")
  106. assert not result.exception
  107. assert result.output == "Foo [y/N]: n\nno :(\n"
  108. @click.command()
  109. def test_no():
  110. if click.confirm("Foo", default=True):
  111. click.echo("yes!")
  112. else:
  113. click.echo("no :(")
  114. result = runner.invoke(test_no, input="y\n")
  115. assert not result.exception
  116. assert result.output == "Foo [Y/n]: y\nyes!\n"
  117. result = runner.invoke(test_no, input="\n")
  118. assert not result.exception
  119. assert result.output == "Foo [Y/n]: \nyes!\n"
  120. result = runner.invoke(test_no, input="n\n")
  121. assert not result.exception
  122. assert result.output == "Foo [Y/n]: n\nno :(\n"
  123. def test_confirm_repeat(runner):
  124. cli = click.Command(
  125. "cli", params=[click.Option(["--a/--no-a"], default=None, prompt=True)]
  126. )
  127. result = runner.invoke(cli, input="\ny\n")
  128. assert result.output == "A [y/n]: \nError: invalid input\nA [y/n]: y\n"
  129. @pytest.mark.skipif(WIN, reason="Different behavior on windows.")
  130. def test_prompts_abort(monkeypatch, capsys):
  131. def f(_):
  132. raise KeyboardInterrupt()
  133. monkeypatch.setattr("click.termui.hidden_prompt_func", f)
  134. try:
  135. click.prompt("Password", hide_input=True)
  136. except click.Abort:
  137. click.echo("interrupted")
  138. out, err = capsys.readouterr()
  139. assert out == "Password:\ninterrupted\n"
  140. def _test_gen_func():
  141. yield "a"
  142. yield "b"
  143. yield "c"
  144. yield "abc"
  145. @pytest.mark.parametrize("cat", ["cat", "cat ", "cat "])
  146. @pytest.mark.parametrize(
  147. "test",
  148. [
  149. # We need lambda here, because pytest will
  150. # reuse the parameters, and then the generators
  151. # are already used and will not yield anymore
  152. ("just text\n", lambda: "just text"),
  153. ("iterable\n", lambda: ["itera", "ble"]),
  154. ("abcabc\n", lambda: _test_gen_func),
  155. ("abcabc\n", lambda: _test_gen_func()),
  156. ("012345\n", lambda: (c for c in range(6))),
  157. ],
  158. )
  159. def test_echo_via_pager(monkeypatch, capfd, cat, test):
  160. monkeypatch.setitem(os.environ, "PAGER", cat)
  161. monkeypatch.setattr(click._termui_impl, "isatty", lambda x: True)
  162. expected_output = test[0]
  163. test_input = test[1]()
  164. click.echo_via_pager(test_input)
  165. out, err = capfd.readouterr()
  166. assert out == expected_output
  167. def test_echo_color_flag(monkeypatch, capfd):
  168. isatty = True
  169. monkeypatch.setattr(click._compat, "isatty", lambda x: isatty)
  170. text = "foo"
  171. styled_text = click.style(text, fg="red")
  172. assert styled_text == "\x1b[31mfoo\x1b[0m"
  173. click.echo(styled_text, color=False)
  174. out, err = capfd.readouterr()
  175. assert out == f"{text}\n"
  176. click.echo(styled_text, color=True)
  177. out, err = capfd.readouterr()
  178. assert out == f"{styled_text}\n"
  179. isatty = True
  180. click.echo(styled_text)
  181. out, err = capfd.readouterr()
  182. assert out == f"{styled_text}\n"
  183. isatty = False
  184. # Faking isatty() is not enough on Windows;
  185. # the implementation caches the colorama wrapped stream
  186. # so we have to use a new stream for each test
  187. stream = StringIO()
  188. click.echo(styled_text, file=stream)
  189. assert stream.getvalue() == f"{text}\n"
  190. stream = StringIO()
  191. click.echo(styled_text, file=stream, color=True)
  192. assert stream.getvalue() == f"{styled_text}\n"
  193. def test_prompt_cast_default(capfd, monkeypatch):
  194. monkeypatch.setattr(sys, "stdin", StringIO("\n"))
  195. value = click.prompt("value", default="100", type=int)
  196. capfd.readouterr()
  197. assert type(value) is int # noqa E721
  198. @pytest.mark.skipif(WIN, reason="Test too complex to make work windows.")
  199. def test_echo_writing_to_standard_error(capfd, monkeypatch):
  200. def emulate_input(text):
  201. """Emulate keyboard input."""
  202. monkeypatch.setattr(sys, "stdin", StringIO(text))
  203. click.echo("Echo to standard output")
  204. out, err = capfd.readouterr()
  205. assert out == "Echo to standard output\n"
  206. assert err == ""
  207. click.echo("Echo to standard error", err=True)
  208. out, err = capfd.readouterr()
  209. assert out == ""
  210. assert err == "Echo to standard error\n"
  211. emulate_input("asdlkj\n")
  212. click.prompt("Prompt to stdin")
  213. out, err = capfd.readouterr()
  214. assert out == "Prompt to stdin: "
  215. assert err == ""
  216. emulate_input("asdlkj\n")
  217. click.prompt("Prompt to stderr", err=True)
  218. out, err = capfd.readouterr()
  219. assert out == " "
  220. assert err == "Prompt to stderr:"
  221. emulate_input("y\n")
  222. click.confirm("Prompt to stdin")
  223. out, err = capfd.readouterr()
  224. assert out == "Prompt to stdin [y/N]: "
  225. assert err == ""
  226. emulate_input("y\n")
  227. click.confirm("Prompt to stderr", err=True)
  228. out, err = capfd.readouterr()
  229. assert out == " "
  230. assert err == "Prompt to stderr [y/N]:"
  231. monkeypatch.setattr(click.termui, "isatty", lambda x: True)
  232. monkeypatch.setattr(click.termui, "getchar", lambda: " ")
  233. click.pause("Pause to stdout")
  234. out, err = capfd.readouterr()
  235. assert out == "Pause to stdout\n"
  236. assert err == ""
  237. click.pause("Pause to stderr", err=True)
  238. out, err = capfd.readouterr()
  239. assert out == ""
  240. assert err == "Pause to stderr\n"
  241. def test_echo_with_capsys(capsys):
  242. click.echo("Capture me.")
  243. out, err = capsys.readouterr()
  244. assert out == "Capture me.\n"
  245. def test_open_file(runner):
  246. @click.command()
  247. @click.argument("filename")
  248. def cli(filename):
  249. with click.open_file(filename) as f:
  250. click.echo(f.read())
  251. click.echo("meep")
  252. with runner.isolated_filesystem():
  253. with open("hello.txt", "w") as f:
  254. f.write("Cool stuff")
  255. result = runner.invoke(cli, ["hello.txt"])
  256. assert result.exception is None
  257. assert result.output == "Cool stuff\nmeep\n"
  258. result = runner.invoke(cli, ["-"], input="foobar")
  259. assert result.exception is None
  260. assert result.output == "foobar\nmeep\n"
  261. def test_open_file_pathlib_dash(runner):
  262. @click.command()
  263. @click.argument(
  264. "filename", type=click.Path(allow_dash=True, path_type=pathlib.Path)
  265. )
  266. def cli(filename):
  267. click.echo(str(type(filename)))
  268. with click.open_file(filename) as f:
  269. click.echo(f.read())
  270. result = runner.invoke(cli, ["-"], input="value")
  271. assert result.exception is None
  272. assert result.output == "pathlib.Path\nvalue\n"
  273. def test_open_file_ignore_errors_stdin(runner):
  274. @click.command()
  275. @click.argument("filename")
  276. def cli(filename):
  277. with click.open_file(filename, errors="ignore") as f:
  278. click.echo(f.read())
  279. result = runner.invoke(cli, ["-"], input=os.urandom(16))
  280. assert result.exception is None
  281. def test_open_file_respects_ignore(runner):
  282. with runner.isolated_filesystem():
  283. with open("test.txt", "w") as f:
  284. f.write("Hello world!")
  285. with click.open_file("test.txt", encoding="utf8", errors="ignore") as f:
  286. assert f.errors == "ignore"
  287. def test_open_file_ignore_invalid_utf8(runner):
  288. with runner.isolated_filesystem():
  289. with open("test.txt", "wb") as f:
  290. f.write(b"\xe2\x28\xa1")
  291. with click.open_file("test.txt", encoding="utf8", errors="ignore") as f:
  292. f.read()
  293. def test_open_file_ignore_no_encoding(runner):
  294. with runner.isolated_filesystem():
  295. with open("test.bin", "wb") as f:
  296. f.write(os.urandom(16))
  297. with click.open_file("test.bin", errors="ignore") as f:
  298. f.read()
  299. @pytest.mark.skipif(WIN, reason="os.chmod() is not fully supported on Windows.")
  300. @pytest.mark.parametrize("permissions", [0o400, 0o444, 0o600, 0o644])
  301. def test_open_file_atomic_permissions_existing_file(runner, permissions):
  302. with runner.isolated_filesystem():
  303. with open("existing.txt", "w") as f:
  304. f.write("content")
  305. os.chmod("existing.txt", permissions)
  306. @click.command()
  307. @click.argument("filename")
  308. def cli(filename):
  309. click.open_file(filename, "w", atomic=True).close()
  310. result = runner.invoke(cli, ["existing.txt"])
  311. assert result.exception is None
  312. assert stat.S_IMODE(os.stat("existing.txt").st_mode) == permissions
  313. @pytest.mark.skipif(WIN, reason="os.stat() is not fully supported on Windows.")
  314. def test_open_file_atomic_permissions_new_file(runner):
  315. with runner.isolated_filesystem():
  316. @click.command()
  317. @click.argument("filename")
  318. def cli(filename):
  319. click.open_file(filename, "w", atomic=True).close()
  320. # Create a test file to get the expected permissions for new files
  321. # according to the current umask.
  322. with open("test.txt", "w"):
  323. pass
  324. permissions = stat.S_IMODE(os.stat("test.txt").st_mode)
  325. result = runner.invoke(cli, ["new.txt"])
  326. assert result.exception is None
  327. assert stat.S_IMODE(os.stat("new.txt").st_mode) == permissions
  328. def test_iter_keepopenfile(tmpdir):
  329. expected = list(map(str, range(10)))
  330. p = tmpdir.mkdir("testdir").join("testfile")
  331. p.write("\n".join(expected))
  332. with p.open() as f:
  333. for e_line, a_line in zip(expected, click.utils.KeepOpenFile(f)):
  334. assert e_line == a_line.strip()
  335. def test_iter_lazyfile(tmpdir):
  336. expected = list(map(str, range(10)))
  337. p = tmpdir.mkdir("testdir").join("testfile")
  338. p.write("\n".join(expected))
  339. with p.open() as f:
  340. with click.utils.LazyFile(f.name) as lf:
  341. for e_line, a_line in zip(expected, lf):
  342. assert e_line == a_line.strip()
  343. class MockMain:
  344. __slots__ = "__package__"
  345. def __init__(self, package_name):
  346. self.__package__ = package_name
  347. @pytest.mark.parametrize(
  348. ("path", "main", "expected"),
  349. [
  350. ("example.py", None, "example.py"),
  351. (str(pathlib.Path("/foo/bar/example.py")), None, "example.py"),
  352. ("example", None, "example"),
  353. (str(pathlib.Path("example/__main__.py")), "example", "python -m example"),
  354. (str(pathlib.Path("example/cli.py")), "example", "python -m example.cli"),
  355. (str(pathlib.Path("./example")), "", "example"),
  356. ],
  357. )
  358. def test_detect_program_name(path, main, expected):
  359. assert click.utils._detect_program_name(path, _main=MockMain(main)) == expected
  360. def test_expand_args(monkeypatch):
  361. user = os.path.expanduser("~")
  362. assert user in click.utils._expand_args(["~"])
  363. monkeypatch.setenv("CLICK_TEST", "hello")
  364. assert "hello" in click.utils._expand_args(["$CLICK_TEST"])
  365. #assert "pyproject.toml" in click.utils._expand_args(["*.toml"])
  366. #assert os.path.join("docs", "conf.py") in click.utils._expand_args(["**/conf.py"])
  367. assert "*.not-found" in click.utils._expand_args(["*.not-found"])
  368. # a bad glob pattern, such as a pytest identifier, should return itself
  369. assert click.utils._expand_args(["test.py::test_bad"])[0] == "test.py::test_bad"
  370. @pytest.mark.parametrize(
  371. ("value", "max_length", "expect"),
  372. [
  373. pytest.param("", 10, "", id="empty"),
  374. pytest.param("123 567 90", 10, "123 567 90", id="equal length, no dot"),
  375. pytest.param("123 567 9. aaaa bbb", 10, "123 567 9.", id="sentence < max"),
  376. pytest.param("123 567\n\n 9. aaaa bbb", 10, "123 567", id="paragraph < max"),
  377. pytest.param("123 567 90123.", 10, "123 567...", id="truncate"),
  378. pytest.param("123 5678 xxxxxx", 10, "123...", id="length includes suffix"),
  379. pytest.param(
  380. "token in ~/.netrc ciao ciao",
  381. 20,
  382. "token in ~/.netrc...",
  383. id="ignore dot in word",
  384. ),
  385. ],
  386. )
  387. @pytest.mark.parametrize(
  388. "alter",
  389. [
  390. pytest.param(None, id=""),
  391. pytest.param(
  392. lambda text: "\n\b\n" + " ".join(text.split(" ")) + "\n", id="no-wrap mark"
  393. ),
  394. ],
  395. )
  396. def test_make_default_short_help(value, max_length, alter, expect):
  397. assert len(expect) <= max_length
  398. if alter:
  399. value = alter(value)
  400. out = click.utils.make_default_short_help(value, max_length)
  401. assert out == expect