Skip to content

Commit 8068f06

Browse files
authored
fix: report static notebook render failures cleanly (#908)
1 parent 35d5588 commit 8068f06

2 files changed

Lines changed: 59 additions & 2 deletions

File tree

‎rsconnect/bundle.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -824,8 +824,8 @@ def make_notebook_html_bundle(
824824
cmd.append("--TagRemovePreprocessor.remove_input_tags=['hide_input']")
825825
try:
826826
output = check_output(cmd)
827-
except subprocess.CalledProcessError:
828-
raise
827+
except subprocess.CalledProcessError as error:
828+
raise RSConnectException(f"Rendering {filename} with nbconvert failed (see output above)") from error
829829

830830
nb_name = basename(filename)
831831
filename = splitext(nb_name)[0] + ".html"

‎tests/test_main.py‎

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import re
55
import shutil
66
import sys
7+
import venv
78
from os.path import join
89
from types import SimpleNamespace
910
from unittest import TestCase, mock
@@ -352,6 +353,62 @@ def test_deploy_notebook_static_passes_interpreter_not_version(self):
352353
assert bundle_args[0] is make_notebook_html_bundle
353354
assert bundle_args[2] == sys.executable
354355

356+
@pytest.mark.parametrize(
357+
("contents", "diagnostic", "missing_nbconvert"),
358+
[
359+
("not json", "NotJSONError", False),
360+
("not json", "No module named nbconvert", True),
361+
(
362+
json.dumps(
363+
{
364+
"cells": [
365+
{
366+
"cell_type": "code",
367+
"execution_count": None,
368+
"metadata": {},
369+
"outputs": [],
370+
"source": ["raise ValueError('boom')"],
371+
}
372+
],
373+
"metadata": {"kernelspec": {"name": "python3", "display_name": "Python 3"}},
374+
"nbformat": 4,
375+
"nbformat_minor": 5,
376+
}
377+
),
378+
"ValueError: boom",
379+
False,
380+
),
381+
],
382+
)
383+
def test_deploy_static_notebook_render_failure(self, tmp_path, capfd, contents, diagnostic, missing_nbconvert):
384+
notebook = tmp_path / "broken.ipynb"
385+
notebook.write_text(contents)
386+
interpreter = sys.executable
387+
if missing_nbconvert:
388+
env_dir = tmp_path / "empty-env"
389+
venv.EnvBuilder(with_pip=False, symlinks=os.name != "nt").create(env_dir)
390+
interpreter = str(env_dir / ("Scripts/python.exe" if os.name == "nt" else "bin/python"))
391+
fake_env = SimpleNamespace(python="3.12.1", python_interpreter=interpreter)
392+
393+
with mock.patch("rsconnect.main.Environment.create_python_environment", return_value=fake_env), mock.patch(
394+
"rsconnect.main.RSConnectExecutor"
395+
) as mock_executor:
396+
mock_executor.return_value.make_bundle.side_effect = lambda func, *args: func(*args)
397+
args = apply_common_args(
398+
["deploy", "notebook", str(notebook), "--static", "--no-verify"],
399+
server="http://fake_server",
400+
key="FAKE_API_KEY",
401+
)
402+
result = CliRunner().invoke(cli, args)
403+
404+
stderr = capfd.readouterr().err
405+
assert result.exit_code == 1
406+
assert diagnostic in re.sub(r"\x1b\[[0-9;]*m", "", stderr)
407+
assert f"Error: Rendering {notebook} with nbconvert failed (see output above)" in result.output
408+
assert "Traceback (most recent call last)" not in result.output
409+
assert "Internal error" not in result.output + stderr
410+
assert "subprocess.CalledProcessError" not in result.output + stderr
411+
355412
@httpretty.activate(verbose=True, allow_net_connect=False)
356413
def test_deploy_verify_before_activate(self, caplog):
357414
# Without --draft or --no-verify, the default flow deploys the bundle as a

0 commit comments

Comments
 (0)