From 3e31c520470feb2188f324fb1d77bd0a54e0bdcc Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 23 Dec 2024 14:21:41 -0500 Subject: [PATCH] Move installable targets out of uv-resolver crate --- crates/uv-resolver/src/lib.rs | 2 +- .../src/lock/{target.rs => installable.rs} | 147 +++--------------- crates/uv-resolver/src/lock/mod.rs | 6 +- .../uv-resolver/src/lock/requirements_txt.rs | 6 +- crates/uv/src/commands/project/add.rs | 3 +- crates/uv/src/commands/project/export.rs | 5 +- .../uv/src/commands/project/install_target.rs | 143 +++++++++++++++++ crates/uv/src/commands/project/mod.rs | 1 + crates/uv/src/commands/project/remove.rs | 7 +- crates/uv/src/commands/project/run.rs | 3 +- crates/uv/src/commands/project/sync.rs | 3 +- 11 files changed, 185 insertions(+), 141 deletions(-) rename crates/uv-resolver/src/lock/{target.rs => installable.rs} (72%) create mode 100644 crates/uv/src/commands/project/install_target.rs diff --git a/crates/uv-resolver/src/lib.rs b/crates/uv-resolver/src/lib.rs index d9d1930b94b01..629539bace9e4 100644 --- a/crates/uv-resolver/src/lib.rs +++ b/crates/uv-resolver/src/lib.rs @@ -5,7 +5,7 @@ pub use exclusions::Exclusions; pub use flat_index::{FlatDistributions, FlatIndex}; pub use fork_strategy::ForkStrategy; pub use lock::{ - InstallTarget, Lock, LockError, LockVersion, PackageMap, RequirementsTxtExport, + Installable, Lock, LockError, LockVersion, Package, PackageMap, RequirementsTxtExport, ResolverManifest, SatisfiesResult, TreeDisplay, VERSION, }; pub use manifest::Manifest; diff --git a/crates/uv-resolver/src/lock/target.rs b/crates/uv-resolver/src/lock/installable.rs similarity index 72% rename from crates/uv-resolver/src/lock/target.rs rename to crates/uv-resolver/src/lock/installable.rs index dcbae78e6df93..66dc1984262a6 100644 --- a/crates/uv-resolver/src/lock/target.rs +++ b/crates/uv-resolver/src/lock/installable.rs @@ -1,154 +1,49 @@ +use std::collections::hash_map::Entry; +use std::collections::{BTreeMap, VecDeque}; +use std::path::Path; + use either::Either; use petgraph::Graph; use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet}; -use std::collections::hash_map::Entry; -use std::collections::{BTreeMap, VecDeque}; + use uv_configuration::{BuildOptions, DevGroupsManifest, ExtrasSpecification, InstallOptions}; use uv_distribution_types::{Edge, Node, Resolution, ResolvedDist}; -use uv_normalize::{ExtraName, GroupName, PackageName, DEV_DEPENDENCIES}; +use uv_normalize::{ExtraName, GroupName, PackageName}; use uv_pep508::MarkerTree; use uv_platform_tags::Tags; use uv_pypi_types::{ResolverMarkerEnvironment, VerbatimParsedUrl}; -use uv_workspace::dependency_groups::{DependencyGroupError, FlatDependencyGroups}; -use uv_workspace::Workspace; +use uv_workspace::dependency_groups::DependencyGroupError; use crate::lock::{LockErrorKind, Package, TagPolicy}; use crate::{Lock, LockError}; -/// A target that can be installed from a lockfile. -#[derive(Debug, Copy, Clone)] -pub enum InstallTarget<'env> { - /// A project (which could be a workspace root or member). - Project { - workspace: &'env Workspace, - name: &'env PackageName, - lock: &'env Lock, - }, - /// An entire workspace. - Workspace { - workspace: &'env Workspace, - lock: &'env Lock, - }, - /// An entire workspace with a (legacy) non-project root. - NonProjectWorkspace { - workspace: &'env Workspace, - lock: &'env Lock, - }, -} - -impl<'env> InstallTarget<'env> { - /// Return the [`Workspace`] of the target. - pub fn workspace(&self) -> &'env Workspace { - match self { - Self::Project { workspace, .. } => workspace, - Self::Workspace { workspace, .. } => workspace, - Self::NonProjectWorkspace { workspace, .. } => workspace, - } - } +pub trait Installable<'lock> { + /// Return the root install path. + fn install_path(&self) -> &'lock Path; - /// Return the [`Lock`] of the target. - pub fn lock(&self) -> &'env Lock { - match self { - Self::Project { lock, .. } => lock, - Self::Workspace { lock, .. } => lock, - Self::NonProjectWorkspace { lock, .. } => lock, - } - } + /// Return the [`Lock`] to install. + fn lock(&self) -> &'lock Lock; - /// Return the [`PackageName`] of the target. - pub fn packages(&self) -> impl Iterator { - match self { - Self::Project { name, .. } => Either::Right(Either::Left(std::iter::once(*name))), - Self::NonProjectWorkspace { lock, .. } => Either::Left(lock.members().iter()), - Self::Workspace { lock, .. } => { - // Identify the workspace members. - // - // 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. - if lock.members().is_empty() { - Either::Right(Either::Right( - lock.root().into_iter().map(|package| &package.id.name), - )) - } else { - Either::Left(lock.members().iter()) - } - } - } - } + /// Return the [`PackageName`] of the root packages in the target. + fn roots(&self) -> impl Iterator; /// Return the [`InstallTarget`] dependency groups. /// /// Returns dependencies that apply to the workspace root, but not any of its members. As such, /// only returns a non-empty iterator for virtual workspaces, which can include dev dependencies /// on the virtual root. - pub fn groups( + fn groups( &self, ) -> Result< BTreeMap>>, DependencyGroupError, - > { - match self { - Self::Project { .. } => Ok(BTreeMap::default()), - Self::Workspace { .. } => Ok(BTreeMap::default()), - Self::NonProjectWorkspace { workspace, .. } => { - // For non-projects, we might have `dependency-groups` or `tool.uv.dev-dependencies` - // that are attached to the workspace root (which isn't a member). - - // First, collect `tool.uv.dev_dependencies` - let dev_dependencies = workspace - .pyproject_toml() - .tool - .as_ref() - .and_then(|tool| tool.uv.as_ref()) - .and_then(|uv| uv.dev_dependencies.as_ref()); - - // Then, collect `dependency-groups` - let dependency_groups = workspace - .pyproject_toml() - .dependency_groups - .iter() - .flatten() - .collect::>(); - - // Merge any overlapping groups. - let mut map = BTreeMap::new(); - for (name, dependencies) in - FlatDependencyGroups::from_dependency_groups(&dependency_groups) - .map_err(|err| err.with_dev_dependencies(dev_dependencies))? - .into_iter() - .chain( - // Only add the `dev` group if `dev-dependencies` is defined. - dev_dependencies.into_iter().map(|requirements| { - (DEV_DEPENDENCIES.clone(), requirements.clone()) - }), - ) - { - match map.entry(name) { - std::collections::btree_map::Entry::Vacant(entry) => { - entry.insert(dependencies); - } - std::collections::btree_map::Entry::Occupied(mut entry) => { - entry.get_mut().extend(dependencies); - } - } - } - - Ok(map) - } - } - } + >; /// Return the [`PackageName`] of the target, if available. - pub fn project_name(&self) -> Option<&PackageName> { - match self { - Self::Project { name, .. } => Some(name), - Self::Workspace { .. } => None, - Self::NonProjectWorkspace { .. } => None, - } - } + fn project_name(&self) -> Option<&PackageName>; /// Convert the [`Lock`] to a [`Resolution`] using the given marker environment, tags, and root. - pub fn to_resolution( + fn to_resolution( &self, marker_env: &ResolverMarkerEnvironment, tags: &Tags, @@ -169,7 +64,7 @@ impl<'env> InstallTarget<'env> { let root = petgraph.add_node(Node::Root); // Add the workspace packages to the queue. - for root_name in self.packages() { + for root_name in self.roots() { let dist = self .lock() .find_by_name(root_name) @@ -419,7 +314,7 @@ impl<'env> InstallTarget<'env> { build_options: &BuildOptions, ) -> Result { let dist = package.to_dist( - self.workspace().install_path(), + self.install_path(), TagPolicy::Required(tags), build_options, )?; @@ -436,7 +331,7 @@ impl<'env> InstallTarget<'env> { /// Create a non-installable [`Node`] from a [`Package`]. fn non_installable_node(&self, package: &Package, tags: &Tags) -> Result { let dist = package.to_dist( - self.workspace().install_path(), + self.install_path(), TagPolicy::Preferred(tags), &BuildOptions::default(), )?; diff --git a/crates/uv-resolver/src/lock/mod.rs b/crates/uv-resolver/src/lock/mod.rs index 91aefe78d4f49..7f9e2ba23d2e6 100644 --- a/crates/uv-resolver/src/lock/mod.rs +++ b/crates/uv-resolver/src/lock/mod.rs @@ -14,9 +14,9 @@ use toml_edit::{value, Array, ArrayOfTables, InlineTable, Item, Table, Value}; use url::Url; use crate::fork_strategy::ForkStrategy; +pub use crate::lock::installable::Installable; pub use crate::lock::map::PackageMap; pub use crate::lock::requirements_txt::RequirementsTxtExport; -pub use crate::lock::target::InstallTarget; pub use crate::lock::tree::TreeDisplay; use crate::requires_python::SimplifiedMarkerTree; use crate::resolution::{AnnotatedDist, ResolutionGraphNode}; @@ -51,9 +51,9 @@ use uv_types::{BuildContext, HashStrategy}; use uv_workspace::dependency_groups::DependencyGroupError; use uv_workspace::Workspace; +mod installable; mod map; mod requirements_txt; -mod target; mod tree; /// The current version of the lockfile format. @@ -630,7 +630,7 @@ impl Lock { .iter() .copied() .map(|marker| SimplifiedMarkerTree::new(&self.requires_python, marker)) - .filter_map(super::requires_python::SimplifiedMarkerTree::try_to_string), + .filter_map(SimplifiedMarkerTree::try_to_string), ); doc.insert("supported-markers", value(supported_environments)); } diff --git a/crates/uv-resolver/src/lock/requirements_txt.rs b/crates/uv-resolver/src/lock/requirements_txt.rs index b398a23bb0518..7d2fd8bba9679 100644 --- a/crates/uv-resolver/src/lock/requirements_txt.rs +++ b/crates/uv-resolver/src/lock/requirements_txt.rs @@ -21,7 +21,7 @@ use uv_pypi_types::{ParsedArchiveUrl, ParsedGitUrl}; use crate::graph_ops::marker_reachability; use crate::lock::{Package, PackageId, Source}; use crate::universal_marker::{ConflictMarker, UniversalMarker}; -use crate::{InstallTarget, LockError}; +use crate::{Installable, LockError}; /// An export of a [`Lock`] that renders in `requirements.txt` format. #[derive(Debug)] @@ -33,7 +33,7 @@ pub struct RequirementsTxtExport<'lock> { impl<'lock> RequirementsTxtExport<'lock> { pub fn from_lock( - target: InstallTarget<'lock>, + target: &impl Installable<'lock>, prune: &[PackageName], extras: &ExtrasSpecification, dev: &DevGroupsManifest, @@ -51,7 +51,7 @@ impl<'lock> RequirementsTxtExport<'lock> { let root = petgraph.add_node(Node::Root); // Add the workspace package to the queue. - for root_name in target.packages() { + for root_name in target.roots() { if prune.contains(root_name) { continue; } diff --git a/crates/uv/src/commands/project/add.rs b/crates/uv/src/commands/project/add.rs index 55288defffa7b..520d44c822f62 100644 --- a/crates/uv/src/commands/project/add.rs +++ b/crates/uv/src/commands/project/add.rs @@ -28,7 +28,7 @@ use uv_pep508::{ExtraName, Requirement, UnnamedRequirement, VersionOrUrl}; use uv_pypi_types::{redact_credentials, ParsedUrl, RequirementSource, VerbatimParsedUrl}; use uv_python::{Interpreter, PythonDownloads, PythonEnvironment, PythonPreference, PythonRequest}; use uv_requirements::{NamedRequirementsResolver, RequirementsSource, RequirementsSpecification}; -use uv_resolver::{FlatIndex, InstallTarget}; +use uv_resolver::FlatIndex; use uv_scripts::{Pep723Item, Pep723Script}; use uv_settings::PythonInstallMirrors; use uv_types::{BuildIsolation, HashStrategy}; @@ -41,6 +41,7 @@ use crate::commands::pip::loggers::{ DefaultInstallLogger, DefaultResolveLogger, SummaryResolveLogger, }; use crate::commands::pip::operations::Modifications; +use crate::commands::project::install_target::InstallTarget; use crate::commands::project::lock::LockMode; use crate::commands::project::{ init_script_python_requirement, lock, ProjectError, ProjectInterpreter, ScriptInterpreter, diff --git a/crates/uv/src/commands/project/export.rs b/crates/uv/src/commands/project/export.rs index 00ed04bf52075..6aaa381fdd12c 100644 --- a/crates/uv/src/commands/project/export.rs +++ b/crates/uv/src/commands/project/export.rs @@ -15,10 +15,11 @@ use uv_configuration::{ use uv_dispatch::SharedState; use uv_normalize::PackageName; use uv_python::{PythonDownloads, PythonPreference, PythonRequest}; -use uv_resolver::{InstallTarget, RequirementsTxtExport}; +use uv_resolver::RequirementsTxtExport; use uv_workspace::{DiscoveryOptions, MemberDiscovery, VirtualProject, Workspace}; use crate::commands::pip::loggers::DefaultResolveLogger; +use crate::commands::project::install_target::InstallTarget; use crate::commands::project::lock::{do_safe_lock, LockMode}; use crate::commands::project::{ default_dependency_groups, detect_conflicts, DependencyGroupsTarget, ProjectError, @@ -186,7 +187,7 @@ pub(crate) async fn export( match format { ExportFormat::RequirementsTxt => { let export = RequirementsTxtExport::from_lock( - target, + &target, &prune, &extras, &dev, diff --git a/crates/uv/src/commands/project/install_target.rs b/crates/uv/src/commands/project/install_target.rs new file mode 100644 index 0000000000000..cadc22ace909e --- /dev/null +++ b/crates/uv/src/commands/project/install_target.rs @@ -0,0 +1,143 @@ +use std::collections::BTreeMap; +use std::path::Path; + +use itertools::Either; + +use uv_normalize::{GroupName, PackageName, DEV_DEPENDENCIES}; +use uv_pypi_types::VerbatimParsedUrl; +use uv_resolver::{Installable, Lock, Package}; +use uv_workspace::dependency_groups::{DependencyGroupError, FlatDependencyGroups}; +use uv_workspace::Workspace; + +/// A target that can be installed from a lockfile. +#[derive(Debug, Copy, Clone)] +pub(crate) enum InstallTarget<'lock> { + /// A project (which could be a workspace root or member). + Project { + workspace: &'lock Workspace, + name: &'lock PackageName, + lock: &'lock Lock, + }, + /// An entire workspace. + Workspace { + workspace: &'lock Workspace, + lock: &'lock Lock, + }, + /// An entire workspace with a (legacy) non-project root. + NonProjectWorkspace { + workspace: &'lock Workspace, + lock: &'lock Lock, + }, +} + +impl<'lock> Installable<'lock> for InstallTarget<'lock> { + fn install_path(&self) -> &'lock Path { + match self { + Self::Project { workspace, .. } => workspace.install_path(), + Self::Workspace { workspace, .. } => workspace.install_path(), + Self::NonProjectWorkspace { workspace, .. } => workspace.install_path(), + } + } + + fn lock(&self) -> &'lock Lock { + match self { + Self::Project { lock, .. } => lock, + Self::Workspace { lock, .. } => lock, + Self::NonProjectWorkspace { lock, .. } => lock, + } + } + + fn roots(&self) -> impl Iterator { + match self { + Self::Project { name, .. } => Either::Right(Either::Left(std::iter::once(*name))), + Self::NonProjectWorkspace { lock, .. } => Either::Left(lock.members().iter()), + Self::Workspace { lock, .. } => { + // Identify the workspace members. + // + // 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. + if lock.members().is_empty() { + Either::Right(Either::Right(lock.root().into_iter().map(Package::name))) + } else { + Either::Left(lock.members().iter()) + } + } + } + } + + fn groups( + &self, + ) -> Result< + BTreeMap>>, + DependencyGroupError, + > { + match self { + Self::Project { .. } => Ok(BTreeMap::default()), + Self::Workspace { .. } => Ok(BTreeMap::default()), + Self::NonProjectWorkspace { workspace, .. } => { + // For non-projects, we might have `dependency-groups` or `tool.uv.dev-dependencies` + // that are attached to the workspace root (which isn't a member). + + // First, collect `tool.uv.dev_dependencies` + let dev_dependencies = workspace + .pyproject_toml() + .tool + .as_ref() + .and_then(|tool| tool.uv.as_ref()) + .and_then(|uv| uv.dev_dependencies.as_ref()); + + // Then, collect `dependency-groups` + let dependency_groups = workspace + .pyproject_toml() + .dependency_groups + .iter() + .flatten() + .collect::>(); + + // Merge any overlapping groups. + let mut map = BTreeMap::new(); + for (name, dependencies) in + FlatDependencyGroups::from_dependency_groups(&dependency_groups) + .map_err(|err| err.with_dev_dependencies(dev_dependencies))? + .into_iter() + .chain( + // Only add the `dev` group if `dev-dependencies` is defined. + dev_dependencies.into_iter().map(|requirements| { + (DEV_DEPENDENCIES.clone(), requirements.clone()) + }), + ) + { + match map.entry(name) { + std::collections::btree_map::Entry::Vacant(entry) => { + entry.insert(dependencies); + } + std::collections::btree_map::Entry::Occupied(mut entry) => { + entry.get_mut().extend(dependencies); + } + } + } + + Ok(map) + } + } + } + + fn project_name(&self) -> Option<&PackageName> { + match self { + Self::Project { name, .. } => Some(name), + Self::Workspace { .. } => None, + Self::NonProjectWorkspace { .. } => None, + } + } +} + +impl<'lock> InstallTarget<'lock> { + /// Return the [`Workspace`] of the target. + pub(crate) fn workspace(&self) -> &'lock Workspace { + match self { + Self::Project { workspace, .. } => workspace, + Self::Workspace { workspace, .. } => workspace, + Self::NonProjectWorkspace { workspace, .. } => workspace, + } + } +} diff --git a/crates/uv/src/commands/project/mod.rs b/crates/uv/src/commands/project/mod.rs index 3c218fd3322de..4869aaf38c04c 100644 --- a/crates/uv/src/commands/project/mod.rs +++ b/crates/uv/src/commands/project/mod.rs @@ -53,6 +53,7 @@ pub(crate) mod add; pub(crate) mod environment; pub(crate) mod export; pub(crate) mod init; +mod install_target; pub(crate) mod lock; pub(crate) mod remove; pub(crate) mod run; diff --git a/crates/uv/src/commands/project/remove.rs b/crates/uv/src/commands/project/remove.rs index df9e8ee9228a5..b032487c2b16b 100644 --- a/crates/uv/src/commands/project/remove.rs +++ b/crates/uv/src/commands/project/remove.rs @@ -1,9 +1,9 @@ -use anyhow::{Context, Result}; use std::fmt::Write; use std::path::Path; -use uv_settings::PythonInstallMirrors; +use anyhow::{Context, Result}; use owo_colors::OwoColorize; + use uv_cache::Cache; use uv_client::Connectivity; use uv_configuration::{ @@ -15,8 +15,8 @@ use uv_fs::Simplified; use uv_normalize::DEV_DEPENDENCIES; use uv_pep508::PackageName; use uv_python::{PythonDownloads, PythonPreference, PythonRequest}; -use uv_resolver::InstallTarget; use uv_scripts::Pep723Script; +use uv_settings::PythonInstallMirrors; use uv_warnings::warn_user_once; use uv_workspace::pyproject::DependencyType; use uv_workspace::pyproject_mut::{DependencyTarget, PyProjectTomlMut}; @@ -24,6 +24,7 @@ use uv_workspace::{DiscoveryOptions, VirtualProject, Workspace}; use crate::commands::pip::loggers::{DefaultInstallLogger, DefaultResolveLogger}; use crate::commands::pip::operations::Modifications; +use crate::commands::project::install_target::InstallTarget; use crate::commands::project::lock::LockMode; use crate::commands::project::{default_dependency_groups, ProjectError}; use crate::commands::{diagnostics, project, ExitStatus}; diff --git a/crates/uv/src/commands/project/run.rs b/crates/uv/src/commands/project/run.rs index fccbe068afc85..9fb3ae476ba46 100644 --- a/crates/uv/src/commands/project/run.rs +++ b/crates/uv/src/commands/project/run.rs @@ -31,7 +31,7 @@ use uv_python::{ PythonPreference, PythonRequest, PythonVersionFile, VersionFileDiscoveryOptions, }; use uv_requirements::{RequirementsSource, RequirementsSpecification}; -use uv_resolver::{InstallTarget, Lock}; +use uv_resolver::Lock; use uv_scripts::Pep723Item; use uv_settings::PythonInstallMirrors; use uv_static::EnvVars; @@ -43,6 +43,7 @@ use crate::commands::pip::loggers::{ }; use crate::commands::pip::operations::Modifications; use crate::commands::project::environment::CachedEnvironment; +use crate::commands::project::install_target::InstallTarget; use crate::commands::project::lock::LockMode; use crate::commands::project::{ default_dependency_groups, validate_project_requires_python, DependencyGroupsTarget, diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs index 53fd3a573359f..c3eb80a030011 100644 --- a/crates/uv/src/commands/project/sync.rs +++ b/crates/uv/src/commands/project/sync.rs @@ -23,7 +23,7 @@ use uv_pypi_types::{ LenientRequirement, ParsedArchiveUrl, ParsedGitUrl, ParsedUrl, VerbatimParsedUrl, }; use uv_python::{PythonDownloads, PythonEnvironment, PythonPreference, PythonRequest}; -use uv_resolver::{FlatIndex, InstallTarget}; +use uv_resolver::{FlatIndex, Installable}; use uv_settings::PythonInstallMirrors; use uv_types::{BuildIsolation, HashStrategy}; use uv_warnings::warn_user; @@ -33,6 +33,7 @@ use uv_workspace::{DiscoveryOptions, MemberDiscovery, VirtualProject, Workspace} use crate::commands::pip::loggers::{DefaultInstallLogger, DefaultResolveLogger, InstallLogger}; use crate::commands::pip::operations; use crate::commands::pip::operations::Modifications; +use crate::commands::project::install_target::InstallTarget; use crate::commands::project::lock::{do_safe_lock, LockMode}; use crate::commands::project::{ default_dependency_groups, detect_conflicts, DependencyGroupsTarget, ProjectError,