From 75cc0b7b4f743aebca020f0f7333e82aa4463d1a Mon Sep 17 00:00:00 2001 From: Alessandro Molina Date: Thu, 18 Dec 2025 11:37:40 +0100 Subject: [PATCH 01/12] basic uv_export --- rsconnect/subprocesses/inspect_environment.py | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/rsconnect/subprocesses/inspect_environment.py b/rsconnect/subprocesses/inspect_environment.py index 92af5452..74be1862 100644 --- a/rsconnect/subprocesses/inspect_environment.py +++ b/rsconnect/subprocesses/inspect_environment.py @@ -13,6 +13,7 @@ import json import locale import os +import tempfile import re import subprocess import sys @@ -74,6 +75,8 @@ def detect_environment(dirname: str, requirements_file: Optional[str] = "require if requirements_file is None: result = pip_freeze() + elif os.path.basename(requirements_file) == "uv.lock": + result = uv_export(dirname, requirements_file) else: result = output_file(dirname, requirements_file, "pip") or pip_freeze() @@ -186,6 +189,55 @@ def pip_freeze(): } +def uv_export(dirname: str, lock_filename: str): + """ + Export requirements from a uv.lock file using `uv export`. + """ + lock_path = lock_filename + if not os.path.isabs(lock_filename): + lock_path = os.path.join(dirname, lock_filename) + + if not os.path.exists(lock_path): + raise EnvironmentException("uv.lock not found: %s" % lock_filename) + + with tempfile.NamedTemporaryFile(delete=True) as tmp_file: + try: + proc = subprocess.Popen( + ["uv", "export", "--format", "requirements-txt", "--locked", "--output", tmp_file.name], + cwd=os.path.dirname(lock_path), + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + ) + stdout, stderr = proc.communicate() + status = proc.returncode + except Exception as exception: + raise EnvironmentException("Error during uv export: %s" % str(exception)) + + if status != 0: + msg = stderr or ("exited with code %d" % status) + raise EnvironmentException("Error during uv export: %s" % msg) + + tmp_file.seek(0) + exported = tmp_file.read() + + requirements = filter_pip_freeze_output(exported) + requirements = ( + "# requirements.txt.lock generated from uv.lock by rsconnect-python on " + + str(datetime.datetime.now(datetime.timezone.utc)) + + "\n" + + requirements + ) + + return { + "filename": "requirements.txt.lock", + "contents": requirements, + "source": "uv_lock", + "package_manager": "uv", + "pip": get_version("pip"), + } + + def filter_pip_freeze_output(pip_stdout: str): # Filter out dependency on `rsconnect` and ignore output lines from pip which start with `[notice]` return "\n".join( From a65d151dabac7bdfefd7f07bb5fd163f9062fa2d Mon Sep 17 00:00:00 2001 From: Alessandro Molina Date: Thu, 18 Dec 2025 12:09:27 +0100 Subject: [PATCH 02/12] just set pip to none if it's not used --- rsconnect/subprocesses/inspect_environment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rsconnect/subprocesses/inspect_environment.py b/rsconnect/subprocesses/inspect_environment.py index 74be1862..47dfa9db 100644 --- a/rsconnect/subprocesses/inspect_environment.py +++ b/rsconnect/subprocesses/inspect_environment.py @@ -234,7 +234,7 @@ def uv_export(dirname: str, lock_filename: str): "contents": requirements, "source": "uv_lock", "package_manager": "uv", - "pip": get_version("pip"), + "pip": None, } From 0dc42f75d43443ab9b26152f3e4a7d95b0c2ff01 Mon Sep 17 00:00:00 2001 From: Alessandro Molina Date: Thu, 18 Dec 2025 12:53:01 +0100 Subject: [PATCH 03/12] Simplify uv export --- rsconnect/subprocesses/inspect_environment.py | 34 ++++++++++++------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/rsconnect/subprocesses/inspect_environment.py b/rsconnect/subprocesses/inspect_environment.py index 47dfa9db..e70c01e3 100644 --- a/rsconnect/subprocesses/inspect_environment.py +++ b/rsconnect/subprocesses/inspect_environment.py @@ -200,26 +200,34 @@ def uv_export(dirname: str, lock_filename: str): if not os.path.exists(lock_path): raise EnvironmentException("uv.lock not found: %s" % lock_filename) - with tempfile.NamedTemporaryFile(delete=True) as tmp_file: + with tempfile.NamedTemporaryFile(mode="w+", encoding="utf-8") as tmp_file: try: - proc = subprocess.Popen( - ["uv", "export", "--format", "requirements-txt", "--locked", "--output", tmp_file.name], + result = subprocess.run( + [ + "uv", + "export", + "--format", + "requirements-txt", + "--frozen", + "--no-hashes", + "--no-annotate", + "--offline", + "--output-file", + tmp_file.name, + ], cwd=os.path.dirname(lock_path), - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - universal_newlines=True, + stdout=sys.stderr, + stderr=sys.stderr, + check=False, ) - stdout, stderr = proc.communicate() - status = proc.returncode except Exception as exception: raise EnvironmentException("Error during uv export: %s" % str(exception)) - if status != 0: - msg = stderr or ("exited with code %d" % status) - raise EnvironmentException("Error during uv export: %s" % msg) + if result.returncode != 0: + raise EnvironmentException("Error during uv export: exited with code %d" % result.returncode) - tmp_file.seek(0) - exported = tmp_file.read() + with open(tmp_file.name) as output_file: + exported = output_file.read() requirements = filter_pip_freeze_output(exported) requirements = ( From 37ef483922ae76275f931c3d2bd4207ae3dcf2b4 Mon Sep 17 00:00:00 2001 From: Alessandro Molina Date: Thu, 18 Dec 2025 16:43:55 +0100 Subject: [PATCH 04/12] omit header --- rsconnect/subprocesses/inspect_environment.py | 1 + 1 file changed, 1 insertion(+) diff --git a/rsconnect/subprocesses/inspect_environment.py b/rsconnect/subprocesses/inspect_environment.py index e70c01e3..cb0f2430 100644 --- a/rsconnect/subprocesses/inspect_environment.py +++ b/rsconnect/subprocesses/inspect_environment.py @@ -212,6 +212,7 @@ def uv_export(dirname: str, lock_filename: str): "--no-hashes", "--no-annotate", "--offline", + "--no-header", "--output-file", tmp_file.name, ], From d1eebdf3945bde37fff61f5be5f0a6ef5e01ac8d Mon Sep 17 00:00:00 2001 From: Alessandro Molina Date: Thu, 18 Dec 2025 18:00:03 +0100 Subject: [PATCH 05/12] Add CHANGELOG --- docs/CHANGELOG.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index caecc6ed..a66cc122 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -10,15 +10,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - `rsconnect content get-lockfile` command allows fetching a lockfile with the -dependencies installed by connect to run the deployed content + dependencies installed by connect to run the deployed content - `rsconnect content venv` command recreates a local python environment -equal to the one used by connect to run the content. + equal to the one used by connect to run the content. - Added `--requirements-file` option on deploy and write-manifest commands to -supply an explicit requirements file instead of detecting the environment. + supply an explicit requirements file instead of detecting the environment. +- `uv.lock` can now be supplied via `--requirements-file` for deploy and write-manifest. - Bundle uploads now include git metadata (source, source_repo, source_branch, source_commit) when deploying from a git repository. This metadata is automatically detected and sent to Posit Connect 2025.12.0 or later. Use `--metadata key=value` to provide additional metadata or override detected values. Use `--no-metadata` to disable automatic detection. (#736) + supply an explicit requirements file instead of detecting the environment. + ## [1.28.2] - 2025-12-05 From 3f02bf869f78bfae1f9e18823fb4ea8282fce3fc Mon Sep 17 00:00:00 2001 From: Alessandro Molina Date: Thu, 18 Dec 2025 18:04:18 +0100 Subject: [PATCH 06/12] Implement test --- rsconnect/subprocesses/inspect_environment.py | 1 + tests/test_environment.py | 70 ++++++++++++++++++- 2 files changed, 70 insertions(+), 1 deletion(-) diff --git a/rsconnect/subprocesses/inspect_environment.py b/rsconnect/subprocesses/inspect_environment.py index cb0f2430..f65cf30f 100644 --- a/rsconnect/subprocesses/inspect_environment.py +++ b/rsconnect/subprocesses/inspect_environment.py @@ -213,6 +213,7 @@ def uv_export(dirname: str, lock_filename: str): "--no-annotate", "--offline", "--no-header", + "--no-emit-project", "--output-file", tmp_file.name, ], diff --git a/tests/test_environment.py b/tests/test_environment.py index 0495fc90..94da4e90 100644 --- a/tests/test_environment.py +++ b/tests/test_environment.py @@ -10,7 +10,12 @@ import rsconnect.environment from rsconnect.exception import RSConnectException from rsconnect.environment import Environment, which_python -from rsconnect.subprocesses.inspect_environment import get_python_version, get_default_locale, filter_pip_freeze_output +from rsconnect.subprocesses.inspect_environment import ( + detect_environment, + filter_pip_freeze_output, + get_default_locale, + get_python_version, +) from .utils import get_dir @@ -125,6 +130,69 @@ def test_filter_pip_freeze_output(self): self.assertEqual(filtered, expected) +def test_uv_lock_export(tmp_path): + project_dir = tmp_path / "project" + project_dir.mkdir() + (project_dir / "pyproject.toml").write_text( + "[project]\nname='demo'\nversion='0.0.0'\n" + "dependencies=['aiofiles==24.1.0','annotated-doc==0.0.4','annotated-types==0.7.0']\n", + encoding="utf-8", + ) + (project_dir / "uv.lock").write_text( + """version = 1 +revision = 3 +requires-python = ">=3.11.3" + +[[package]] +name = "aiofiles" +version = "24.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://example.com/aiofiles-24.1.0.tar.gz", hash = "sha256:1" } +wheels = [{ url = "https://example.com/aiofiles-24.1.0.whl", hash = "sha256:2" }] + +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://example.com/annotated_doc-0.0.4.tar.gz", hash = "sha256:3" } +wheels = [{ url = "https://example.com/annotated_doc-0.0.4.whl", hash = "sha256:4" }] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://example.com/annotated_types-0.7.0.tar.gz", hash = "sha256:5" } +wheels = [{ url = "https://example.com/annotated_types-0.7.0.whl", hash = "sha256:6" }] + +[[package]] +name = "demo" +version = "0.0.0" +source = { virtual = "." } +dependencies = [ + { name = "aiofiles" }, + { name = "annotated-doc" }, + { name = "annotated-types" }, +] + +[package.metadata] +requires-dist = [ + { name = "aiofiles", specifier = "==24.1.0" }, + { name = "annotated-doc", specifier = "==0.0.4" }, + { name = "annotated-types", specifier = "==0.7.0" }, +]""", + encoding="utf-8", + ) + + env = detect_environment(str(project_dir), requirements_file="uv.lock") + + assert env.filename == "requirements.txt.lock" + assert "aiofiles==24.1.0" in env.contents + assert "annotated-doc==0.0.4" in env.contents + assert "annotated-types==0.7.0" in env.contents + assert env.source == "uv_lock" + assert env.package_manager == "uv" + + class WhichPythonTestCase(TestCase): def test_default(self): self.assertEqual(which_python(), sys.executable) From 6757b0be6b9475286536e786bc444bd3c84b9716 Mon Sep 17 00:00:00 2001 From: Alessandro Molina Date: Thu, 18 Dec 2025 18:14:07 +0100 Subject: [PATCH 07/12] Improve help messages --- rsconnect/main.py | 23 +++++------------------ 1 file changed, 5 insertions(+), 18 deletions(-) diff --git a/rsconnect/main.py b/rsconnect/main.py index 7b96021c..d07260ea 100644 --- a/rsconnect/main.py +++ b/rsconnect/main.py @@ -1094,10 +1094,7 @@ def _warn_on_ignored_requirements(directory: str, requirements_file_name: str): "-r", type=click.Path(dir_okay=False), default="requirements.txt", - help=( - "Path to requirements file to record in the manifest instead of detecting the environment. " - "Must be inside the notebook directory. Use 'none' to capture via pip freeze." - ), + help=("Path to requirements file listing the project dependencies. " "Must be inside the notebook directory."), ) @click.option( "--package-installer", @@ -1273,10 +1270,7 @@ def deploy_notebook( "-r", type=click.Path(dir_okay=False), default="requirements.txt", - help=( - "Path to requirements file to record in the manifest instead of detecting the environment. " - "Must be inside the notebook directory. Use 'none' to capture via pip freeze." - ), + help=("Path to requirements file listing the project dependencies. " "Must be inside the notebook directory."), ) @click.option( "--package-installer", @@ -1528,10 +1522,7 @@ def deploy_manifest( "-r", type=click.Path(dir_okay=False), default="requirements.txt", - help=( - "Path to requirements file to record in the manifest instead of detecting the environment. " - "Must be inside the project directory. Use 'none' to capture via pip freeze." - ), + help=("Path to requirements file listing the project dependencies. " "Must be inside the project directory."), ) @click.option( "--package-installer", @@ -1957,8 +1948,7 @@ def generate_deploy_python(app_mode: AppMode, alias: str, min_version: str, desc "-r", type=click.Path(dir_okay=False), help=( - "Path to requirements file to record in the manifest instead of detecting the environment. " - "Must be inside the deployment directory. Use 'none' to capture via pip freeze." + "Path to requirements file listing the project dependencies. " "Must be inside the deployment directory." ), ) @click.option( @@ -2659,10 +2649,7 @@ def generate_write_manifest_python(app_mode: AppMode, alias: str, desc: Optional "--requirements-file", "-r", type=click.Path(dir_okay=False), - help=( - "Path to requirements file to record in the manifest instead of detecting the environment. " - "Must be inside the application directory. Use 'none' to capture via pip freeze." - ), + help="Path to requirements file listing the project dependencies. Must be inside the application directory.", ) @click.option( "--package-installer", From 869b342f3aa4e9ebd234dc08dab0ec04c002de1c Mon Sep 17 00:00:00 2001 From: Alessandro Molina Date: Thu, 18 Dec 2025 18:21:00 +0100 Subject: [PATCH 08/12] merge literal strings --- rsconnect/main.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/rsconnect/main.py b/rsconnect/main.py index d07260ea..a7ce01b0 100644 --- a/rsconnect/main.py +++ b/rsconnect/main.py @@ -1094,7 +1094,7 @@ def _warn_on_ignored_requirements(directory: str, requirements_file_name: str): "-r", type=click.Path(dir_okay=False), default="requirements.txt", - help=("Path to requirements file listing the project dependencies. " "Must be inside the notebook directory."), + help="Path to requirements file listing the project dependencies. Must be inside the notebook directory.", ) @click.option( "--package-installer", @@ -1270,7 +1270,7 @@ def deploy_notebook( "-r", type=click.Path(dir_okay=False), default="requirements.txt", - help=("Path to requirements file listing the project dependencies. " "Must be inside the notebook directory."), + help="Path to requirements file listing the project dependencies. Must be inside the notebook directory.", ) @click.option( "--package-installer", @@ -1522,7 +1522,7 @@ def deploy_manifest( "-r", type=click.Path(dir_okay=False), default="requirements.txt", - help=("Path to requirements file listing the project dependencies. " "Must be inside the project directory."), + help="Path to requirements file listing the project dependencies. Must be inside the project directory.", ) @click.option( "--package-installer", @@ -1947,9 +1947,7 @@ def generate_deploy_python(app_mode: AppMode, alias: str, min_version: str, desc "--requirements-file", "-r", type=click.Path(dir_okay=False), - help=( - "Path to requirements file listing the project dependencies. " "Must be inside the deployment directory." - ), + help="Path to requirements file listing the project dependencies. Must be inside the deployment directory.", ) @click.option( "--package-installer", @@ -2166,7 +2164,7 @@ def write_manifest(): type=click.Path(dir_okay=False), help=( "Path to requirements file to record in the manifest instead of detecting the environment. " - "Must be inside the notebook directory. Use 'none' to capture via pip freeze." + "Must be inside the notebook directory." ), ) @click.option( From ded155603b0a1583657b8df4d9cbed44ce0703d2 Mon Sep 17 00:00:00 2001 From: Alessandro Molina Date: Thu, 18 Dec 2025 18:23:00 +0100 Subject: [PATCH 09/12] Fix test after semplification --- tests/test_environment.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_environment.py b/tests/test_environment.py index 94da4e90..187e2a43 100644 --- a/tests/test_environment.py +++ b/tests/test_environment.py @@ -148,21 +148,21 @@ def test_uv_lock_export(tmp_path): version = "24.1.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://example.com/aiofiles-24.1.0.tar.gz", hash = "sha256:1" } -wheels = [{ url = "https://example.com/aiofiles-24.1.0.whl", hash = "sha256:2" }] +wheels = [{ url = "https://example.com/aiofiles-24.1.0-py3-none-any.whl", hash = "sha256:2" }] [[package]] name = "annotated-doc" version = "0.0.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://example.com/annotated_doc-0.0.4.tar.gz", hash = "sha256:3" } -wheels = [{ url = "https://example.com/annotated_doc-0.0.4.whl", hash = "sha256:4" }] +wheels = [{ url = "https://example.com/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:4" }] [[package]] name = "annotated-types" version = "0.7.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://example.com/annotated_types-0.7.0.tar.gz", hash = "sha256:5" } -wheels = [{ url = "https://example.com/annotated_types-0.7.0.whl", hash = "sha256:6" }] +wheels = [{ url = "https://example.com/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:6" }] [[package]] name = "demo" From 6452f94b41d0fb0b0e0a20f429d89ff9fa958405 Mon Sep 17 00:00:00 2001 From: Alessandro Molina Date: Thu, 18 Dec 2025 18:43:33 +0100 Subject: [PATCH 10/12] thanks windows... --- rsconnect/subprocesses/inspect_environment.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/rsconnect/subprocesses/inspect_environment.py b/rsconnect/subprocesses/inspect_environment.py index f65cf30f..7aa4b358 100644 --- a/rsconnect/subprocesses/inspect_environment.py +++ b/rsconnect/subprocesses/inspect_environment.py @@ -200,7 +200,8 @@ def uv_export(dirname: str, lock_filename: str): if not os.path.exists(lock_path): raise EnvironmentException("uv.lock not found: %s" % lock_filename) - with tempfile.NamedTemporaryFile(mode="w+", encoding="utf-8") as tmp_file: + with tempfile.TemporaryDirectory() as tmpdir: + output_path = os.path.join(tmpdir, "requirements.txt.lock") try: result = subprocess.run( [ @@ -215,7 +216,7 @@ def uv_export(dirname: str, lock_filename: str): "--no-header", "--no-emit-project", "--output-file", - tmp_file.name, + output_path, ], cwd=os.path.dirname(lock_path), stdout=sys.stderr, @@ -228,7 +229,7 @@ def uv_export(dirname: str, lock_filename: str): if result.returncode != 0: raise EnvironmentException("Error during uv export: exited with code %d" % result.returncode) - with open(tmp_file.name) as output_file: + with open(output_path) as output_file: exported = output_file.read() requirements = filter_pip_freeze_output(exported) From ff3dffc342673c6c3c4a94fe3d4c90a4549e8da5 Mon Sep 17 00:00:00 2001 From: Alessandro Molina Date: Thu, 18 Dec 2025 18:44:34 +0100 Subject: [PATCH 11/12] better to be explicit --- rsconnect/subprocesses/inspect_environment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rsconnect/subprocesses/inspect_environment.py b/rsconnect/subprocesses/inspect_environment.py index 7aa4b358..8fcd5fd3 100644 --- a/rsconnect/subprocesses/inspect_environment.py +++ b/rsconnect/subprocesses/inspect_environment.py @@ -229,7 +229,7 @@ def uv_export(dirname: str, lock_filename: str): if result.returncode != 0: raise EnvironmentException("Error during uv export: exited with code %d" % result.returncode) - with open(output_path) as output_file: + with open(output_path, mode="r", encoding="utf-8") as output_file: exported = output_file.read() requirements = filter_pip_freeze_output(exported) From a2bbf097874c067b4d29a8283274a337d0dba091 Mon Sep 17 00:00:00 2001 From: Alessandro Molina Date: Fri, 19 Dec 2025 16:55:14 +0100 Subject: [PATCH 12/12] Update help message --- rsconnect/main.py | 51 +++++++++++++++++++++++++++++++++++++---------- 1 file changed, 41 insertions(+), 10 deletions(-) diff --git a/rsconnect/main.py b/rsconnect/main.py index a7ce01b0..e939ab30 100644 --- a/rsconnect/main.py +++ b/rsconnect/main.py @@ -1094,7 +1094,12 @@ def _warn_on_ignored_requirements(directory: str, requirements_file_name: str): "-r", type=click.Path(dir_okay=False), default="requirements.txt", - help="Path to requirements file listing the project dependencies. Must be inside the notebook directory.", + help=( + "Path to requirements file listing the project dependencies. " + "Any file compatible with requirements.txt format or uv.lock is accepted, " + "a requirements.txt.lock retrieved with 'rsconnect content get-lockfile' is also supported. " + "Must be inside the project directory." + ), ) @click.option( "--package-installer", @@ -1270,7 +1275,12 @@ def deploy_notebook( "-r", type=click.Path(dir_okay=False), default="requirements.txt", - help="Path to requirements file listing the project dependencies. Must be inside the notebook directory.", + help=( + "Path to requirements file listing the project dependencies. " + "Any file compatible with requirements.txt format or uv.lock is accepted, " + "a requirements.txt.lock retrieved with 'rsconnect content get-lockfile' is also supported. " + "Must be inside the project directory." + ), ) @click.option( "--package-installer", @@ -1522,7 +1532,12 @@ def deploy_manifest( "-r", type=click.Path(dir_okay=False), default="requirements.txt", - help="Path to requirements file listing the project dependencies. Must be inside the project directory.", + help=( + "Path to requirements file listing the project dependencies. " + "Any file compatible with requirements.txt format or uv.lock is accepted, " + "a requirements.txt.lock retrieved with 'rsconnect content get-lockfile' is also supported. " + "Must be inside the project directory." + ), ) @click.option( "--package-installer", @@ -1947,7 +1962,12 @@ def generate_deploy_python(app_mode: AppMode, alias: str, min_version: str, desc "--requirements-file", "-r", type=click.Path(dir_okay=False), - help="Path to requirements file listing the project dependencies. Must be inside the deployment directory.", + help=( + "Path to requirements file listing the project dependencies. " + "Any file compatible with requirements.txt format or uv.lock is accepted, " + "a requirements.txt.lock retrieved with 'rsconnect content get-lockfile' is also supported. " + "Must be inside the project directory." + ), ) @click.option( "--package-installer", @@ -2163,8 +2183,10 @@ def write_manifest(): "-r", type=click.Path(dir_okay=False), help=( - "Path to requirements file to record in the manifest instead of detecting the environment. " - "Must be inside the notebook directory." + "Path to requirements file listing the project dependencies. " + "Any file compatible with requirements.txt format or uv.lock is accepted, " + "a requirements.txt.lock retrieved with 'rsconnect content get-lockfile' is also supported. " + "Must be inside the project directory." ), ) @click.option( @@ -2280,8 +2302,10 @@ def write_manifest_notebook( "-r", type=click.Path(exists=True, dir_okay=False), help=( - "Path to requirements file to record in the manifest instead of detecting the environment. " - "Must be inside the notebook directory. Use 'none' to capture via pip freeze." + "Path to requirements file listing the project dependencies. " + "Any file compatible with requirements.txt format or uv.lock is accepted, " + "a requirements.txt.lock retrieved with 'rsconnect content get-lockfile' is also supported. " + "Must be inside the project directory." ), ) @click.option( @@ -2433,7 +2457,9 @@ def write_manifest_voila( "-r", type=click.Path(dir_okay=False), help=( - "Path to requirements file to record in the manifest instead of detecting the environment. " + "Path to requirements file listing the project dependencies. " + "Any file compatible with requirements.txt format or uv.lock is accepted, " + "a requirements.txt.lock retrieved with 'rsconnect content get-lockfile' is also supported. " "Must be inside the project directory." ), ) @@ -2647,7 +2673,12 @@ def generate_write_manifest_python(app_mode: AppMode, alias: str, desc: Optional "--requirements-file", "-r", type=click.Path(dir_okay=False), - help="Path to requirements file listing the project dependencies. Must be inside the application directory.", + help=( + "Path to requirements file listing the project dependencies. " + "Any file compatible with requirements.txt format or uv.lock is accepted, " + "a requirements.txt.lock retrieved with 'rsconnect content get-lockfile' is also supported. " + "Must be inside the project directory." + ), ) @click.option( "--package-installer",