Skip to content

Commit 0dedc41

Browse files
authored
Add export command extract alternative lockfile format (#70)
1 parent 77a5659 commit 0dedc41

File tree

3 files changed

+120
-26
lines changed

3 files changed

+120
-26
lines changed

src/juv/__init__.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,18 @@ def tree(
494494
tree(path=Path(file))
495495

496496

497+
@cli.command()
498+
@click.argument("file", type=click.Path(exists=True), required=True)
499+
def export(
500+
*,
501+
file: str,
502+
) -> None:
503+
"""Export the notebook's lockfile to an alternate format."""
504+
from ._export import export
505+
506+
export(path=Path(file))
507+
508+
497509
def main() -> None:
498510
"""Run the CLI."""
499511
upgrade_legacy_jupyter_command(sys.argv)

src/juv/_export.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import sys
2+
import tempfile
3+
from pathlib import Path
4+
5+
import jupytext
6+
7+
from ._nbutils import code_cell, write_ipynb
8+
from ._pep723 import includes_inline_metadata
9+
from ._utils import find
10+
from ._uv import uv
11+
12+
13+
def export(
14+
path: Path,
15+
) -> None:
16+
notebook = jupytext.read(path, fmt="ipynb")
17+
lockfile_contents = notebook.get("metadata", {}).get("uv.lock")
18+
19+
# need a reference so we can modify the cell["source"]
20+
cell = find(
21+
lambda cell: (
22+
cell["cell_type"] == "code"
23+
and includes_inline_metadata("".join(cell["source"]))
24+
),
25+
notebook["cells"],
26+
)
27+
28+
if cell is None:
29+
notebook["cells"].insert(0, code_cell("", hidden=True))
30+
cell = notebook["cells"][0]
31+
32+
with tempfile.NamedTemporaryFile(
33+
mode="w+",
34+
delete=True,
35+
suffix=".py",
36+
dir=path.parent,
37+
encoding="utf-8",
38+
) as f:
39+
lockfile = Path(f"{f.name}.lock")
40+
41+
f.write(cell["source"].strip())
42+
f.flush()
43+
44+
if lockfile_contents:
45+
lockfile.write_text(lockfile_contents)
46+
47+
result = uv(["export", "--script", f.name], check=True)
48+
49+
sys.stdout.write(result.stdout.decode("utf-8"))
50+
51+
if lockfile.exists():
52+
notebook.metadata["uv.lock"] = lockfile.read_text(encoding="utf-8")
53+
write_ipynb(notebook, path)
54+
lockfile.unlink(missing_ok=True)

tests/test_juv.py

Lines changed: 54 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1176,26 +1176,24 @@ def test_tree(
11761176
""")
11771177

11781178

1179-
def test_tree_updates_lock(
1179+
def test_clear_lock(
11801180
tmp_path: pathlib.Path,
11811181
monkeypatch: pytest.MonkeyPatch,
11821182
) -> None:
11831183
monkeypatch.chdir(tmp_path)
11841184

11851185
invoke(["init", "test.ipynb"])
1186-
invoke(["lock", "test.ipynb"])
11871186
invoke(["add", "test.ipynb", "attrs"])
1188-
invoke(["tree", "test.ipynb"])
1189-
notebook = jupytext.read(tmp_path / "test.ipynb")
1190-
assert notebook.metadata["uv.lock"] == snapshot("""\
1187+
invoke(["lock", "test.ipynb"])
1188+
assert jupytext.read(tmp_path / "test.ipynb").metadata.get("uv.lock") == snapshot("""\
11911189
version = 1
11921190
requires-python = ">=3.13"
11931191
11941192
[options]
11951193
exclude-newer = "2023-02-01T02:00:00Z"
11961194
11971195
[manifest]
1198-
requirements = [{ name = "attrs", specifier = ">=22.2.0" }]
1196+
requirements = [{ name = "attrs" }]
11991197
12001198
[[package]]
12011199
name = "attrs"
@@ -1207,40 +1205,60 @@ def test_tree_updates_lock(
12071205
]
12081206
""")
12091207

1210-
notebook.cells[0] = new_code_cell("""# /// script
1211-
# dependencies = []
1212-
# requires-python = ">=3.8"
1213-
# ///
1214-
""")
1215-
write_ipynb(notebook, tmp_path / "test.ipynb")
1216-
invoke(["tree", "test.ipynb"])
1217-
assert jupytext.read(tmp_path / "test.ipynb").metadata["uv.lock"] == snapshot("""\
1218-
version = 1
1219-
requires-python = ">=3.8"
1208+
result = invoke(["lock", "test.ipynb", "--clear"])
1209+
assert result.exit_code == 0
1210+
assert result.stdout == snapshot("Cleared lockfile `test.ipynb`\n")
1211+
assert jupytext.read(tmp_path / "test.ipynb").metadata.get("uv.lock") is None
12201212

1221-
[options]
1222-
exclude-newer = "2023-02-01T02:00:00Z"
1223-
""")
1213+
1214+
def sanitize_uv_export_command(output: str) -> str:
1215+
"""Replace the temporary file path after 'uv export --script' with <TEMPFILE>"""
1216+
pattern = r"(uv export --script )([^\s]+[\\/][^\s]+\.py)"
1217+
replacement = r"\1<TEMPFILE>"
1218+
return re.sub(pattern, replacement, output)
12241219

12251220

1226-
def test_clear_lock(
1221+
def test_export(
12271222
tmp_path: pathlib.Path,
12281223
monkeypatch: pytest.MonkeyPatch,
12291224
) -> None:
12301225
monkeypatch.chdir(tmp_path)
12311226

12321227
invoke(["init", "test.ipynb"])
12331228
invoke(["add", "test.ipynb", "attrs"])
1229+
result = invoke(["export", "test.ipynb"])
1230+
assert result.exit_code == 0
1231+
assert sanitize_uv_export_command(result.stdout) == snapshot("""\
1232+
# This file was autogenerated by uv via the following command:
1233+
# uv export --script <TEMPFILE>
1234+
attrs==22.2.0 \\
1235+
--hash=sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836 \\
1236+
--hash=sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99
1237+
""")
1238+
1239+
1240+
@pytest.mark.parametrize("command", ["export", "tree"])
1241+
def test_commands_update_lock(
1242+
command: str,
1243+
tmp_path: pathlib.Path,
1244+
monkeypatch: pytest.MonkeyPatch,
1245+
) -> None:
1246+
monkeypatch.chdir(tmp_path)
1247+
1248+
invoke(["init", "test.ipynb"])
12341249
invoke(["lock", "test.ipynb"])
1235-
assert jupytext.read(tmp_path / "test.ipynb").metadata.get("uv.lock") == snapshot("""\
1250+
invoke(["add", "test.ipynb", "attrs"])
1251+
invoke([command, "test.ipynb"])
1252+
notebook = jupytext.read(tmp_path / "test.ipynb")
1253+
assert notebook.metadata["uv.lock"] == snapshot("""\
12361254
version = 1
12371255
requires-python = ">=3.13"
12381256
12391257
[options]
12401258
exclude-newer = "2023-02-01T02:00:00Z"
12411259
12421260
[manifest]
1243-
requirements = [{ name = "attrs" }]
1261+
requirements = [{ name = "attrs", specifier = ">=22.2.0" }]
12441262
12451263
[[package]]
12461264
name = "attrs"
@@ -1252,7 +1270,17 @@ def test_clear_lock(
12521270
]
12531271
""")
12541272

1255-
result = invoke(["lock", "test.ipynb", "--clear"])
1256-
assert result.exit_code == 0
1257-
assert result.stdout == snapshot("Cleared lockfile `test.ipynb`\n")
1258-
assert jupytext.read(tmp_path / "test.ipynb").metadata.get("uv.lock") is None
1273+
notebook.cells[0] = new_code_cell("""# /// script
1274+
# dependencies = []
1275+
# requires-python = ">=3.8"
1276+
# ///
1277+
""")
1278+
write_ipynb(notebook, tmp_path / "test.ipynb")
1279+
invoke([command, "test.ipynb"])
1280+
assert jupytext.read(tmp_path / "test.ipynb").metadata["uv.lock"] == snapshot("""\
1281+
version = 1
1282+
requires-python = ">=3.8"
1283+
1284+
[options]
1285+
exclude-newer = "2023-02-01T02:00:00Z"
1286+
""")

0 commit comments

Comments
 (0)