Skip to content
Open
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
5 changes: 5 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased] - ??

### Added

- Introduced `--package-manager=uv|pip` option to enforce a specific package manager
to be used when deploying content. When omitted, the server will decide.

## [1.27.1] - 2025-08-12

### Fixed
Expand Down
62 changes: 49 additions & 13 deletions rsconnect/bundle.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,11 @@ class ManifestDataPythonPackageManager(TypedDict):
name: str
version: str
package_file: str
# When set, hints server how to perform installs.
# If True, server may perform installs using `uv`.
# If False, server should not use `uv`.
# If omitted, behavior is server-driven (migration default).
allow_uv: NotRequired[bool]


class ManifestData(TypedDict):
Expand All @@ -137,7 +142,6 @@ class ManifestData(TypedDict):
class Manifest:
def __init__(
self,
*,
version: Optional[int] = None,
environment: Optional[Environment] = None,
app_mode: Optional[AppMode] = None,
Expand Down Expand Up @@ -185,15 +189,19 @@ def __init__(
self.data["metadata"]["content_category"] = "site"

if environment:
package_manager = environment.package_manager
self.data["python"] = {
"version": environment.python,
"package_manager": {
"name": package_manager,
"version": getattr(environment, package_manager),
"package_file": environment.filename,
},
pm_name = environment.package_manager
pm_version_value = getattr(environment, pm_name, None)
if pm_version_value is None:
# Fallback: use pip version if available; otherwise empty string
pm_version_value = getattr(environment, "pip", "")
pm: ManifestDataPythonPackageManager = {
"name": pm_name,
"version": pm_version_value,
"package_file": environment.filename,
}
if getattr(environment, "package_manager_allow_uv", None) is not None:
pm["allow_uv"] = typing.cast(bool, environment.package_manager_allow_uv)
self.data["python"] = {"version": environment.python, "package_manager": pm}

if environment.python_version_requirement:
# If the environment has a python version requirement,
Expand Down Expand Up @@ -585,7 +593,13 @@ def make_notebook_source_bundle(
nb_name = basename(file)

manifest = make_source_manifest(
AppModes.JUPYTER_NOTEBOOK, environment, nb_name, None, image, env_management_py, env_management_r
AppModes.JUPYTER_NOTEBOOK,
environment,
nb_name,
None,
image,
env_management_py,
env_management_r,
)
if hide_all_input:
if "jupyter" not in manifest:
Expand Down Expand Up @@ -884,7 +898,13 @@ def make_api_manifest(

relevant_files = create_file_list(directory, extra_files, excludes)
manifest = make_source_manifest(
app_mode, environment, entry_point, None, image, env_management_py, env_management_r
app_mode,
environment,
entry_point,
None,
image,
env_management_py,
env_management_r,
)

manifest_add_buffer(manifest, environment.filename, environment.contents)
Expand Down Expand Up @@ -1657,7 +1677,13 @@ def write_notebook_manifest_json(
raise RSConnectException('Could not determine the app mode from "%s"; please specify one.' % extension)

manifest_data = make_source_manifest(
app_mode, environment, file_name, None, image, env_management_py, env_management_r
app_mode,
environment,
file_name,
None,
image,
env_management_py,
env_management_r,
)
if hide_all_input or hide_tagged_input:
if "jupyter" not in manifest_data:
Expand Down Expand Up @@ -1913,7 +1939,15 @@ def write_api_manifest_json(
"""
extra_files = validate_extra_files(directory, extra_files)
manifest, _ = make_api_manifest(
directory, entry_point, app_mode, environment, extra_files, excludes, image, env_management_py, env_management_r
directory,
entry_point,
app_mode,
environment,
extra_files,
excludes,
image,
env_management_py,
env_management_r,
)
manifest_path = join(directory, "manifest.json")

Expand Down Expand Up @@ -1998,6 +2032,8 @@ def write_quarto_manifest_json(
extra_files,
excludes,
image,
env_management_py,
env_management_r,
)

base_dir = file_or_directory
Expand Down
14 changes: 13 additions & 1 deletion rsconnect/environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ def __init__(
# Fields that are not loaded from the environment subprocess
self.python_version_requirement = python_version_requirement
self.python_interpreter = python_interpreter
# Optional override of server install behavior. If None, server-driven
# default is used.
self.package_manager_allow_uv: typing.Optional[bool] = None

def __getattr__(self, name: str) -> typing.Any:
# We directly proxy the attributes of the EnvironmentData object
Expand All @@ -56,7 +59,7 @@ def __getattr__(self, name: str) -> typing.Any:
def __setattr__(self, name: str, value: typing.Any) -> None:
if name in self.DATA_FIELDS:
# proxy the attribute to the underlying EnvironmentData object
self._data._replace(**{name: value})
self._data = self._data._replace(**{name: value})
else:
super().__setattr__(name, value)

Expand Down Expand Up @@ -101,6 +104,7 @@ def create_python_environment(
python: typing.Optional[str] = None,
override_python_version: typing.Optional[str] = None,
app_file: typing.Optional[str] = None,
package_manager: typing.Optional[str] = None,
) -> "Environment":
"""Given a project directory and a Python executable, return Environment information.

Expand Down Expand Up @@ -153,6 +157,14 @@ def create_python_environment(
# that didn't support environment.python.requires
environment.python = override_python_version

if package_manager is not None:
if package_manager not in ("pip", "uv"):
raise RSConnectException("Unsupported package manager: %s" % package_manager)
# Override the package manager name recorded by inspector
environment.package_manager = package_manager # type: ignore[attr-defined]
# Derive allow_uv from selection
environment.package_manager_allow_uv = True if package_manager == "uv" else False

if force_generate:
_warn_on_ignored_requirements(directory, environment.filename)

Expand Down
Loading
Loading