Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
only the rendered HTML, so the selected Python environment only needs `nbconvert`,
`ipykernel`, and the packages the notebook imports. That environment no longer
needs rsconnect installed. (#923)
- `deploy notebook --static` warns about options that have no effect on the
rendered HTML, such as `--requirements-file`, `--force-generate`, `--image`, and
the environment management options, and about extra files, which aren't included.
- `--override-python-version` with a major and minor version, such as `3.11`,
requests the latest Python patch release in that series instead of `3.11.0`.
This requires Posit Connect 2025.03.0 or later; older servers use the local
Expand Down
36 changes: 36 additions & 0 deletions rsconnect/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2108,6 +2108,41 @@ def _warn_on_ignored_requirements(directory: str, requirements_file_name: str):
warn_user(" Warning: the existing %s file will not be used or considered." % requirements_file_name)


# deploy notebook parameters that only affect a source bundle's Python or R environment.
# A static notebook bundle contains only the rendered HTML, so they have no effect.
_STATIC_NOTEBOOK_IGNORED_PARAMS = (
Comment thread
jmwoliver marked this conversation as resolved.
Resolved
"requirements_file",
"force_generate",
"package_installer",
"override_python_version",
"image",
"disable_env_management",
"env_management_py",
"env_management_r",
"exclude_renv",
)


def _warn_on_options_ignored_by_static_notebook(ctx: click.Context, extra_files: tuple[str, ...]) -> None:
"""Warn about deploy notebook options and extra files that --static ignores."""
ignored_options = [
next(opt for opt in param.opts if opt.startswith("--"))
for param in ctx.command.params
if param.name in _STATIC_NOTEBOOK_IGNORED_PARAMS
and validation.get_parameter_source_name_from_ctx(param.name, ctx) not in ("DEFAULT", "<source unknown>")
]
if ignored_options:
warn_user(
" Warning: --static deploys only the rendered HTML, so these options have no effect: %s"
% ", ".join(ignored_options)
)
if extra_files:
warn_user(
" Warning: --static deploys only the rendered HTML, so these extra files are not included: %s"
% ", ".join(extra_files)
)


# noinspection SpellCheckingInspection,DuplicatedCode
@deploy.command(
name="notebook",
Expand Down Expand Up @@ -2240,6 +2275,7 @@ def deploy_notebook(
if static:
# The notebook is rendered locally and the bundle holds only the HTML, so Connect
# never builds an environment for it. Only an interpreter to run nbconvert is needed.
_warn_on_options_ignored_by_static_notebook(ctx, extra_files)
Comment thread
jmwoliver marked this conversation as resolved.
Resolved
python_interpreter = which_python(python, base_dir)
else:
requirements_file = resolve_requirements_file(base_dir, requirements_file, force_generate)
Expand Down
76 changes: 76 additions & 0 deletions tests/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,7 @@ def test_deploy_notebook_static_needs_no_environment(self, tmp_path, extra_files
assert bundle_args[0] is make_notebook_html_bundle
assert bundle_args[2] == sys.executable
assert sorted(p.name for p in tmp_path.iterdir()) == sorted(["nb.ipynb", *extra_files])
assert "--static deploys only the rendered HTML" not in result.output

def test_deploy_notebook_static_renders_with_python_option(self, tmp_path):
notebook = tmp_path / "nb.ipynb"
Expand Down Expand Up @@ -421,6 +422,81 @@ def test_deploy_notebook_static_renders_with_notebook_project_venv(self, tmp_pat
assert bundle_args[0] is make_notebook_html_bundle
assert bundle_args[2] == str(python)

@pytest.mark.parametrize(
("options", "ignored"),
[
(["--requirements-file", "requirements.txt"], ["--requirements-file"]),
(["--force-generate"], ["--force-generate"]),
(["--package-installer", "uv"], ["--package-installer"]),
(["--override-python-version", "3.12"], ["--override-python-version"]),
(["--image", "rstudio/content-base"], ["--image"]),
(["--disable-env-management"], ["--disable-env-management"]),
(["--disable-env-management-py"], ["--disable-env-management-py"]),
(["--disable-env-management-r"], ["--disable-env-management-r"]),
(["--exclude-renv"], ["--exclude-renv"]),
(["-r", "requirements.txt", "-g"], ["--requirements-file", "--force-generate"]),
],
)
def test_deploy_notebook_static_warns_on_ignored_options(self, tmp_path, options, ignored):
notebook = tmp_path / "nb.ipynb"
shutil.copy(get_dir(join("pip1", "dummy.ipynb")), notebook)

with mock.patch("rsconnect.main.RSConnectExecutor"):
args = apply_common_args(
["deploy", "notebook", str(notebook), "--static", "--no-verify", *options],
server="http://fake_server",
key="FAKE_API_KEY",
)
result = CliRunner().invoke(cli, args)

assert result.exit_code == 0, result.output
prefix = "Warning: --static deploys only the rendered HTML, so these options have no effect: "
[warning] = [line for line in result.output.splitlines() if prefix in line]
# The warning lists options in declaration order, which isn't part of the contract.
assert sorted(warning.split(prefix, 1)[1].split(", ")) == sorted(ignored)
assert "extra files" not in result.output

def test_deploy_notebook_without_static_does_not_warn_on_static_ignored_options(self, tmp_path):
notebook = tmp_path / "nb.ipynb"
shutil.copy(get_dir(join("pip1", "dummy.ipynb")), notebook)
data_file = tmp_path / "data.csv"
data_file.write_text("")

with mock.patch("rsconnect.main.Environment.create_python_environment"), mock.patch(
"rsconnect.main.RSConnectExecutor"
):
args = apply_common_args(
["deploy", "notebook", str(notebook), str(data_file), "--no-verify", "-r", "requirements.txt", "-g"],
server="http://fake_server",
key="FAKE_API_KEY",
)
result = CliRunner().invoke(cli, args)

assert result.exit_code == 0, result.output
assert "Warning: --static deploys only the rendered HTML" not in result.output

def test_deploy_notebook_static_warns_on_extra_files(self, tmp_path):
notebook = tmp_path / "nb.ipynb"
shutil.copy(get_dir(join("pip1", "dummy.ipynb")), notebook)
data_files = [tmp_path / "data.csv", tmp_path / "helpers.py"]
for data_file in data_files:
data_file.write_text("")

with mock.patch("rsconnect.main.RSConnectExecutor"):
args = apply_common_args(
["deploy", "notebook", str(notebook), *map(str, data_files), "--static", "--no-verify"],
server="http://fake_server",
key="FAKE_API_KEY",
)
result = CliRunner().invoke(cli, args)

assert result.exit_code == 0, result.output
assert (
"Warning: --static deploys only the rendered HTML, so these extra files are not included: "
f"{data_files[0]}, {data_files[1]}\n" in result.output
)
assert "have no effect" not in result.output

@pytest.mark.parametrize(
("contents", "diagnostic", "missing_nbconvert"),
[
Expand Down
Loading