diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 85d64d06..7a558367 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -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 diff --git a/rsconnect/main.py b/rsconnect/main.py index 68d20324..85c87292 100644 --- a/rsconnect/main.py +++ b/rsconnect/main.py @@ -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 = ( + "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", "") + ] + 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", @@ -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) python_interpreter = which_python(python, base_dir) else: requirements_file = resolve_requirements_file(base_dir, requirements_file, force_generate) diff --git a/tests/test_main.py b/tests/test_main.py index fa345569..78c67f30 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -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" @@ -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"), [