]> git.madduck.net Git - etc/vim.git/blob - tests/test_ipynb.py

madduck's git repository

Every one of the projects in this repository is available at the canonical URL git://git.madduck.net/madduck/pub/<projectpath> — see each project's metadata for the exact URL.

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.

SSH access, as well as push access can be individually arranged.

If you use my repositories frequently, consider adding the following snippet to ~/.gitconfig and using the third clone URL listed for each project:

[url "git://git.madduck.net/madduck/"]
  insteadOf = madduck:

Assignment to env var in Jupyter Notebook doesn't round-trip (#2642)
[etc/vim.git] / tests / test_ipynb.py
1 import re
2
3 from click.testing import CliRunner
4 from black.handle_ipynb_magics import jupyter_dependencies_are_installed
5 from black import (
6     main,
7     NothingChanged,
8     format_cell,
9     format_file_contents,
10     format_file_in_place,
11 )
12 import pytest
13 from black import Mode
14 from _pytest.monkeypatch import MonkeyPatch
15 from py.path import local
16 from tests.util import DATA_DIR
17
18 pytestmark = pytest.mark.jupyter
19 pytest.importorskip("IPython", reason="IPython is an optional dependency")
20 pytest.importorskip("tokenize_rt", reason="tokenize-rt is an optional dependency")
21
22 JUPYTER_MODE = Mode(is_ipynb=True)
23
24 runner = CliRunner()
25
26
27 def test_noop() -> None:
28     src = 'foo = "a"'
29     with pytest.raises(NothingChanged):
30         format_cell(src, fast=True, mode=JUPYTER_MODE)
31
32
33 @pytest.mark.parametrize("fast", [True, False])
34 def test_trailing_semicolon(fast: bool) -> None:
35     src = 'foo = "a" ;'
36     result = format_cell(src, fast=fast, mode=JUPYTER_MODE)
37     expected = 'foo = "a";'
38     assert result == expected
39
40
41 def test_trailing_semicolon_with_comment() -> None:
42     src = 'foo = "a" ;  # bar'
43     result = format_cell(src, fast=True, mode=JUPYTER_MODE)
44     expected = 'foo = "a";  # bar'
45     assert result == expected
46
47
48 def test_trailing_semicolon_with_comment_on_next_line() -> None:
49     src = "import black;\n\n# this is a comment"
50     with pytest.raises(NothingChanged):
51         format_cell(src, fast=True, mode=JUPYTER_MODE)
52
53
54 def test_trailing_semicolon_indented() -> None:
55     src = "with foo:\n    plot_bar();"
56     with pytest.raises(NothingChanged):
57         format_cell(src, fast=True, mode=JUPYTER_MODE)
58
59
60 def test_trailing_semicolon_noop() -> None:
61     src = 'foo = "a";'
62     with pytest.raises(NothingChanged):
63         format_cell(src, fast=True, mode=JUPYTER_MODE)
64
65
66 def test_cell_magic() -> None:
67     src = "%%time\nfoo =bar"
68     result = format_cell(src, fast=True, mode=JUPYTER_MODE)
69     expected = "%%time\nfoo = bar"
70     assert result == expected
71
72
73 def test_cell_magic_noop() -> None:
74     src = "%%time\n2 + 2"
75     with pytest.raises(NothingChanged):
76         format_cell(src, fast=True, mode=JUPYTER_MODE)
77
78
79 @pytest.mark.parametrize(
80     "src, expected",
81     (
82         pytest.param("ls =!ls", "ls = !ls", id="System assignment"),
83         pytest.param("!ls\n'foo'", '!ls\n"foo"', id="System call"),
84         pytest.param("!!ls\n'foo'", '!!ls\n"foo"', id="Other system call"),
85         pytest.param("?str\n'foo'", '?str\n"foo"', id="Help"),
86         pytest.param("??str\n'foo'", '??str\n"foo"', id="Other help"),
87         pytest.param(
88             "%matplotlib inline\n'foo'",
89             '%matplotlib inline\n"foo"',
90             id="Line magic with argument",
91         ),
92         pytest.param("%time\n'foo'", '%time\n"foo"', id="Line magic without argument"),
93         pytest.param(
94             "env =  %env var", "env = %env var", id="Assignment to environment variable"
95         ),
96         pytest.param("env =  %env", "env = %env", id="Assignment to magic"),
97     ),
98 )
99 def test_magic(src: str, expected: str) -> None:
100     result = format_cell(src, fast=True, mode=JUPYTER_MODE)
101     assert result == expected
102
103
104 @pytest.mark.parametrize(
105     "src",
106     (
107         "%%bash\n2+2",
108         "%%html --isolated\n2+2",
109     ),
110 )
111 def test_non_python_magics(src: str) -> None:
112     with pytest.raises(NothingChanged):
113         format_cell(src, fast=True, mode=JUPYTER_MODE)
114
115
116 def test_set_input() -> None:
117     src = "a = b??"
118     with pytest.raises(NothingChanged):
119         format_cell(src, fast=True, mode=JUPYTER_MODE)
120
121
122 def test_input_already_contains_transformed_magic() -> None:
123     src = '%time foo()\nget_ipython().run_cell_magic("time", "", "foo()\\n")'
124     with pytest.raises(NothingChanged):
125         format_cell(src, fast=True, mode=JUPYTER_MODE)
126
127
128 def test_magic_noop() -> None:
129     src = "ls = !ls"
130     with pytest.raises(NothingChanged):
131         format_cell(src, fast=True, mode=JUPYTER_MODE)
132
133
134 def test_cell_magic_with_magic() -> None:
135     src = "%%t -n1\nls =!ls"
136     result = format_cell(src, fast=True, mode=JUPYTER_MODE)
137     expected = "%%t -n1\nls = !ls"
138     assert result == expected
139
140
141 def test_cell_magic_nested() -> None:
142     src = "%%time\n%%time\n2+2"
143     result = format_cell(src, fast=True, mode=JUPYTER_MODE)
144     expected = "%%time\n%%time\n2 + 2"
145     assert result == expected
146
147
148 def test_cell_magic_with_magic_noop() -> None:
149     src = "%%t -n1\nls = !ls"
150     with pytest.raises(NothingChanged):
151         format_cell(src, fast=True, mode=JUPYTER_MODE)
152
153
154 def test_automagic() -> None:
155     src = "pip install black"
156     with pytest.raises(NothingChanged):
157         format_cell(src, fast=True, mode=JUPYTER_MODE)
158
159
160 def test_multiline_magic() -> None:
161     src = "%time 1 + \\\n2"
162     with pytest.raises(NothingChanged):
163         format_cell(src, fast=True, mode=JUPYTER_MODE)
164
165
166 def test_multiline_no_magic() -> None:
167     src = "1 + \\\n2"
168     result = format_cell(src, fast=True, mode=JUPYTER_MODE)
169     expected = "1 + 2"
170     assert result == expected
171
172
173 def test_cell_magic_with_invalid_body() -> None:
174     src = "%%time\nif True"
175     with pytest.raises(NothingChanged):
176         format_cell(src, fast=True, mode=JUPYTER_MODE)
177
178
179 def test_empty_cell() -> None:
180     src = ""
181     with pytest.raises(NothingChanged):
182         format_cell(src, fast=True, mode=JUPYTER_MODE)
183
184
185 def test_entire_notebook_empty_metadata() -> None:
186     with open(DATA_DIR / "notebook_empty_metadata.ipynb", "rb") as fd:
187         content_bytes = fd.read()
188     content = content_bytes.decode()
189     result = format_file_contents(content, fast=True, mode=JUPYTER_MODE)
190     expected = (
191         "{\n"
192         ' "cells": [\n'
193         "  {\n"
194         '   "cell_type": "code",\n'
195         '   "execution_count": null,\n'
196         '   "metadata": {\n'
197         '    "tags": []\n'
198         "   },\n"
199         '   "outputs": [],\n'
200         '   "source": [\n'
201         '    "%%time\\n",\n'
202         '    "\\n",\n'
203         '    "print(\\"foo\\")"\n'
204         "   ]\n"
205         "  },\n"
206         "  {\n"
207         '   "cell_type": "code",\n'
208         '   "execution_count": null,\n'
209         '   "metadata": {},\n'
210         '   "outputs": [],\n'
211         '   "source": []\n'
212         "  }\n"
213         " ],\n"
214         ' "metadata": {},\n'
215         ' "nbformat": 4,\n'
216         ' "nbformat_minor": 4\n'
217         "}\n"
218     )
219     assert result == expected
220
221
222 def test_entire_notebook_trailing_newline() -> None:
223     with open(DATA_DIR / "notebook_trailing_newline.ipynb", "rb") as fd:
224         content_bytes = fd.read()
225     content = content_bytes.decode()
226     result = format_file_contents(content, fast=True, mode=JUPYTER_MODE)
227     expected = (
228         "{\n"
229         ' "cells": [\n'
230         "  {\n"
231         '   "cell_type": "code",\n'
232         '   "execution_count": null,\n'
233         '   "metadata": {\n'
234         '    "tags": []\n'
235         "   },\n"
236         '   "outputs": [],\n'
237         '   "source": [\n'
238         '    "%%time\\n",\n'
239         '    "\\n",\n'
240         '    "print(\\"foo\\")"\n'
241         "   ]\n"
242         "  },\n"
243         "  {\n"
244         '   "cell_type": "code",\n'
245         '   "execution_count": null,\n'
246         '   "metadata": {},\n'
247         '   "outputs": [],\n'
248         '   "source": []\n'
249         "  }\n"
250         " ],\n"
251         ' "metadata": {\n'
252         '  "interpreter": {\n'
253         '   "hash": "e758f3098b5b55f4d87fe30bbdc1367f20f246b483f96267ee70e6c40cb185d8"\n'  # noqa:B950
254         "  },\n"
255         '  "kernelspec": {\n'
256         '   "display_name": "Python 3.8.10 64-bit (\'black\': venv)",\n'
257         '   "name": "python3"\n'
258         "  },\n"
259         '  "language_info": {\n'
260         '   "name": "python",\n'
261         '   "version": ""\n'
262         "  }\n"
263         " },\n"
264         ' "nbformat": 4,\n'
265         ' "nbformat_minor": 4\n'
266         "}\n"
267     )
268     assert result == expected
269
270
271 def test_entire_notebook_no_trailing_newline() -> None:
272     with open(DATA_DIR / "notebook_no_trailing_newline.ipynb", "rb") as fd:
273         content_bytes = fd.read()
274     content = content_bytes.decode()
275     result = format_file_contents(content, fast=True, mode=JUPYTER_MODE)
276     expected = (
277         "{\n"
278         ' "cells": [\n'
279         "  {\n"
280         '   "cell_type": "code",\n'
281         '   "execution_count": null,\n'
282         '   "metadata": {\n'
283         '    "tags": []\n'
284         "   },\n"
285         '   "outputs": [],\n'
286         '   "source": [\n'
287         '    "%%time\\n",\n'
288         '    "\\n",\n'
289         '    "print(\\"foo\\")"\n'
290         "   ]\n"
291         "  },\n"
292         "  {\n"
293         '   "cell_type": "code",\n'
294         '   "execution_count": null,\n'
295         '   "metadata": {},\n'
296         '   "outputs": [],\n'
297         '   "source": []\n'
298         "  }\n"
299         " ],\n"
300         ' "metadata": {\n'
301         '  "interpreter": {\n'
302         '   "hash": "e758f3098b5b55f4d87fe30bbdc1367f20f246b483f96267ee70e6c40cb185d8"\n'  # noqa: B950
303         "  },\n"
304         '  "kernelspec": {\n'
305         '   "display_name": "Python 3.8.10 64-bit (\'black\': venv)",\n'
306         '   "name": "python3"\n'
307         "  },\n"
308         '  "language_info": {\n'
309         '   "name": "python",\n'
310         '   "version": ""\n'
311         "  }\n"
312         " },\n"
313         ' "nbformat": 4,\n'
314         ' "nbformat_minor": 4\n'
315         "}"
316     )
317     assert result == expected
318
319
320 def test_entire_notebook_without_changes() -> None:
321     with open(DATA_DIR / "notebook_without_changes.ipynb", "rb") as fd:
322         content_bytes = fd.read()
323     content = content_bytes.decode()
324     with pytest.raises(NothingChanged):
325         format_file_contents(content, fast=True, mode=JUPYTER_MODE)
326
327
328 def test_non_python_notebook() -> None:
329     with open(DATA_DIR / "non_python_notebook.ipynb", "rb") as fd:
330         content_bytes = fd.read()
331     content = content_bytes.decode()
332     with pytest.raises(NothingChanged):
333         format_file_contents(content, fast=True, mode=JUPYTER_MODE)
334
335
336 def test_empty_string() -> None:
337     with pytest.raises(NothingChanged):
338         format_file_contents("", fast=True, mode=JUPYTER_MODE)
339
340
341 def test_unparseable_notebook() -> None:
342     path = DATA_DIR / "notebook_which_cant_be_parsed.ipynb"
343     msg = rf"File '{re.escape(str(path))}' cannot be parsed as valid Jupyter notebook\."
344     with pytest.raises(ValueError, match=msg):
345         format_file_in_place(path, fast=True, mode=JUPYTER_MODE)
346
347
348 def test_ipynb_diff_with_change() -> None:
349     result = runner.invoke(
350         main,
351         [
352             str(DATA_DIR / "notebook_trailing_newline.ipynb"),
353             "--diff",
354         ],
355     )
356     expected = "@@ -1,3 +1,3 @@\n %%time\n \n-print('foo')\n" '+print("foo")\n'
357     assert expected in result.output
358
359
360 def test_ipynb_diff_with_no_change() -> None:
361     result = runner.invoke(
362         main,
363         [
364             str(DATA_DIR / "notebook_without_changes.ipynb"),
365             "--diff",
366         ],
367     )
368     expected = "1 file would be left unchanged."
369     assert expected in result.output
370
371
372 def test_cache_isnt_written_if_no_jupyter_deps_single(
373     monkeypatch: MonkeyPatch, tmpdir: local
374 ) -> None:
375     # Check that the cache isn't written to if Jupyter dependencies aren't installed.
376     jupyter_dependencies_are_installed.cache_clear()
377     nb = DATA_DIR / "notebook_trailing_newline.ipynb"
378     tmp_nb = tmpdir / "notebook.ipynb"
379     with open(nb) as src, open(tmp_nb, "w") as dst:
380         dst.write(src.read())
381     monkeypatch.setattr(
382         "black.jupyter_dependencies_are_installed", lambda verbose, quiet: False
383     )
384     result = runner.invoke(main, [str(tmpdir / "notebook.ipynb")])
385     assert "No Python files are present to be formatted. Nothing to do" in result.output
386     jupyter_dependencies_are_installed.cache_clear()
387     monkeypatch.setattr(
388         "black.jupyter_dependencies_are_installed", lambda verbose, quiet: True
389     )
390     result = runner.invoke(main, [str(tmpdir / "notebook.ipynb")])
391     assert "reformatted" in result.output
392
393
394 def test_cache_isnt_written_if_no_jupyter_deps_dir(
395     monkeypatch: MonkeyPatch, tmpdir: local
396 ) -> None:
397     # Check that the cache isn't written to if Jupyter dependencies aren't installed.
398     jupyter_dependencies_are_installed.cache_clear()
399     nb = DATA_DIR / "notebook_trailing_newline.ipynb"
400     tmp_nb = tmpdir / "notebook.ipynb"
401     with open(nb) as src, open(tmp_nb, "w") as dst:
402         dst.write(src.read())
403     monkeypatch.setattr(
404         "black.files.jupyter_dependencies_are_installed", lambda verbose, quiet: False
405     )
406     result = runner.invoke(main, [str(tmpdir)])
407     assert "No Python files are present to be formatted. Nothing to do" in result.output
408     jupyter_dependencies_are_installed.cache_clear()
409     monkeypatch.setattr(
410         "black.files.jupyter_dependencies_are_installed", lambda verbose, quiet: True
411     )
412     result = runner.invoke(main, [str(tmpdir)])
413     assert "reformatted" in result.output
414
415
416 def test_ipynb_flag(tmpdir: local) -> None:
417     nb = DATA_DIR / "notebook_trailing_newline.ipynb"
418     tmp_nb = tmpdir / "notebook.a_file_extension_which_is_definitely_not_ipynb"
419     with open(nb) as src, open(tmp_nb, "w") as dst:
420         dst.write(src.read())
421     result = runner.invoke(
422         main,
423         [
424             str(tmp_nb),
425             "--diff",
426             "--ipynb",
427         ],
428     )
429     expected = "@@ -1,3 +1,3 @@\n %%time\n \n-print('foo')\n" '+print("foo")\n'
430     assert expected in result.output
431
432
433 def test_ipynb_and_pyi_flags() -> None:
434     nb = DATA_DIR / "notebook_trailing_newline.ipynb"
435     result = runner.invoke(
436         main,
437         [
438             str(nb),
439             "--pyi",
440             "--ipynb",
441             "--diff",
442         ],
443     )
444     assert isinstance(result.exception, SystemExit)
445     expected = "Cannot pass both `pyi` and `ipynb` flags!\n"
446     assert result.output == expected
447
448
449 def test_unable_to_replace_magics(monkeypatch: MonkeyPatch) -> None:
450     src = "%%time\na = 'foo'"
451     monkeypatch.setattr("black.handle_ipynb_magics.TOKEN_HEX", lambda _: "foo")
452     with pytest.raises(
453         AssertionError, match="Black was not able to replace IPython magic"
454     ):
455         format_cell(src, fast=True, mode=JUPYTER_MODE)