Refactors

This commit is contained in:
Charlie Marsh 2025-10-31 21:30:27 -04:00
parent 70f3ec15c6
commit 31d031c319
5 changed files with 130 additions and 252 deletions

View File

@ -75,9 +75,7 @@ impl<'lock> Installable<'lock> for InstallTarget<'lock> {
match self {
Self::Project { name, .. } => Box::new(std::iter::once(*name)),
Self::Projects { names, .. } => Box::new(names.iter()),
Self::NonProjectWorkspace { lock, .. } => {
Box::new(lock.members().iter())
}
Self::NonProjectWorkspace { lock, .. } => Box::new(lock.members().iter()),
Self::Workspace { lock, .. } => {
// Identify the workspace members.
//
@ -297,9 +295,9 @@ impl<'lock> InstallTarget<'lock> {
Err(ProjectError::MissingExtraProject(extra.clone()))
}
Self::Projects { .. } => {
Err(ProjectError::MissingExtraWorkspace(extra.clone()))
Err(ProjectError::MissingExtraProjects(extra.clone()))
}
_ => Err(ProjectError::MissingExtraWorkspace(extra.clone())),
_ => Err(ProjectError::MissingExtraProjects(extra.clone())),
};
}
}
@ -355,7 +353,7 @@ impl<'lock> InstallTarget<'lock> {
for group in groups.explicit_names() {
if !known_groups.contains(group) {
return Err(ProjectError::MissingGroupWorkspace(group.clone()));
return Err(ProjectError::MissingGroupProjects(group.clone()));
}
}
}
@ -376,8 +374,12 @@ impl<'lock> InstallTarget<'lock> {
for group in groups.explicit_names() {
if !known_groups.contains(group) {
return match self {
Self::Project { .. } => Err(ProjectError::MissingGroupProject(group.clone())),
Self::Projects { .. } => Err(ProjectError::MissingGroupWorkspace(group.clone())),
Self::Project { .. } => {
Err(ProjectError::MissingGroupProject(group.clone()))
}
Self::Projects { .. } => {
Err(ProjectError::MissingGroupProjects(group.clone()))
}
_ => unreachable!(),
};
}
@ -402,108 +404,27 @@ impl<'lock> InstallTarget<'lock> {
groups: &DependencyGroupsWithDefaults,
) -> BTreeSet<&PackageName> {
match self {
Self::Project { name, lock, .. } => {
Self::Project { lock, .. } | Self::Projects { lock, .. } => {
let roots = self.roots().collect::<FxHashSet<_>>();
// Collect the packages by name for efficient lookup.
let packages = lock
.packages()
.iter()
.map(|p| (p.name(), p))
.collect::<BTreeMap<_, _>>();
// We'll include the project itself
let mut required_members = BTreeSet::new();
required_members.insert(*name);
// Find all workspace member dependencies recursively
let mut queue: VecDeque<(&PackageName, Option<&ExtraName>)> = VecDeque::new();
let mut seen: FxHashSet<(&PackageName, Option<&ExtraName>)> = FxHashSet::default();
let Some(root_package) = packages.get(name) else {
return required_members;
};
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;
}
for dependency in dependencies {
let name = dependency.package_name();
queue.push_back((name, None));
for extra in dependency.extra() {
queue.push_back((name, Some(extra)));
}
}
}
while let Some((pkg_name, extra)) = queue.pop_front() {
if lock.members().contains(pkg_name) {
required_members.insert(pkg_name);
}
let Some(package) = packages.get(pkg_name) else {
continue;
};
let Some(dependencies) = extra
.map(|extra_name| {
package
.optional_dependencies()
.get(extra_name)
.map(Vec::as_slice)
})
.unwrap_or(Some(package.dependencies()))
else {
continue;
};
for dependency in dependencies {
let name = dependency.package_name();
if seen.insert((name, None)) {
queue.push_back((name, None));
}
for extra in dependency.extra() {
if seen.insert((name, Some(extra))) {
queue.push_back((name, Some(extra)));
}
}
}
}
required_members
}
Self::Projects { names, lock, .. } => {
// Collect the packages by name for efficient lookup.
let packages = lock
.packages()
.iter()
.map(|p| (p.name(), p))
.map(|package| (package.name(), package))
.collect::<BTreeMap<_, _>>();
// We'll include all specified projects
let mut required_members = BTreeSet::new();
for name in names.iter() {
required_members.insert(name);
for name in &roots {
required_members.insert(*name);
}
// Find all workspace member dependencies recursively for all specified packages
let mut queue: VecDeque<(&PackageName, Option<&ExtraName>)> = VecDeque::new();
let mut seen: FxHashSet<(&PackageName, Option<&ExtraName>)> = FxHashSet::default();
for name in names.iter() {
for name in roots {
let Some(root_package) = packages.get(name) else {
continue;
};
@ -515,7 +436,8 @@ impl<'lock> InstallTarget<'lock> {
}
// Add explicitly activated extras for the root package
for extra in extras.extra_names(root_package.optional_dependencies().keys()) {
for extra in extras.extra_names(root_package.optional_dependencies().keys())
{
if seen.insert((name, Some(extra))) {
queue.push_back((name, Some(extra)));
}
@ -541,12 +463,12 @@ impl<'lock> InstallTarget<'lock> {
}
}
while let Some((pkg_name, extra)) = queue.pop_front() {
if lock.members().contains(pkg_name) {
required_members.insert(pkg_name);
while let Some((package_name, extra)) = queue.pop_front() {
if lock.members().contains(package_name) {
required_members.insert(package_name);
}
let Some(package) = packages.get(pkg_name) else {
let Some(package) = packages.get(package_name) else {
continue;
};

View File

@ -161,7 +161,7 @@ pub(crate) enum ProjectError {
MissingGroupProject(GroupName),
#[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")]
MissingGroupScript(GroupName),
@ -175,7 +175,7 @@ pub(crate) enum ProjectError {
MissingExtraProject(ExtraName),
#[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")]
MissingExtraScript(ExtraName),

View File

@ -109,40 +109,28 @@ pub(crate) async fn sync(
&workspace_cache,
)
.await?
} else if !package.is_empty() {
// If a single package is specified, use it as the current project
if package.len() == 1 {
} else if let [name] = package.as_slice() {
VirtualProject::Project(
Workspace::discover(
project_dir,
&DiscoveryOptions::default(),
&workspace_cache,
)
Workspace::discover(project_dir, &DiscoveryOptions::default(), &workspace_cache)
.await?
.with_current_project(package[0].clone())
.with_context(|| format!("Package `{}` not found in workspace", package[0]))?,
.with_current_project(name.clone())
.with_context(|| format!("Package `{name}` not found in workspace"))?,
)
} else {
// Multiple packages specified - discover the workspace and validate all packages exist
let workspace = Workspace::discover(
let project = VirtualProject::discover(
project_dir,
&DiscoveryOptions::default(),
&workspace_cache,
)
.await?;
// Validate that all specified packages exist in the workspace
for pkg in &package {
if !workspace.packages().contains_key(pkg) {
anyhow::bail!("Package `{pkg}` not found in workspace");
for name in &package {
if !project.workspace().packages().contains_key(name) {
return Err(anyhow::anyhow!("Package `{name}` not found in workspace",));
}
}
VirtualProject::NonProject(workspace)
}
} else {
VirtualProject::discover(project_dir, &DiscoveryOptions::default(), &workspace_cache)
.await?
project
};
// TODO(lucab): improve warning content
@ -493,49 +481,45 @@ fn identify_installation_target<'a>(
workspace: project.workspace(),
lock,
}
} else if !package.is_empty() {
if package.len() == 1 {
InstallTarget::Project {
workspace: project.workspace(),
name: &package[0],
lock,
}
} else {
InstallTarget::Projects {
workspace: project.workspace(),
names: package,
lock,
}
}
} else {
// By default, install the root package.
InstallTarget::Project {
match package {
// By default, install the root project.
[] => InstallTarget::Project {
workspace: project.workspace(),
name: project.project_name(),
lock,
},
[name] => InstallTarget::Project {
workspace: project.workspace(),
name,
lock,
},
names => InstallTarget::Projects {
workspace: project.workspace(),
names,
lock,
},
}
}
}
VirtualProject::NonProject(workspace) => {
if all_packages {
InstallTarget::NonProjectWorkspace { workspace, lock }
} else if !package.is_empty() {
if package.len() == 1 {
InstallTarget::Project {
workspace,
name: &package[0],
lock,
}
} else {
InstallTarget::Projects {
workspace,
names: package,
lock,
}
}
} else {
match package {
// By default, install the entire workspace.
InstallTarget::NonProjectWorkspace { workspace, lock }
[] => InstallTarget::NonProjectWorkspace { workspace, lock },
[name] => InstallTarget::Project {
workspace,
name,
lock,
},
names => InstallTarget::Projects {
workspace,
names,
lock,
},
}
}
}
}

View File

@ -280,126 +280,98 @@ fn multiple_packages() -> Result<()> {
name = "root"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["child-a", "child-b", "child-c"]
dependencies = ["foo", "bar", "baz"]
[tool.uv.sources]
child-a = { workspace = true }
child-b = { workspace = true }
child-c = { workspace = true }
foo = { workspace = true }
bar = { workspace = true }
baz = { workspace = true }
[tool.uv.workspace]
members = ["packages/*"]
"#,
)?;
let src = context.temp_dir.child("src").child("root");
src.create_dir_all()?;
src.child("__init__.py").touch()?;
// Create child-a with requests dependency
let child_a = context.temp_dir.child("packages").child("child-a");
fs_err::create_dir_all(&child_a)?;
child_a.child("pyproject.toml").write_str(
context
.temp_dir
.child("packages")
.child("foo")
.child("pyproject.toml")
.write_str(
r#"
[project]
name = "child-a"
name = "foo"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["requests"]
[build-system]
requires = ["setuptools>=42"]
build-backend = "setuptools.build_meta"
dependencies = ["anyio"]
"#,
)?;
let src = child_a.child("src").child("child_a");
src.create_dir_all()?;
src.child("__init__.py").touch()?;
// Create child-b with httpx dependency
let child_b = context.temp_dir.child("packages").child("child-b");
fs_err::create_dir_all(&child_b)?;
child_b.child("pyproject.toml").write_str(
context
.temp_dir
.child("packages")
.child("bar")
.child("pyproject.toml")
.write_str(
r#"
[project]
name = "child-b"
name = "bar"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["httpx"]
[build-system]
requires = ["setuptools>=42"]
build-backend = "setuptools.build_meta"
dependencies = ["typing-extensions"]
"#,
)?;
let src = child_b.child("src").child("child_b");
src.create_dir_all()?;
src.child("__init__.py").touch()?;
// Create child-c with pytest dependency
let child_c = context.temp_dir.child("packages").child("child-c");
fs_err::create_dir_all(&child_c)?;
child_c.child("pyproject.toml").write_str(
context
.temp_dir
.child("packages")
.child("baz")
.child("pyproject.toml")
.write_str(
r#"
[project]
name = "child-c"
name = "baz"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["pytest"]
[build-system]
requires = ["setuptools>=42"]
build-backend = "setuptools.build_meta"
dependencies = ["iniconfig"]
"#,
)?;
let src = child_c.child("src").child("child_c");
src.create_dir_all()?;
src.child("__init__.py").touch()?;
// Sync only child-a and child-b
// Sync `foo` and `bar`.
uv_snapshot!(context.filters(), context.sync()
.arg("--package").arg("child-a")
.arg("--package").arg("child-b"), @r"
.arg("--package").arg("foo")
.arg("--package").arg("bar"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 19 packages in [TIME]
Prepared 12 packages in [TIME]
Installed 12 packages in [TIME]
Resolved 9 packages in [TIME]
Prepared 6 packages in [TIME]
Installed 6 packages in [TIME]
+ anyio==4.3.0
+ certifi==2024.2.2
+ charset-normalizer==3.3.2
+ child-a==0.1.0 (from file://[TEMP_DIR]/packages/child-a)
+ child-b==0.1.0 (from file://[TEMP_DIR]/packages/child-b)
+ h11==0.14.0
+ httpcore==1.0.4
+ httpx==0.27.0
+ bar==0.1.0 (from file://[TEMP_DIR]/packages/bar)
+ foo==0.1.0 (from file://[TEMP_DIR]/packages/foo)
+ idna==3.6
+ requests==2.31.0
+ sniffio==1.3.1
+ urllib3==2.2.1
+ typing-extensions==4.10.0
");
// Now sync all three packages
// Sync `foo`, `bar`, and `baz`.
uv_snapshot!(context.filters(), context.sync()
.arg("--package").arg("child-a")
.arg("--package").arg("child-b")
.arg("--package").arg("child-c"), @r"
.arg("--package").arg("foo")
.arg("--package").arg("bar")
.arg("--package").arg("baz"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 19 packages in [TIME]
Prepared 5 packages in [TIME]
Installed 5 packages in [TIME]
+ child-c==0.1.0 (from file://[TEMP_DIR]/packages/child-c)
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
+ packaging==24.0
+ pluggy==1.4.0
+ pytest==8.1.1
");
Ok(())

View File

@ -1513,9 +1513,9 @@ uv sync [OPTIONS]
<ul>
<li><code>text</code>: Display the result in a human-readable 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>
<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>If the workspace member does not exist, uv will exit with an error.</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 packages.</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>
<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>