11"""Tests for `j-cli convert`."""
22
3+ import subprocess
34from pathlib import Path
45
56import nbformat
67import pytest
78from click .testing import CliRunner
89
10+ from jupyter_jcli import pair_baseline
11+ from jupyter_jcli .canonicalize import canonicalize_py_text
912from jupyter_jcli .cli import main
1013from jupyter_jcli .parser import parse_py_percent , parse_py_percent_text
1114
@@ -19,6 +22,46 @@ def _invoke(*args: str):
1922 return runner .invoke (main , list (args ), catch_exceptions = False )
2023
2124
25+ @pytest .fixture
26+ def git_repo (tmp_path : Path ) -> Path :
27+ subprocess .run (["git" , "init" ], cwd = str (tmp_path ), check = True , capture_output = True )
28+ subprocess .run (
29+ ["git" , "config" , "user.email" , "test@test.com" ],
30+ cwd = str (tmp_path ),
31+ check = True ,
32+ capture_output = True ,
33+ )
34+ subprocess .run (
35+ ["git" , "config" , "user.name" , "Test User" ],
36+ cwd = str (tmp_path ),
37+ check = True ,
38+ capture_output = True ,
39+ )
40+ return tmp_path
41+
42+
43+ def _git (repo : Path , * args : str ) -> subprocess .CompletedProcess [str ]:
44+ return subprocess .run (
45+ ["git" , * args ],
46+ cwd = str (repo ),
47+ check = True ,
48+ capture_output = True ,
49+ text = True ,
50+ )
51+
52+
53+ def _has_ref (repo : Path , rel_py_path : str ) -> bool :
54+ ref_name = pair_baseline ._ref_name (Path (rel_py_path ).as_posix ())
55+ result = subprocess .run (
56+ ["git" , "for-each-ref" , ref_name , "--format=%(refname)" ],
57+ cwd = str (repo ),
58+ check = True ,
59+ capture_output = True ,
60+ text = True ,
61+ )
62+ return result .stdout .strip () == ref_name
63+
64+
2265def _make_ipynb (cells : list [tuple [str , str , list ]], kernel : str = "python3" ) -> nbformat .NotebookNode :
2366 nb = nbformat .v4 .new_notebook ()
2467 nb .metadata ["kernelspec" ] = {"name" : kernel , "display_name" : kernel , "language" : "python" }
@@ -87,6 +130,51 @@ def test_roundtrip_content(self, tmp_path):
87130 assert parsed .cells [0 ].source == "a = 1"
88131 assert parsed .cells [1 ].source == "b = 2"
89132
133+ def test_canonical_ipynb_to_py_refreshes_baseline (self , git_repo ):
134+ nb = _make_ipynb ([("code" , "x = 1" , [])])
135+ ipynb = git_repo / "nb.ipynb"
136+ nbformat .write (nb , str (ipynb ))
137+ py = git_repo / "nb.py"
138+
139+ result = _invoke ("convert" , "ipynb-to-py" , str (ipynb ), str (py ))
140+
141+ assert result .exit_code == 0
142+ assert _has_ref (git_repo , "nb.py" )
143+ assert pair_baseline .read_baseline (py ) == canonicalize_py_text (py .read_text (encoding = "utf-8" ))
144+
145+ def test_dummy_ipynb_to_py_refreshes_baseline (self , git_repo ):
146+ nb = _make_ipynb ([("code" , "x = 1" , [])])
147+ ipynb = git_repo / "nb.ipynb"
148+ nbformat .write (nb , str (ipynb ))
149+ py = git_repo / "nb.dummy.py"
150+
151+ result = _invoke ("convert" , "ipynb-to-py" , str (ipynb ), str (py ))
152+
153+ assert result .exit_code == 0
154+ assert _has_ref (git_repo , "nb.dummy.py" )
155+
156+ def test_noncanonical_ipynb_to_py_does_not_refresh_baseline (self , git_repo ):
157+ nb = _make_ipynb ([("code" , "x = 1" , [])])
158+ ipynb = git_repo / "nb.ipynb"
159+ nbformat .write (nb , str (ipynb ))
160+ py = git_repo / "custom.py"
161+
162+ result = _invoke ("convert" , "ipynb-to-py" , str (ipynb ), str (py ))
163+
164+ assert result .exit_code == 0
165+ assert not _has_ref (git_repo , "custom.py" )
166+
167+ def test_ipynb_to_py_non_git_still_succeeds (self , tmp_path ):
168+ nb = _make_ipynb ([("code" , "x = 1" , [])])
169+ ipynb = tmp_path / "nb.ipynb"
170+ nbformat .write (nb , str (ipynb ))
171+ py = tmp_path / "nb.py"
172+
173+ result = _invoke ("convert" , "ipynb-to-py" , str (ipynb ), str (py ))
174+
175+ assert result .exit_code == 0
176+ assert py .exists ()
177+
90178
91179# ---------------------------------------------------------------------------
92180# py-to-ipynb — new file creation
@@ -129,6 +217,37 @@ def test_dummy_py_default_output(self, tmp_path):
129217 assert result .exit_code == 0
130218 assert (tmp_path / "foo.ipynb" ).exists ()
131219
220+ def test_canonical_py_to_ipynb_refreshes_baseline (self , git_repo ):
221+ py = git_repo / "script.py"
222+ py .write_text ("# %%\n x = 1\n " , encoding = "utf-8" )
223+
224+ result = _invoke ("convert" , "py-to-ipynb" , str (py ))
225+
226+ assert result .exit_code == 0
227+ assert (git_repo / "script.ipynb" ).exists ()
228+ assert _has_ref (git_repo , "script.py" )
229+ assert pair_baseline .read_baseline (py ) == canonicalize_py_text (py .read_text (encoding = "utf-8" ))
230+
231+ def test_noncanonical_py_to_ipynb_does_not_refresh_baseline (self , git_repo ):
232+ py = git_repo / "script.py"
233+ py .write_text ("# %%\n x = 1\n " , encoding = "utf-8" )
234+ out = git_repo / "custom.ipynb"
235+
236+ result = _invoke ("convert" , "py-to-ipynb" , str (py ), str (out ))
237+
238+ assert result .exit_code == 0
239+ assert out .exists ()
240+ assert not _has_ref (git_repo , "script.py" )
241+
242+ def test_py_to_ipynb_non_git_still_succeeds (self , tmp_path ):
243+ py = tmp_path / "script.py"
244+ py .write_text ("# %%\n x = 1\n " , encoding = "utf-8" )
245+
246+ result = _invoke ("convert" , "py-to-ipynb" , str (py ))
247+
248+ assert result .exit_code == 0
249+ assert (tmp_path / "script.ipynb" ).exists ()
250+
132251
133252# ---------------------------------------------------------------------------
134253# py-to-ipynb — in-place update (preserve outputs)
0 commit comments