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.
4 from contextlib import ExitStack as does_not_raise
5 from dataclasses import replace
6 from typing import ContextManager
9 from _pytest.monkeypatch import MonkeyPatch
10 from click.testing import CliRunner
20 from black.handle_ipynb_magics import jupyter_dependencies_are_installed
21 from tests.util import DATA_DIR, get_case_path, read_jupyter_notebook
23 with contextlib.suppress(ModuleNotFoundError):
25 pytestmark = pytest.mark.jupyter
26 pytest.importorskip("IPython", reason="IPython is an optional dependency")
27 pytest.importorskip("tokenize_rt", reason="tokenize-rt is an optional dependency")
29 JUPYTER_MODE = Mode(is_ipynb=True)
31 EMPTY_CONFIG = DATA_DIR / "empty_pyproject.toml"
36 def test_noop() -> None:
38 with pytest.raises(NothingChanged):
39 format_cell(src, fast=True, mode=JUPYTER_MODE)
42 @pytest.mark.parametrize("fast", [True, False])
43 def test_trailing_semicolon(fast: bool) -> None:
45 result = format_cell(src, fast=fast, mode=JUPYTER_MODE)
46 expected = 'foo = "a";'
47 assert result == expected
50 def test_trailing_semicolon_with_comment() -> None:
51 src = 'foo = "a" ; # bar'
52 result = format_cell(src, fast=True, mode=JUPYTER_MODE)
53 expected = 'foo = "a"; # bar'
54 assert result == expected
57 def test_trailing_semicolon_with_comment_on_next_line() -> None:
58 src = "import black;\n\n# this is a comment"
59 with pytest.raises(NothingChanged):
60 format_cell(src, fast=True, mode=JUPYTER_MODE)
63 def test_trailing_semicolon_indented() -> None:
64 src = "with foo:\n plot_bar();"
65 with pytest.raises(NothingChanged):
66 format_cell(src, fast=True, mode=JUPYTER_MODE)
69 def test_trailing_semicolon_noop() -> None:
71 with pytest.raises(NothingChanged):
72 format_cell(src, fast=True, mode=JUPYTER_MODE)
75 @pytest.mark.parametrize(
78 pytest.param(JUPYTER_MODE, id="default mode"),
80 replace(JUPYTER_MODE, python_cell_magics={"cust1", "cust2"}),
81 id="custom cell magics mode",
85 def test_cell_magic(mode: Mode) -> None:
86 src = "%%time\nfoo =bar"
87 result = format_cell(src, fast=True, mode=mode)
88 expected = "%%time\nfoo = bar"
89 assert result == expected
92 def test_cell_magic_noop() -> None:
94 with pytest.raises(NothingChanged):
95 format_cell(src, fast=True, mode=JUPYTER_MODE)
98 @pytest.mark.parametrize(
101 pytest.param(JUPYTER_MODE, id="default mode"),
103 replace(JUPYTER_MODE, python_cell_magics={"cust1", "cust2"}),
104 id="custom cell magics mode",
108 @pytest.mark.parametrize(
111 pytest.param("ls =!ls", "ls = !ls", id="System assignment"),
112 pytest.param("!ls\n'foo'", '!ls\n"foo"', id="System call"),
113 pytest.param("!!ls\n'foo'", '!!ls\n"foo"', id="Other system call"),
114 pytest.param("?str\n'foo'", '?str\n"foo"', id="Help"),
115 pytest.param("??str\n'foo'", '??str\n"foo"', id="Other help"),
117 "%matplotlib inline\n'foo'",
118 '%matplotlib inline\n"foo"',
119 id="Line magic with argument",
121 pytest.param("%time\n'foo'", '%time\n"foo"', id="Line magic without argument"),
123 "env = %env var", "env = %env var", id="Assignment to environment variable"
125 pytest.param("env = %env", "env = %env", id="Assignment to magic"),
128 def test_magic(src: str, expected: str, mode: Mode) -> None:
129 result = format_cell(src, fast=True, mode=mode)
130 assert result == expected
133 @pytest.mark.parametrize(
137 "%%html --isolated\n2+2",
138 "%%writefile e.txt\n meh\n meh",
141 def test_non_python_magics(src: str) -> None:
142 with pytest.raises(NothingChanged):
143 format_cell(src, fast=True, mode=JUPYTER_MODE)
147 IPython.version_info < (8, 3),
148 reason="Change in how TransformerManager transforms this input",
150 def test_set_input() -> None:
153 result = format_cell(src, fast=True, mode=JUPYTER_MODE)
154 assert result == expected
157 def test_input_already_contains_transformed_magic() -> None:
158 src = '%time foo()\nget_ipython().run_cell_magic("time", "", "foo()\\n")'
159 with pytest.raises(NothingChanged):
160 format_cell(src, fast=True, mode=JUPYTER_MODE)
163 def test_magic_noop() -> None:
165 with pytest.raises(NothingChanged):
166 format_cell(src, fast=True, mode=JUPYTER_MODE)
169 def test_cell_magic_with_magic() -> None:
170 src = "%%timeit -n1\nls =!ls"
171 result = format_cell(src, fast=True, mode=JUPYTER_MODE)
172 expected = "%%timeit -n1\nls = !ls"
173 assert result == expected
176 @pytest.mark.parametrize(
177 "mode, expected_output, expectation",
181 "%%custom_python_magic -n1 -n2\nx=2",
182 pytest.raises(NothingChanged),
183 id="No change when cell magic not registered",
186 replace(JUPYTER_MODE, python_cell_magics={"cust1", "cust2"}),
187 "%%custom_python_magic -n1 -n2\nx=2",
188 pytest.raises(NothingChanged),
189 id="No change when other cell magics registered",
192 replace(JUPYTER_MODE, python_cell_magics={"custom_python_magic", "cust1"}),
193 "%%custom_python_magic -n1 -n2\nx = 2",
195 id="Correctly change when cell magic registered",
199 def test_cell_magic_with_custom_python_magic(
200 mode: Mode, expected_output: str, expectation: ContextManager[object]
203 result = format_cell(
204 "%%custom_python_magic -n1 -n2\nx=2",
208 assert result == expected_output
211 def test_cell_magic_nested() -> None:
212 src = "%%time\n%%time\n2+2"
213 result = format_cell(src, fast=True, mode=JUPYTER_MODE)
214 expected = "%%time\n%%time\n2 + 2"
215 assert result == expected
218 def test_cell_magic_with_magic_noop() -> None:
219 src = "%%t -n1\nls = !ls"
220 with pytest.raises(NothingChanged):
221 format_cell(src, fast=True, mode=JUPYTER_MODE)
224 def test_automagic() -> None:
225 src = "pip install black"
226 with pytest.raises(NothingChanged):
227 format_cell(src, fast=True, mode=JUPYTER_MODE)
230 def test_multiline_magic() -> None:
231 src = "%time 1 + \\\n2"
232 with pytest.raises(NothingChanged):
233 format_cell(src, fast=True, mode=JUPYTER_MODE)
236 def test_multiline_no_magic() -> None:
238 result = format_cell(src, fast=True, mode=JUPYTER_MODE)
240 assert result == expected
243 def test_cell_magic_with_invalid_body() -> None:
244 src = "%%time\nif True"
245 with pytest.raises(NothingChanged):
246 format_cell(src, fast=True, mode=JUPYTER_MODE)
249 def test_empty_cell() -> None:
251 with pytest.raises(NothingChanged):
252 format_cell(src, fast=True, mode=JUPYTER_MODE)
255 def test_entire_notebook_empty_metadata() -> None:
256 content = read_jupyter_notebook("jupyter", "notebook_empty_metadata")
257 result = format_file_contents(content, fast=True, mode=JUPYTER_MODE)
262 ' "cell_type": "code",\n'
263 ' "execution_count": null,\n'
271 ' "print(\\"foo\\")"\n'
275 ' "cell_type": "code",\n'
276 ' "execution_count": null,\n'
284 ' "nbformat_minor": 4\n'
287 assert result == expected
290 def test_entire_notebook_trailing_newline() -> None:
291 content = read_jupyter_notebook("jupyter", "notebook_trailing_newline")
292 result = format_file_contents(content, fast=True, mode=JUPYTER_MODE)
297 ' "cell_type": "code",\n'
298 ' "execution_count": null,\n'
306 ' "print(\\"foo\\")"\n'
310 ' "cell_type": "code",\n'
311 ' "execution_count": null,\n'
318 ' "interpreter": {\n'
319 ' "hash": "e758f3098b5b55f4d87fe30bbdc1367f20f246b483f96267ee70e6c40cb185d8"\n' # noqa:B950
322 ' "display_name": "Python 3.8.10 64-bit (\'black\': venv)",\n'
323 ' "name": "python3"\n'
325 ' "language_info": {\n'
326 ' "name": "python",\n'
331 ' "nbformat_minor": 4\n'
334 assert result == expected
337 def test_entire_notebook_no_trailing_newline() -> None:
338 content = read_jupyter_notebook("jupyter", "notebook_no_trailing_newline")
339 result = format_file_contents(content, fast=True, mode=JUPYTER_MODE)
344 ' "cell_type": "code",\n'
345 ' "execution_count": null,\n'
353 ' "print(\\"foo\\")"\n'
357 ' "cell_type": "code",\n'
358 ' "execution_count": null,\n'
365 ' "interpreter": {\n'
366 ' "hash": "e758f3098b5b55f4d87fe30bbdc1367f20f246b483f96267ee70e6c40cb185d8"\n' # noqa: B950
369 ' "display_name": "Python 3.8.10 64-bit (\'black\': venv)",\n'
370 ' "name": "python3"\n'
372 ' "language_info": {\n'
373 ' "name": "python",\n'
378 ' "nbformat_minor": 4\n'
381 assert result == expected
384 def test_entire_notebook_without_changes() -> None:
385 content = read_jupyter_notebook("jupyter", "notebook_without_changes")
386 with pytest.raises(NothingChanged):
387 format_file_contents(content, fast=True, mode=JUPYTER_MODE)
390 def test_non_python_notebook() -> None:
391 content = read_jupyter_notebook("jupyter", "non_python_notebook")
393 with pytest.raises(NothingChanged):
394 format_file_contents(content, fast=True, mode=JUPYTER_MODE)
397 def test_empty_string() -> None:
398 with pytest.raises(NothingChanged):
399 format_file_contents("", fast=True, mode=JUPYTER_MODE)
402 def test_unparseable_notebook() -> None:
403 path = get_case_path("jupyter", "notebook_which_cant_be_parsed.ipynb")
404 msg = rf"File '{re.escape(str(path))}' cannot be parsed as valid Jupyter notebook\."
405 with pytest.raises(ValueError, match=msg):
406 format_file_in_place(path, fast=True, mode=JUPYTER_MODE)
409 def test_ipynb_diff_with_change() -> None:
410 result = runner.invoke(
413 str(get_case_path("jupyter", "notebook_trailing_newline.ipynb")),
415 f"--config={EMPTY_CONFIG}",
418 expected = "@@ -1,3 +1,3 @@\n %%time\n \n-print('foo')\n+print(\"foo\")\n"
419 assert expected in result.output
422 def test_ipynb_diff_with_no_change() -> None:
423 result = runner.invoke(
426 str(get_case_path("jupyter", "notebook_without_changes.ipynb")),
428 f"--config={EMPTY_CONFIG}",
431 expected = "1 file would be left unchanged."
432 assert expected in result.output
435 def test_cache_isnt_written_if_no_jupyter_deps_single(
436 monkeypatch: MonkeyPatch, tmp_path: pathlib.Path
438 # Check that the cache isn't written to if Jupyter dependencies aren't installed.
439 jupyter_dependencies_are_installed.cache_clear()
440 nb = get_case_path("jupyter", "notebook_trailing_newline.ipynb")
441 tmp_nb = tmp_path / "notebook.ipynb"
442 tmp_nb.write_bytes(nb.read_bytes())
443 monkeypatch.setattr("black.jupyter_dependencies_are_installed", lambda warn: False)
444 result = runner.invoke(
445 main, [str(tmp_path / "notebook.ipynb"), f"--config={EMPTY_CONFIG}"]
447 assert "No Python files are present to be formatted. Nothing to do" in result.output
448 jupyter_dependencies_are_installed.cache_clear()
449 monkeypatch.setattr("black.jupyter_dependencies_are_installed", lambda warn: True)
450 result = runner.invoke(
451 main, [str(tmp_path / "notebook.ipynb"), f"--config={EMPTY_CONFIG}"]
453 assert "reformatted" in result.output
456 def test_cache_isnt_written_if_no_jupyter_deps_dir(
457 monkeypatch: MonkeyPatch, tmp_path: pathlib.Path
459 # Check that the cache isn't written to if Jupyter dependencies aren't installed.
460 jupyter_dependencies_are_installed.cache_clear()
461 nb = get_case_path("jupyter", "notebook_trailing_newline.ipynb")
462 tmp_nb = tmp_path / "notebook.ipynb"
463 tmp_nb.write_bytes(nb.read_bytes())
465 "black.files.jupyter_dependencies_are_installed", lambda warn: False
467 result = runner.invoke(main, [str(tmp_path), f"--config={EMPTY_CONFIG}"])
468 assert "No Python files are present to be formatted. Nothing to do" in result.output
469 jupyter_dependencies_are_installed.cache_clear()
471 "black.files.jupyter_dependencies_are_installed", lambda warn: True
473 result = runner.invoke(main, [str(tmp_path), f"--config={EMPTY_CONFIG}"])
474 assert "reformatted" in result.output
477 def test_ipynb_flag(tmp_path: pathlib.Path) -> None:
478 nb = get_case_path("jupyter", "notebook_trailing_newline.ipynb")
479 tmp_nb = tmp_path / "notebook.a_file_extension_which_is_definitely_not_ipynb"
480 tmp_nb.write_bytes(nb.read_bytes())
481 result = runner.invoke(
487 f"--config={EMPTY_CONFIG}",
490 expected = "@@ -1,3 +1,3 @@\n %%time\n \n-print('foo')\n+print(\"foo\")\n"
491 assert expected in result.output
494 def test_ipynb_and_pyi_flags() -> None:
495 nb = get_case_path("jupyter", "notebook_trailing_newline.ipynb")
496 result = runner.invoke(
503 f"--config={EMPTY_CONFIG}",
506 assert isinstance(result.exception, SystemExit)
507 expected = "Cannot pass both `pyi` and `ipynb` flags!\n"
508 assert result.output == expected
511 def test_unable_to_replace_magics(monkeypatch: MonkeyPatch) -> None:
512 src = "%%time\na = 'foo'"
513 monkeypatch.setattr("black.handle_ipynb_magics.TOKEN_HEX", lambda _: "foo")
515 AssertionError, match="Black was not able to replace IPython magic"
517 format_cell(src, fast=True, mode=JUPYTER_MODE)