Accept multiple packages in uv sync (#16543)

## Summary

Closes https://github.com/astral-sh/uv/issues/12130.

---------

Co-authored-by: Zanie Blue <contact@zanie.dev>
Co-authored-by: konsti <konstin@mailbox.org>
This commit is contained in:
Charlie Marsh 2025-11-04 09:17:58 -05:00 committed by GitHub
parent 60a811e715
commit 9a6eafc043
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 265 additions and 86 deletions

View file

@ -3669,14 +3669,14 @@ pub struct SyncArgs {
#[arg(long, conflicts_with = "package")] #[arg(long, conflicts_with = "package")]
pub all_packages: bool, pub all_packages: bool,
/// Sync for a specific package in the workspace. /// Sync for specific packages in the workspace.
/// ///
/// The workspace's environment (`.venv`) is updated to reflect the subset of dependencies /// The workspace's environment (`.venv`) is updated to reflect the subset of dependencies
/// declared by the specified workspace member package. /// declared by the specified workspace member packages.
/// ///
/// If the workspace member does not exist, uv will exit with an error. /// If any workspace member does not exist, uv will exit with an error.
#[arg(long, conflicts_with = "all_packages")] #[arg(long, conflicts_with = "all_packages")]
pub package: Option<PackageName>, pub package: Vec<PackageName>,
/// Sync the environment for a Python script, rather than the current project. /// Sync the environment for a Python script, rather than the current project.
/// ///

View file

@ -26,6 +26,12 @@ pub(crate) enum InstallTarget<'lock> {
name: &'lock PackageName, name: &'lock PackageName,
lock: &'lock Lock, lock: &'lock Lock,
}, },
/// Multiple specific projects in a workspace.
Projects {
workspace: &'lock Workspace,
names: &'lock [PackageName],
lock: &'lock Lock,
},
/// An entire workspace. /// An entire workspace.
Workspace { Workspace {
workspace: &'lock Workspace, workspace: &'lock Workspace,
@ -47,6 +53,7 @@ impl<'lock> Installable<'lock> for InstallTarget<'lock> {
fn install_path(&self) -> &'lock Path { fn install_path(&self) -> &'lock Path {
match self { match self {
Self::Project { workspace, .. } => workspace.install_path(), Self::Project { workspace, .. } => workspace.install_path(),
Self::Projects { workspace, .. } => workspace.install_path(),
Self::Workspace { workspace, .. } => workspace.install_path(), Self::Workspace { workspace, .. } => workspace.install_path(),
Self::NonProjectWorkspace { workspace, .. } => workspace.install_path(), Self::NonProjectWorkspace { workspace, .. } => workspace.install_path(),
Self::Script { script, .. } => script.path.parent().unwrap(), Self::Script { script, .. } => script.path.parent().unwrap(),
@ -56,36 +63,38 @@ impl<'lock> Installable<'lock> for InstallTarget<'lock> {
fn lock(&self) -> &'lock Lock { fn lock(&self) -> &'lock Lock {
match self { match self {
Self::Project { lock, .. } => lock, Self::Project { lock, .. } => lock,
Self::Projects { lock, .. } => lock,
Self::Workspace { lock, .. } => lock, Self::Workspace { lock, .. } => lock,
Self::NonProjectWorkspace { lock, .. } => lock, Self::NonProjectWorkspace { lock, .. } => lock,
Self::Script { lock, .. } => lock, Self::Script { lock, .. } => lock,
} }
} }
fn roots(&self) -> impl Iterator<Item = &PackageName> { #[allow(refining_impl_trait)]
fn roots(&self) -> Box<dyn Iterator<Item = &PackageName> + '_> {
match self { match self {
Self::Project { name, .. } => Either::Left(Either::Left(std::iter::once(*name))), Self::Project { name, .. } => Box::new(std::iter::once(*name)),
Self::NonProjectWorkspace { lock, .. } => { Self::Projects { names, .. } => Box::new(names.iter()),
Either::Left(Either::Right(lock.members().iter())) Self::NonProjectWorkspace { lock, .. } => Box::new(lock.members().iter()),
}
Self::Workspace { lock, .. } => { Self::Workspace { lock, .. } => {
// Identify the workspace members. // Identify the workspace members.
// //
// The members are encoded directly in the lockfile, unless the workspace contains a // The members are encoded directly in the lockfile, unless the workspace contains a
// single member at the root, in which case, we identify it by its source. // single member at the root, in which case, we identify it by its source.
if lock.members().is_empty() { if lock.members().is_empty() {
Either::Right(Either::Left(lock.root().into_iter().map(Package::name))) Box::new(lock.root().into_iter().map(Package::name))
} else { } else {
Either::Left(Either::Right(lock.members().iter())) Box::new(lock.members().iter())
} }
} }
Self::Script { .. } => Either::Right(Either::Right(std::iter::empty())), Self::Script { .. } => Box::new(std::iter::empty()),
} }
} }
fn project_name(&self) -> Option<&PackageName> { fn project_name(&self) -> Option<&PackageName> {
match self { match self {
Self::Project { name, .. } => Some(name), Self::Project { name, .. } => Some(name),
Self::Projects { .. } => None,
Self::Workspace { .. } => None, Self::Workspace { .. } => None,
Self::NonProjectWorkspace { .. } => None, Self::NonProjectWorkspace { .. } => None,
Self::Script { .. } => None, Self::Script { .. } => None,
@ -98,6 +107,7 @@ impl<'lock> InstallTarget<'lock> {
pub(crate) fn indexes(self) -> impl Iterator<Item = &'lock Index> { pub(crate) fn indexes(self) -> impl Iterator<Item = &'lock Index> {
match self { match self {
Self::Project { workspace, .. } Self::Project { workspace, .. }
| Self::Projects { workspace, .. }
| Self::Workspace { workspace, .. } | Self::Workspace { workspace, .. }
| Self::NonProjectWorkspace { workspace, .. } => { | Self::NonProjectWorkspace { workspace, .. } => {
Either::Left(workspace.indexes().iter().chain( Either::Left(workspace.indexes().iter().chain(
@ -130,6 +140,7 @@ impl<'lock> InstallTarget<'lock> {
pub(crate) fn sources(&self) -> impl Iterator<Item = &Source> { pub(crate) fn sources(&self) -> impl Iterator<Item = &Source> {
match self { match self {
Self::Project { workspace, .. } Self::Project { workspace, .. }
| Self::Projects { workspace, .. }
| Self::Workspace { workspace, .. } | Self::Workspace { workspace, .. }
| Self::NonProjectWorkspace { workspace, .. } => { | Self::NonProjectWorkspace { workspace, .. } => {
Either::Left(workspace.sources().values().flat_map(Sources::iter).chain( Either::Left(workspace.sources().values().flat_map(Sources::iter).chain(
@ -158,6 +169,7 @@ impl<'lock> InstallTarget<'lock> {
) -> impl Iterator<Item = Cow<'lock, uv_pep508::Requirement<VerbatimParsedUrl>>> { ) -> impl Iterator<Item = Cow<'lock, uv_pep508::Requirement<VerbatimParsedUrl>>> {
match self { match self {
Self::Project { workspace, .. } Self::Project { workspace, .. }
| Self::Projects { workspace, .. }
| Self::Workspace { workspace, .. } | Self::Workspace { workspace, .. }
| Self::NonProjectWorkspace { workspace, .. } => { | Self::NonProjectWorkspace { workspace, .. } => {
Either::Left( Either::Left(
@ -256,6 +268,7 @@ impl<'lock> InstallTarget<'lock> {
} }
match self { match self {
Self::Project { lock, .. } Self::Project { lock, .. }
| Self::Projects { lock, .. }
| Self::Workspace { lock, .. } | Self::Workspace { lock, .. }
| Self::NonProjectWorkspace { lock, .. } => { | Self::NonProjectWorkspace { lock, .. } => {
if !lock.supports_provides_extra() { if !lock.supports_provides_extra() {
@ -281,7 +294,10 @@ impl<'lock> InstallTarget<'lock> {
Self::Project { .. } => { Self::Project { .. } => {
Err(ProjectError::MissingExtraProject(extra.clone())) Err(ProjectError::MissingExtraProject(extra.clone()))
} }
_ => Err(ProjectError::MissingExtraWorkspace(extra.clone())), Self::Projects { .. } => {
Err(ProjectError::MissingExtraProjects(extra.clone()))
}
_ => Err(ProjectError::MissingExtraProjects(extra.clone())),
}; };
} }
} }
@ -337,11 +353,11 @@ impl<'lock> InstallTarget<'lock> {
for group in groups.explicit_names() { for group in groups.explicit_names() {
if !known_groups.contains(group) { if !known_groups.contains(group) {
return Err(ProjectError::MissingGroupWorkspace(group.clone())); return Err(ProjectError::MissingGroupProjects(group.clone()));
} }
} }
} }
Self::Project { lock, .. } => { Self::Project { lock, .. } | Self::Projects { lock, .. } => {
let roots = self.roots().collect::<FxHashSet<_>>(); let roots = self.roots().collect::<FxHashSet<_>>();
let member_packages: Vec<&Package> = lock let member_packages: Vec<&Package> = lock
.packages() .packages()
@ -349,7 +365,7 @@ impl<'lock> InstallTarget<'lock> {
.filter(|package| roots.contains(package.name())) .filter(|package| roots.contains(package.name()))
.collect(); .collect();
// Extract the dependency groups defined in the relevant member. // Extract the dependency groups defined in the relevant member(s).
let known_groups = member_packages let known_groups = member_packages
.iter() .iter()
.flat_map(|package| package.dependency_groups().keys()) .flat_map(|package| package.dependency_groups().keys())
@ -357,7 +373,15 @@ impl<'lock> InstallTarget<'lock> {
for group in groups.explicit_names() { for group in groups.explicit_names() {
if !known_groups.contains(group) { if !known_groups.contains(group) {
return Err(ProjectError::MissingGroupProject(group.clone())); return match self {
Self::Project { .. } => {
Err(ProjectError::MissingGroupProject(group.clone()))
}
Self::Projects { .. } => {
Err(ProjectError::MissingGroupProjects(group.clone()))
}
_ => unreachable!(),
};
} }
} }
} }
@ -380,59 +404,71 @@ impl<'lock> InstallTarget<'lock> {
groups: &DependencyGroupsWithDefaults, groups: &DependencyGroupsWithDefaults,
) -> BTreeSet<&PackageName> { ) -> BTreeSet<&PackageName> {
match self { match self {
Self::Project { name, lock, .. } => { Self::Project { lock, .. } | Self::Projects { lock, .. } => {
// Collect the packages by name for efficient lookup let roots = self.roots().collect::<FxHashSet<_>>();
// Collect the packages by name for efficient lookup.
let packages = lock let packages = lock
.packages() .packages()
.iter() .iter()
.map(|p| (p.name(), p)) .map(|package| (package.name(), package))
.collect::<BTreeMap<_, _>>(); .collect::<BTreeMap<_, _>>();
// We'll include the project itself // We'll include all specified projects
let mut required_members = BTreeSet::new(); let mut required_members = BTreeSet::new();
required_members.insert(*name); for name in &roots {
required_members.insert(*name);
}
// Find all workspace member dependencies recursively // Find all workspace member dependencies recursively for all specified packages
let mut queue: VecDeque<(&PackageName, Option<&ExtraName>)> = VecDeque::new(); let mut queue: VecDeque<(&PackageName, Option<&ExtraName>)> = VecDeque::new();
let mut seen: FxHashSet<(&PackageName, Option<&ExtraName>)> = FxHashSet::default(); let mut seen: FxHashSet<(&PackageName, Option<&ExtraName>)> = FxHashSet::default();
let Some(root_package) = packages.get(name) else { for name in roots {
return required_members; let Some(root_package) = packages.get(name) else {
};
if groups.prod() {
// Add the root package
queue.push_back((name, None));
seen.insert((name, None));
// Add explicitly activated extras for the root package
for extra in extras.extra_names(root_package.optional_dependencies().keys()) {
if seen.insert((name, Some(extra))) {
queue.push_back((name, Some(extra)));
}
}
}
// Add activated dependency groups for the root package
for (group_name, dependencies) in root_package.resolved_dependency_groups() {
if !groups.contains(group_name) {
continue; continue;
};
if groups.prod() {
// Add the root package
if seen.insert((name, None)) {
queue.push_back((name, None));
}
// Add explicitly activated extras for the root package
for extra in extras.extra_names(root_package.optional_dependencies().keys())
{
if seen.insert((name, Some(extra))) {
queue.push_back((name, Some(extra)));
}
}
} }
for dependency in dependencies {
let name = dependency.package_name(); // Add activated dependency groups for the root package
queue.push_back((name, None)); for (group_name, dependencies) in root_package.resolved_dependency_groups() {
for extra in dependency.extra() { if !groups.contains(group_name) {
queue.push_back((name, Some(extra))); continue;
}
for dependency in dependencies {
let dep_name = dependency.package_name();
if seen.insert((dep_name, None)) {
queue.push_back((dep_name, None));
}
for extra in dependency.extra() {
if seen.insert((dep_name, Some(extra))) {
queue.push_back((dep_name, Some(extra)));
}
}
} }
} }
} }
while let Some((pkg_name, extra)) = queue.pop_front() { while let Some((package_name, extra)) = queue.pop_front() {
if lock.members().contains(pkg_name) { if lock.members().contains(package_name) {
required_members.insert(pkg_name); required_members.insert(package_name);
} }
let Some(package) = packages.get(pkg_name) else { let Some(package) = packages.get(package_name) else {
continue; continue;
}; };

View file

@ -161,7 +161,7 @@ pub(crate) enum ProjectError {
MissingGroupProject(GroupName), MissingGroupProject(GroupName),
#[error("Group `{0}` is not defined in any project's `dependency-groups` table")] #[error("Group `{0}` is not defined in any project's `dependency-groups` table")]
MissingGroupWorkspace(GroupName), MissingGroupProjects(GroupName),
#[error("PEP 723 scripts do not support dependency groups, but group `{0}` was specified")] #[error("PEP 723 scripts do not support dependency groups, but group `{0}` was specified")]
MissingGroupScript(GroupName), MissingGroupScript(GroupName),
@ -175,7 +175,7 @@ pub(crate) enum ProjectError {
MissingExtraProject(ExtraName), MissingExtraProject(ExtraName),
#[error("Extra `{0}` is not defined in any project's `optional-dependencies` table")] #[error("Extra `{0}` is not defined in any project's `optional-dependencies` table")]
MissingExtraWorkspace(ExtraName), MissingExtraProjects(ExtraName),
#[error("PEP 723 scripts do not support optional dependencies, but extra `{0}` was specified")] #[error("PEP 723 scripts do not support optional dependencies, but extra `{0}` was specified")]
MissingExtraScript(ExtraName), MissingExtraScript(ExtraName),

View file

@ -63,7 +63,7 @@ pub(crate) async fn sync(
dry_run: DryRun, dry_run: DryRun,
active: Option<bool>, active: Option<bool>,
all_packages: bool, all_packages: bool,
package: Option<PackageName>, package: Vec<PackageName>,
extras: ExtrasSpecification, extras: ExtrasSpecification,
groups: DependencyGroups, groups: DependencyGroups,
editable: Option<EditableMode>, editable: Option<EditableMode>,
@ -109,16 +109,28 @@ pub(crate) async fn sync(
&workspace_cache, &workspace_cache,
) )
.await? .await?
} else if let Some(package) = package.as_ref() { } else if let [name] = package.as_slice() {
VirtualProject::Project( VirtualProject::Project(
Workspace::discover(project_dir, &DiscoveryOptions::default(), &workspace_cache) Workspace::discover(project_dir, &DiscoveryOptions::default(), &workspace_cache)
.await? .await?
.with_current_project(package.clone()) .with_current_project(name.clone())
.with_context(|| format!("Package `{package}` not found in workspace"))?, .with_context(|| format!("Package `{name}` not found in workspace"))?,
) )
} else { } else {
VirtualProject::discover(project_dir, &DiscoveryOptions::default(), &workspace_cache) let project = VirtualProject::discover(
.await? project_dir,
&DiscoveryOptions::default(),
&workspace_cache,
)
.await?;
for name in &package {
if !project.workspace().packages().contains_key(name) {
return Err(anyhow::anyhow!("Package `{name}` not found in workspace"));
}
}
project
}; };
// TODO(lucab): improve warning content // TODO(lucab): improve warning content
@ -379,8 +391,7 @@ pub(crate) async fn sync(
} }
// Identify the installation target. // Identify the installation target.
let sync_target = let sync_target = identify_installation_target(&target, outcome.lock(), all_packages, &package);
identify_installation_target(&target, outcome.lock(), all_packages, package.as_ref());
let state = state.fork(); let state = state.fork();
@ -459,7 +470,7 @@ fn identify_installation_target<'a>(
target: &'a SyncTarget, target: &'a SyncTarget,
lock: &'a Lock, lock: &'a Lock,
all_packages: bool, all_packages: bool,
package: Option<&'a PackageName>, package: &'a [PackageName],
) -> InstallTarget<'a> { ) -> InstallTarget<'a> {
match &target { match &target {
SyncTarget::Project(project) => { SyncTarget::Project(project) => {
@ -470,33 +481,45 @@ fn identify_installation_target<'a>(
workspace: project.workspace(), workspace: project.workspace(),
lock, lock,
} }
} else if let Some(package) = package {
InstallTarget::Project {
workspace: project.workspace(),
name: package,
lock,
}
} else { } else {
// By default, install the root package. match package {
InstallTarget::Project { // By default, install the root project.
workspace: project.workspace(), [] => InstallTarget::Project {
name: project.project_name(), workspace: project.workspace(),
lock, name: project.project_name(),
lock,
},
[name] => InstallTarget::Project {
workspace: project.workspace(),
name,
lock,
},
names => InstallTarget::Projects {
workspace: project.workspace(),
names,
lock,
},
} }
} }
} }
VirtualProject::NonProject(workspace) => { VirtualProject::NonProject(workspace) => {
if all_packages { if all_packages {
InstallTarget::NonProjectWorkspace { workspace, lock } InstallTarget::NonProjectWorkspace { workspace, lock }
} else if let Some(package) = package {
InstallTarget::Project {
workspace,
name: package,
lock,
}
} else { } else {
// By default, install the entire workspace. match package {
InstallTarget::NonProjectWorkspace { workspace, lock } // By default, install the entire workspace.
[] => InstallTarget::NonProjectWorkspace { workspace, lock },
[name] => InstallTarget::Project {
workspace,
name,
lock,
},
names => InstallTarget::Projects {
workspace,
names,
lock,
},
}
} }
} }
} }
@ -613,6 +636,7 @@ pub(super) async fn do_sync(
let extra_build_requires = match &target { let extra_build_requires = match &target {
InstallTarget::Workspace { workspace, .. } InstallTarget::Workspace { workspace, .. }
| InstallTarget::Project { workspace, .. } | InstallTarget::Project { workspace, .. }
| InstallTarget::Projects { workspace, .. }
| InstallTarget::NonProjectWorkspace { workspace, .. } => { | InstallTarget::NonProjectWorkspace { workspace, .. } => {
LoweredExtraBuildDependencies::from_workspace( LoweredExtraBuildDependencies::from_workspace(
extra_build_dependencies.clone(), extra_build_dependencies.clone(),

View file

@ -1311,7 +1311,7 @@ pub(crate) struct SyncSettings {
pub(crate) install_options: InstallOptions, pub(crate) install_options: InstallOptions,
pub(crate) modifications: Modifications, pub(crate) modifications: Modifications,
pub(crate) all_packages: bool, pub(crate) all_packages: bool,
pub(crate) package: Option<PackageName>, pub(crate) package: Vec<PackageName>,
pub(crate) python: Option<String>, pub(crate) python: Option<String>,
pub(crate) python_platform: Option<TargetTriple>, pub(crate) python_platform: Option<TargetTriple>,
pub(crate) install_mirrors: PythonInstallMirrors, pub(crate) install_mirrors: PythonInstallMirrors,

View file

@ -3260,6 +3260,16 @@ fn lock_conflicting_workspace_members() -> Result<()> {
error: Package `example` and package `subexample` are incompatible with the declared conflicts: {example, subexample} error: Package `example` and package `subexample` are incompatible with the declared conflicts: {example, subexample}
"); ");
// Attempt to install them together, i.e., with `--package`
uv_snapshot!(context.filters(), context.sync().arg("--frozen").arg("--package").arg("example").arg("--package").arg("subexample"), @r"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Package `example` and package `subexample` are incompatible with the declared conflicts: {example, subexample}
");
Ok(()) Ok(())
} }

View file

@ -268,6 +268,115 @@ fn package() -> Result<()> {
Ok(()) Ok(())
} }
/// Sync multiple packages within a workspace.
#[test]
fn multiple_packages() -> Result<()> {
let context = TestContext::new("3.12");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"
[project]
name = "root"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["foo", "bar", "baz"]
[tool.uv.sources]
foo = { workspace = true }
bar = { workspace = true }
baz = { workspace = true }
[tool.uv.workspace]
members = ["packages/*"]
"#,
)?;
context
.temp_dir
.child("packages")
.child("foo")
.child("pyproject.toml")
.write_str(
r#"
[project]
name = "foo"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["anyio"]
"#,
)?;
context
.temp_dir
.child("packages")
.child("bar")
.child("pyproject.toml")
.write_str(
r#"
[project]
name = "bar"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["typing-extensions"]
"#,
)?;
context
.temp_dir
.child("packages")
.child("baz")
.child("pyproject.toml")
.write_str(
r#"
[project]
name = "baz"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["iniconfig"]
"#,
)?;
// Sync `foo` and `bar`.
uv_snapshot!(context.filters(), context.sync()
.arg("--package").arg("foo")
.arg("--package").arg("bar"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 9 packages in [TIME]
Prepared 6 packages in [TIME]
Installed 6 packages in [TIME]
+ anyio==4.3.0
+ bar==0.1.0 (from file://[TEMP_DIR]/packages/bar)
+ foo==0.1.0 (from file://[TEMP_DIR]/packages/foo)
+ idna==3.6
+ sniffio==1.3.1
+ typing-extensions==4.10.0
");
// Sync `foo`, `bar`, and `baz`.
uv_snapshot!(context.filters(), context.sync()
.arg("--package").arg("foo")
.arg("--package").arg("bar")
.arg("--package").arg("baz"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 9 packages in [TIME]
Prepared 2 packages in [TIME]
Installed 2 packages in [TIME]
+ baz==0.1.0 (from file://[TEMP_DIR]/packages/baz)
+ iniconfig==2.0.0
");
Ok(())
}
/// Test json output /// Test json output
#[test] #[test]
fn sync_json() -> Result<()> { fn sync_json() -> Result<()> {

View file

@ -1513,9 +1513,9 @@ uv sync [OPTIONS]
<ul> <ul>
<li><code>text</code>: Display the result in a human-readable format</li> <li><code>text</code>: Display the result in a human-readable format</li>
<li><code>json</code>: Display the result in JSON format</li> <li><code>json</code>: Display the result in JSON format</li>
</ul></dd><dt id="uv-sync--package"><a href="#uv-sync--package"><code>--package</code></a> <i>package</i></dt><dd><p>Sync for a specific package in the workspace.</p> </ul></dd><dt id="uv-sync--package"><a href="#uv-sync--package"><code>--package</code></a> <i>package</i></dt><dd><p>Sync for specific packages in the workspace.</p>
<p>The workspace's environment (<code>.venv</code>) is updated to reflect the subset of dependencies declared by the specified workspace member package.</p> <p>The workspace's environment (<code>.venv</code>) is updated to reflect the subset of dependencies declared by the specified workspace member packages.</p>
<p>If the workspace member does not exist, uv will exit with an error.</p> <p>If any workspace member does not exist, uv will exit with an error.</p>
</dd><dt id="uv-sync--prerelease"><a href="#uv-sync--prerelease"><code>--prerelease</code></a> <i>prerelease</i></dt><dd><p>The strategy to use when considering pre-release versions.</p> </dd><dt id="uv-sync--prerelease"><a href="#uv-sync--prerelease"><code>--prerelease</code></a> <i>prerelease</i></dt><dd><p>The strategy to use when considering pre-release versions.</p>
<p>By default, uv will accept pre-releases for packages that <em>only</em> publish pre-releases, along with first-party requirements that contain an explicit pre-release marker in the declared specifiers (<code>if-necessary-or-explicit</code>).</p> <p>By default, uv will accept pre-releases for packages that <em>only</em> publish pre-releases, along with first-party requirements that contain an explicit pre-release marker in the declared specifiers (<code>if-necessary-or-explicit</code>).</p>
<p>May also be set with the <code>UV_PRERELEASE</code> environment variable.</p><p>Possible values:</p> <p>May also be set with the <code>UV_PRERELEASE</code> environment variable.</p><p>Possible values:</p>