All patches and comments are welcome. Please squash your changes to logical
commits before using git-format-patch and git-send-email to
patches@git.madduck.net.
If you'd read over the Git project's submission guidelines and adhered to them,
I'd be especially grateful.
2 from dataclasses import replace
5 from contextlib import ExitStack as does_not_raise
6 from typing import ContextManager
8 from click.testing import CliRunner
9 from black.handle_ipynb_magics import jupyter_dependencies_are_installed
18 from black import Mode
19 from _pytest.monkeypatch import MonkeyPatch
20 from tests.util import DATA_DIR
22 with contextlib.suppress(ModuleNotFoundError):
24 pytestmark = pytest.mark.jupyter
25 pytest.importorskip("IPython", reason="IPython is an optional dependency")
26 pytest.importorskip("tokenize_rt", reason="tokenize-rt is an optional dependency")
28 JUPYTER_MODE = Mode(is_ipynb=True)
30 EMPTY_CONFIG = DATA_DIR / "empty_pyproject.toml"
35 def test_noop() -> None:
37 with pytest.raises(NothingChanged):
38 format_cell(src, fast=True, mode=JUPYTER_MODE)
41 @pytest.mark.parametrize("fast", [True, False])
42 def test_trailing_semicolon(fast: bool) -> None:
44 result = format_cell(src, fast=fast, mode=JUPYTER_MODE)
45 expected = 'foo = "a";'
46 assert result == expected
49 def test_trailing_semicolon_with_comment() -> None:
50 src = 'foo = "a" ; # bar'
51 result = format_cell(src, fast=True, mode=JUPYTER_MODE)
52 expected = 'foo = "a"; # bar'
53 assert result == expected
56 def test_trailing_semicolon_with_comment_on_next_line() -> None:
57 src = "import black;\n\n# this is a comment"
58 with pytest.raises(NothingChanged):
59 format_cell(src, fast=True, mode=JUPYTER_MODE)
62 def test_trailing_semicolon_indented() -> None:
63 src = "with foo:\n plot_bar();"
64 with pytest.raises(NothingChanged):
65 format_cell(src, fast=True, mode=JUPYTER_MODE)
68 def test_trailing_semicolon_noop() -> None:
70 with pytest.raises(NothingChanged):
71 format_cell(src, fast=True, mode=JUPYTER_MODE)
74 @pytest.mark.parametrize(
77 pytest.param(JUPYTER_MODE, id="default mode"),
79 replace(JUPYTER_MODE, python_cell_magics={"cust1", "cust1"}),
80 id="custom cell magics mode",
84 def test_cell_magic(mode: Mode) -> None:
85 src = "%%time\nfoo =bar"
86 result = format_cell(src, fast=True, mode=mode)
87 expected = "%%time\nfoo = bar"
88 assert result == expected
91 def test_cell_magic_noop() -> None:
93 with pytest.raises(NothingChanged):
94 format_cell(src, fast=True, mode=JUPYTER_MODE)
97 @pytest.mark.parametrize(
100 pytest.param(JUPYTER_MODE, id="default mode"),
102 replace(JUPYTER_MODE, python_cell_magics={"cust1", "cust1"}),
103 id="custom cell magics mode",
107 @pytest.mark.parametrize(
110 pytest.param("ls =!ls", "ls = !ls", id="System assignment"),
111 pytest.param("!ls\n'foo'", '!ls\n"foo"', id="System call"),
112 pytest.param("!!ls\n'foo'", '!!ls\n"foo"', id="Other system call"),
113 pytest.param("?str\n'foo'", '?str\n"foo"', id="Help"),
114 pytest.param("??str\n'foo'", '??str\n"foo"', id="Other help"),
116 "%matplotlib inline\n'foo'",
117 '%matplotlib inline\n"foo"',
118 id="Line magic with argument",
120 pytest.param("%time\n'foo'", '%time\n"foo"', id="Line magic without argument"),
122 "env = %env var", "env = %env var", id="Assignment to environment variable"
124 pytest.param("env = %env", "env = %env", id="Assignment to magic"),
127 def test_magic(src: str, expected: str, mode: Mode) -> None:
128 result = format_cell(src, fast=True, mode=mode)
129 assert result == expected
132 @pytest.mark.parametrize(
136 "%%html --isolated\n2+2",
137 "%%writefile e.txt\n meh\n meh",
140 def test_non_python_magics(src: str) -> None:
141 with pytest.raises(NothingChanged):
142 format_cell(src, fast=True, mode=JUPYTER_MODE)
146 IPython.version_info < (8, 3),
147 reason="Change in how TransformerManager transforms this input",
149 def test_set_input() -> None:
152 result = format_cell(src, fast=True, mode=JUPYTER_MODE)
153 assert result == expected
156 def test_input_already_contains_transformed_magic() -> None:
157 src = '%time foo()\nget_ipython().run_cell_magic("time", "", "foo()\\n")'
158 with pytest.raises(NothingChanged):
159 format_cell(src, fast=True, mode=JUPYTER_MODE)
162 def test_magic_noop() -> None:
164 with pytest.raises(NothingChanged):
165 format_cell(src, fast=True, mode=JUPYTER_MODE)
168 def test_cell_magic_with_magic() -> None:
169 src = "%%timeit -n1\nls =!ls"
170 result = format_cell(src, fast=True, mode=JUPYTER_MODE)
171 expected = "%%timeit -n1\nls = !ls"
172 assert result == expected
175 @pytest.mark.parametrize(
176 "mode, expected_output, expectation",
180 "%%custom_python_magic -n1 -n2\nx=2",
181 pytest.raises(NothingChanged),
182 id="No change when cell magic not registered",
185 replace(JUPYTER_MODE, python_cell_magics={"cust1", "cust1"}),
186 "%%custom_python_magic -n1 -n2\nx=2",
187 pytest.raises(NothingChanged),
188 id="No change when other cell magics registered",
191 replace(JUPYTER_MODE, python_cell_magics={"custom_python_magic", "cust1"}),
192 "%%custom_python_magic -n1 -n2\nx = 2",
194 id="Correctly change when cell magic registered",
198 def test_cell_magic_with_custom_python_magic(
199 mode: Mode, expected_output: str, expectation: ContextManager[object]
202 result = format_cell(
203 "%%custom_python_magic -n1 -n2\nx=2",
207 assert result == expected_output
210 def test_cell_magic_nested() -> None:
211 src = "%%time\n%%time\n2+2"
212 result = format_cell(src, fast=True, mode=JUPYTER_MODE)
213 expected = "%%time\n%%time\n2 + 2"
214 assert result == expected
217 def test_cell_magic_with_magic_noop() -> None:
218 src = "%%t -n1\nls = !ls"
219 with pytest.raises(NothingChanged):
220 format_cell(src, fast=True, mode=JUPYTER_MODE)
223 def test_automagic() -> None:
224 src = "pip install black"
225 with pytest.raises(NothingChanged):
226 format_cell(src, fast=True, mode=JUPYTER_MODE)
229 def test_multiline_magic() -> None:
230 src = "%time 1 + \\\n2"
231 with pytest.raises(NothingChanged):
232 format_cell(src, fast=True, mode=JUPYTER_MODE)
235 def test_multiline_no_magic() -> None:
237 result = format_cell(src, fast=True, mode=JUPYTER_MODE)
239 assert result == expected
242 def test_cell_magic_with_invalid_body() -> None:
243 src = "%%time\nif True"
244 with pytest.raises(NothingChanged):
245 format_cell(src, fast=True, mode=JUPYTER_MODE)
248 def test_empty_cell() -> None:
250 with pytest.raises(NothingChanged):
251 format_cell(src, fast=True, mode=JUPYTER_MODE)
254 def test_entire_notebook_empty_metadata() -> None:
255 with open(DATA_DIR / "notebook_empty_metadata.ipynb", "rb") as fd:
256 content_bytes = fd.read()
257 content = content_bytes.decode()
258 result = format_file_contents(content, fast=True, mode=JUPYTER_MODE)
263 ' "cell_type": "code",\n'
264 ' "execution_count": null,\n'
272 ' "print(\\"foo\\")"\n'
276 ' "cell_type": "code",\n'
277 ' "execution_count": null,\n'
285 ' "nbformat_minor": 4\n'
288 assert result == expected
291 def test_entire_notebook_trailing_newline() -> None:
292 with open(DATA_DIR / "notebook_trailing_newline.ipynb", "rb") as fd:
293 content_bytes = fd.read()
294 content = content_bytes.decode()
295 result = format_file_contents(content, fast=True, mode=JUPYTER_MODE)
300 ' "cell_type": "code",\n'
301 ' "execution_count": null,\n'
309 ' "print(\\"foo\\")"\n'
313 ' "cell_type": "code",\n'
314 ' "execution_count": null,\n'
321 ' "interpreter": {\n'
322 ' "hash": "e758f3098b5b55f4d87fe30bbdc1367f20f246b483f96267ee70e6c40cb185d8"\n' # noqa:B950
325 ' "display_name": "Python 3.8.10 64-bit (\'black\': venv)",\n'
326 ' "name": "python3"\n'
328 ' "language_info": {\n'
329 ' "name": "python",\n'
334 ' "nbformat_minor": 4\n'
337 assert result == expected
340 def test_entire_notebook_no_trailing_newline() -> None:
341 with open(DATA_DIR / "notebook_no_trailing_newline.ipynb", "rb") as fd:
342 content_bytes = fd.read()
343 content = content_bytes.decode()
344 result = format_file_contents(content, fast=True, mode=JUPYTER_MODE)
349 ' "cell_type": "code",\n'
350 ' "execution_count": null,\n'
358 ' "print(\\"foo\\")"\n'
362 ' "cell_type": "code",\n'
363 ' "execution_count": null,\n'
370 ' "interpreter": {\n'
371 ' "hash": "e758f3098b5b55f4d87fe30bbdc1367f20f246b483f96267ee70e6c40cb185d8"\n' # noqa: B950
374 ' "display_name": "Python 3.8.10 64-bit (\'black\': venv)",\n'
375 ' "name": "python3"\n'
377 ' "language_info": {\n'
378 ' "name": "python",\n'
383 ' "nbformat_minor": 4\n'
386 assert result == expected
389 def test_entire_notebook_without_changes() -> None:
390 with open(DATA_DIR / "notebook_without_changes.ipynb", "rb") as fd:
391 content_bytes = fd.read()
392 content = content_bytes.decode()
393 with pytest.raises(NothingChanged):
394 format_file_contents(content, fast=True, mode=JUPYTER_MODE)
397 def test_non_python_notebook() -> None:
398 with open(DATA_DIR / "non_python_notebook.ipynb", "rb") as fd:
399 content_bytes = fd.read()
400 content = content_bytes.decode()
401 with pytest.raises(NothingChanged):
402 format_file_contents(content, fast=True, mode=JUPYTER_MODE)
405 def test_empty_string() -> None:
406 with pytest.raises(NothingChanged):
407 format_file_contents("", fast=True, mode=JUPYTER_MODE)
410 def test_unparseable_notebook() -> None:
411 path = DATA_DIR / "notebook_which_cant_be_parsed.ipynb"
412 msg = rf"File '{re.escape(str(path))}' cannot be parsed as valid Jupyter notebook\."
413 with pytest.raises(ValueError, match=msg):
414 format_file_in_place(path, fast=True, mode=JUPYTER_MODE)
417 def test_ipynb_diff_with_change() -> None:
418 result = runner.invoke(
421 str(DATA_DIR / "notebook_trailing_newline.ipynb"),
423 f"--config={EMPTY_CONFIG}",
426 expected = "@@ -1,3 +1,3 @@\n %%time\n \n-print('foo')\n+print(\"foo\")\n"
427 assert expected in result.output
430 def test_ipynb_diff_with_no_change() -> None:
431 result = runner.invoke(
434 str(DATA_DIR / "notebook_without_changes.ipynb"),
436 f"--config={EMPTY_CONFIG}",
439 expected = "1 file would be left unchanged."
440 assert expected in result.output
443 def test_cache_isnt_written_if_no_jupyter_deps_single(
444 monkeypatch: MonkeyPatch, tmp_path: pathlib.Path
446 # Check that the cache isn't written to if Jupyter dependencies aren't installed.
447 jupyter_dependencies_are_installed.cache_clear()
448 nb = DATA_DIR / "notebook_trailing_newline.ipynb"
449 tmp_nb = tmp_path / "notebook.ipynb"
450 with open(nb) as src, open(tmp_nb, "w") as dst:
451 dst.write(src.read())
453 "black.jupyter_dependencies_are_installed", lambda verbose, quiet: False
455 result = runner.invoke(
456 main, [str(tmp_path / "notebook.ipynb"), f"--config={EMPTY_CONFIG}"]
458 assert "No Python files are present to be formatted. Nothing to do" in result.output
459 jupyter_dependencies_are_installed.cache_clear()
461 "black.jupyter_dependencies_are_installed", lambda verbose, quiet: True
463 result = runner.invoke(
464 main, [str(tmp_path / "notebook.ipynb"), f"--config={EMPTY_CONFIG}"]
466 assert "reformatted" in result.output
469 def test_cache_isnt_written_if_no_jupyter_deps_dir(
470 monkeypatch: MonkeyPatch, tmp_path: pathlib.Path
472 # Check that the cache isn't written to if Jupyter dependencies aren't installed.
473 jupyter_dependencies_are_installed.cache_clear()
474 nb = DATA_DIR / "notebook_trailing_newline.ipynb"
475 tmp_nb = tmp_path / "notebook.ipynb"
476 with open(nb) as src, open(tmp_nb, "w") as dst:
477 dst.write(src.read())
479 "black.files.jupyter_dependencies_are_installed", lambda verbose, quiet: False
481 result = runner.invoke(main, [str(tmp_path), f"--config={EMPTY_CONFIG}"])
482 assert "No Python files are present to be formatted. Nothing to do" in result.output
483 jupyter_dependencies_are_installed.cache_clear()
485 "black.files.jupyter_dependencies_are_installed", lambda verbose, quiet: True
487 result = runner.invoke(main, [str(tmp_path), f"--config={EMPTY_CONFIG}"])
488 assert "reformatted" in result.output
491 def test_ipynb_flag(tmp_path: pathlib.Path) -> None:
492 nb = DATA_DIR / "notebook_trailing_newline.ipynb"
493 tmp_nb = tmp_path / "notebook.a_file_extension_which_is_definitely_not_ipynb"
494 with open(nb) as src, open(tmp_nb, "w") as dst:
495 dst.write(src.read())
496 result = runner.invoke(
502 f"--config={EMPTY_CONFIG}",
505 expected = "@@ -1,3 +1,3 @@\n %%time\n \n-print('foo')\n+print(\"foo\")\n"
506 assert expected in result.output
509 def test_ipynb_and_pyi_flags() -> None:
510 nb = DATA_DIR / "notebook_trailing_newline.ipynb"
511 result = runner.invoke(
518 f"--config={EMPTY_CONFIG}",
521 assert isinstance(result.exception, SystemExit)
522 expected = "Cannot pass both `pyi` and `ipynb` flags!\n"
523 assert result.output == expected
526 def test_unable_to_replace_magics(monkeypatch: MonkeyPatch) -> None:
527 src = "%%time\na = 'foo'"
528 monkeypatch.setattr("black.handle_ipynb_magics.TOKEN_HEX", lambda _: "foo")
530 AssertionError, match="Black was not able to replace IPython magic"
532 format_cell(src, fast=True, mode=JUPYTER_MODE)