Do not require workspace members to sync with `--frozen` (#6737)

## Summary

Closes https://github.com/astral-sh/uv/issues/6685.
This commit is contained in:
Charlie Marsh 2024-08-28 07:58:50 -04:00 committed by GitHub
parent 485e0d2748
commit 53ef633c6d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 85 additions and 27 deletions

1
Cargo.lock generated
View File

@ -4743,7 +4743,6 @@ dependencies = [
"uv-auth", "uv-auth",
"uv-cache", "uv-cache",
"uv-normalize", "uv-normalize",
"uv-workspace",
] ]
[[package]] [[package]]

View File

@ -20,7 +20,6 @@ pypi-types = { workspace = true }
uv-auth = { workspace = true } uv-auth = { workspace = true }
uv-cache = { workspace = true } uv-cache = { workspace = true }
uv-normalize = { workspace = true } uv-normalize = { workspace = true }
uv-workspace = { workspace = true }
clap = { workspace = true, features = ["derive"], optional = true } clap = { workspace = true, features = ["derive"], optional = true }
either = { workspace = true } either = { workspace = true }

View File

@ -1,9 +1,10 @@
use std::collections::BTreeSet;
use rustc_hash::FxHashSet; use rustc_hash::FxHashSet;
use tracing::debug; use tracing::debug;
use distribution_types::{Name, Resolution}; use distribution_types::{Name, Resolution};
use pep508_rs::PackageName; use pep508_rs::PackageName;
use uv_workspace::VirtualProject;
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
pub struct InstallOptions { pub struct InstallOptions {
@ -28,13 +29,14 @@ impl InstallOptions {
pub fn filter_resolution( pub fn filter_resolution(
&self, &self,
resolution: Resolution, resolution: Resolution,
project: &VirtualProject, project_name: Option<&PackageName>,
members: &BTreeSet<PackageName>,
) -> Resolution { ) -> Resolution {
// If `--no-install-project` is set, remove the project itself. // If `--no-install-project` is set, remove the project itself.
let resolution = self.apply_no_install_project(resolution, project); let resolution = self.apply_no_install_project(resolution, project_name);
// If `--no-install-workspace` is set, remove the project and any workspace members. // If `--no-install-workspace` is set, remove the project and any workspace members.
let resolution = self.apply_no_install_workspace(resolution, project); let resolution = self.apply_no_install_workspace(resolution, members);
// If `--no-install-package` is provided, remove the requested packages. // If `--no-install-package` is provided, remove the requested packages.
self.apply_no_install_package(resolution) self.apply_no_install_package(resolution)
@ -43,13 +45,13 @@ impl InstallOptions {
fn apply_no_install_project( fn apply_no_install_project(
&self, &self,
resolution: Resolution, resolution: Resolution,
project: &VirtualProject, project_name: Option<&PackageName>,
) -> Resolution { ) -> Resolution {
if !self.no_install_project { if !self.no_install_project {
return resolution; return resolution;
} }
let Some(project_name) = project.project_name() else { let Some(project_name) = project_name else {
debug!("Ignoring `--no-install-project` for virtual workspace"); debug!("Ignoring `--no-install-project` for virtual workspace");
return resolution; return resolution;
}; };
@ -60,17 +62,13 @@ impl InstallOptions {
fn apply_no_install_workspace( fn apply_no_install_workspace(
&self, &self,
resolution: Resolution, resolution: Resolution,
project: &VirtualProject, members: &BTreeSet<PackageName>,
) -> Resolution { ) -> Resolution {
if !self.no_install_workspace { if !self.no_install_workspace {
return resolution; return resolution;
} }
let workspace_packages = project.workspace().packages(); resolution.filter(|dist| !members.contains(dist.name()))
resolution.filter(|dist| {
!workspace_packages.contains_key(dist.name())
&& Some(dist.name()) != project.project_name()
})
} }
fn apply_no_install_package(&self, resolution: Resolution) -> Resolution { fn apply_no_install_package(&self, resolution: Resolution) -> Resolution {

View File

@ -409,6 +409,11 @@ impl Lock {
&self.supported_environments &self.supported_environments
} }
/// Returns the workspace members that were used to generate this lock.
pub fn members(&self) -> &BTreeSet<PackageName> {
&self.manifest.members
}
/// If this lockfile was built from a forking resolution with non-identical forks, return the /// If this lockfile was built from a forking resolution with non-identical forks, return the
/// markers of those forks, otherwise `None`. /// markers of those forks, otherwise `None`.
pub fn fork_markers(&self) -> &[MarkerTree] { pub fn fork_markers(&self) -> &[MarkerTree] {

View File

@ -1,6 +1,6 @@
pub use workspace::{ pub use workspace::{
check_nested_workspaces, DiscoveryOptions, ProjectWorkspace, VirtualProject, Workspace, check_nested_workspaces, DiscoveryOptions, MemberDiscovery, ProjectWorkspace, VirtualProject,
WorkspaceError, WorkspaceMember, Workspace, WorkspaceError, WorkspaceMember,
}; };
pub mod pyproject; pub mod pyproject;

View File

@ -44,12 +44,23 @@ pub enum WorkspaceError {
Normalize(#[source] std::io::Error), Normalize(#[source] std::io::Error),
} }
#[derive(Debug, Default, Clone)]
pub enum MemberDiscovery<'a> {
/// Discover all workspace members.
#[default]
All,
/// Don't discover any workspace members.
None,
/// Discover workspace members, but ignore the given paths.
Ignore(FxHashSet<&'a Path>),
}
#[derive(Debug, Default, Clone)] #[derive(Debug, Default, Clone)]
pub struct DiscoveryOptions<'a> { pub struct DiscoveryOptions<'a> {
/// The path to stop discovery at. /// The path to stop discovery at.
pub stop_discovery_at: Option<&'a Path>, pub stop_discovery_at: Option<&'a Path>,
/// The set of member paths to ignore. /// The strategy to use when discovering workspace members.
pub ignore: FxHashSet<&'a Path>, pub members: MemberDiscovery<'a>,
} }
/// A workspace, consisting of a root directory and members. See [`ProjectWorkspace`]. /// A workspace, consisting of a root directory and members. See [`ProjectWorkspace`].
@ -546,7 +557,12 @@ impl Workspace {
.clone(); .clone();
// If the directory is explicitly ignored, skip it. // If the directory is explicitly ignored, skip it.
if options.ignore.contains(member_root.as_path()) { let skip = match &options.members {
MemberDiscovery::All => false,
MemberDiscovery::None => true,
MemberDiscovery::Ignore(ignore) => ignore.contains(member_root.as_path()),
};
if skip {
debug!( debug!(
"Ignoring workspace member: `{}`", "Ignoring workspace member: `{}`",
member_root.simplified_display() member_root.simplified_display()

View File

@ -15,7 +15,7 @@ use uv_python::{
}; };
use uv_resolver::RequiresPython; use uv_resolver::RequiresPython;
use uv_workspace::pyproject_mut::{DependencyTarget, PyProjectTomlMut}; use uv_workspace::pyproject_mut::{DependencyTarget, PyProjectTomlMut};
use uv_workspace::{DiscoveryOptions, Workspace, WorkspaceError}; use uv_workspace::{DiscoveryOptions, MemberDiscovery, Workspace, WorkspaceError};
use crate::commands::project::find_requires_python; use crate::commands::project::find_requires_python;
use crate::commands::reporters::PythonDownloadReporter; use crate::commands::reporters::PythonDownloadReporter;
@ -141,7 +141,7 @@ async fn init_project(
match Workspace::discover( match Workspace::discover(
parent, parent,
&DiscoveryOptions { &DiscoveryOptions {
ignore: std::iter::once(path).collect(), members: MemberDiscovery::Ignore(std::iter::once(path).collect()),
..DiscoveryOptions::default() ..DiscoveryOptions::default()
}, },
) )

View File

@ -1,7 +1,6 @@
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use itertools::Itertools;
use distribution_types::{Dist, ResolvedDist, SourceDist}; use distribution_types::{Dist, ResolvedDist, SourceDist};
use itertools::Itertools;
use pep508_rs::MarkerTree; use pep508_rs::MarkerTree;
use uv_auth::store_credentials_from_url; use uv_auth::store_credentials_from_url;
use uv_cache::Cache; use uv_cache::Cache;
@ -14,7 +13,7 @@ use uv_normalize::{PackageName, DEV_DEPENDENCIES};
use uv_python::{PythonDownloads, PythonEnvironment, PythonPreference, PythonRequest}; use uv_python::{PythonDownloads, PythonEnvironment, PythonPreference, PythonRequest};
use uv_resolver::{FlatIndex, Lock}; use uv_resolver::{FlatIndex, Lock};
use uv_types::{BuildIsolation, HashStrategy}; use uv_types::{BuildIsolation, HashStrategy};
use uv_workspace::{DiscoveryOptions, VirtualProject, Workspace}; use uv_workspace::{DiscoveryOptions, MemberDiscovery, VirtualProject, Workspace};
use crate::commands::pip::loggers::{DefaultInstallLogger, DefaultResolveLogger, InstallLogger}; use crate::commands::pip::loggers::{DefaultInstallLogger, DefaultResolveLogger, InstallLogger};
use crate::commands::pip::operations::Modifications; use crate::commands::pip::operations::Modifications;
@ -52,6 +51,15 @@ pub(crate) async fn sync(
.with_current_project(package.clone()) .with_current_project(package.clone())
.with_context(|| format!("Package `{package}` not found in workspace"))?, .with_context(|| format!("Package `{package}` not found in workspace"))?,
) )
} else if frozen {
VirtualProject::discover(
&CWD,
&DiscoveryOptions {
members: MemberDiscovery::None,
..DiscoveryOptions::default()
},
)
.await?
} else { } else {
VirtualProject::discover(&CWD, &DiscoveryOptions::default()).await? VirtualProject::discover(&CWD, &DiscoveryOptions::default()).await?
}; };
@ -201,7 +209,8 @@ pub(super) async fn do_sync(
let resolution = apply_no_virtual_project(resolution); let resolution = apply_no_virtual_project(resolution);
// Filter resolution based on install-specific options. // Filter resolution based on install-specific options.
let resolution = install_options.filter_resolution(resolution, project); let resolution =
install_options.filter_resolution(resolution, project.project_name(), lock.members());
// Add all authenticated sources to the cache. // Add all authenticated sources to the cache.
for url in index_locations.urls() { for url in index_locations.urls() {

View File

@ -1103,10 +1103,30 @@ fn no_install_workspace() -> Result<()> {
+ sniffio==1.3.1 + sniffio==1.3.1
"###); "###);
// However, we do require the `pyproject.toml`. // Remove the virtual environment.
fs_err::remove_dir_all(&context.venv)?;
// We don't require the `pyproject.toml` for non-root members, if `--frozen` is provided.
fs_err::remove_file(child.join("pyproject.toml"))?; fs_err::remove_file(child.join("pyproject.toml"))?;
uv_snapshot!(context.filters(), context.sync().arg("--no-install-workspace"), @r###" uv_snapshot!(context.filters(), context.sync().arg("--no-install-workspace").arg("--frozen"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Using Python 3.12.[X] interpreter at: [PYTHON-3.12]
Creating virtualenv at: .venv
Prepared 4 packages in [TIME]
Installed 4 packages in [TIME]
+ anyio==3.7.0
+ idna==3.6
+ iniconfig==2.0.0
+ sniffio==1.3.1
"###);
// Unless `--package` is used.
uv_snapshot!(context.filters(), context.sync().arg("--package").arg("child").arg("--no-install-workspace").arg("--frozen"), @r###"
success: false success: false
exit_code: 2 exit_code: 2
----- stdout ----- ----- stdout -----
@ -1115,6 +1135,18 @@ fn no_install_workspace() -> Result<()> {
error: Workspace member `[TEMP_DIR]/child` is missing a `pyproject.toml` (matches: `child`) error: Workspace member `[TEMP_DIR]/child` is missing a `pyproject.toml` (matches: `child`)
"###); "###);
// But we do require the root `pyproject.toml`.
fs_err::remove_file(context.temp_dir.join("pyproject.toml"))?;
uv_snapshot!(context.filters(), context.sync().arg("--no-install-workspace").arg("--frozen"), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: No `pyproject.toml` found in current directory or any parent directory
"###);
Ok(()) Ok(())
} }