Skip to content

Commit

Permalink
Move installable targets out of uv-resolver crate (#10126)
Browse files Browse the repository at this point in the history
## Summary

The proximate motivation is that I want to add new variant for scripts,
but `uv-resolver` can't depend on `uv-scripts` without creating a
circular dependency. However, I think this _does_ just make more sense
-- the resolver crate shouldn't be coupled to the various kinds of
workspaces, and these details are mostly encoded in `projects/lock.rs`
and similar files.
  • Loading branch information
charliermarsh authored Dec 25, 2024
1 parent 6745a8b commit 7c47a45
Show file tree
Hide file tree
Showing 11 changed files with 116 additions and 84 deletions.
2 changes: 1 addition & 1 deletion crates/uv-resolver/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use std::collections::hash_map::Entry;
use std::collections::VecDeque;
use std::path::Path;

use either::Either;
use petgraph::Graph;
Expand All @@ -11,83 +12,25 @@ use uv_normalize::{ExtraName, GroupName, PackageName};
use uv_pep508::MarkerTree;
use uv_platform_tags::Tags;
use uv_pypi_types::ResolverMarkerEnvironment;
use uv_workspace::Workspace;

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,
},
}
pub trait Installable<'lock> {
/// Return the root install path.
fn install_path(&self) -> &'lock Path;

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,
}
}
/// Return the [`Lock`] to install.
fn lock(&self) -> &'lock Lock;

/// 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 [`PackageName`] of the target.
pub fn packages(&self) -> impl Iterator<Item = &PackageName> {
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<Item = &PackageName>;

/// 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,
Expand All @@ -108,7 +51,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)
Expand Down Expand Up @@ -397,7 +340,7 @@ impl<'env> InstallTarget<'env> {
build_options: &BuildOptions,
) -> Result<Node, LockError> {
let dist = package.to_dist(
self.workspace().install_path(),
self.install_path(),
TagPolicy::Required(tags),
build_options,
)?;
Expand All @@ -414,7 +357,7 @@ impl<'env> InstallTarget<'env> {
/// Create a non-installable [`Node`] from a [`Package`].
fn non_installable_node(&self, package: &Package, tags: &Tags) -> Result<Node, LockError> {
let dist = package.to_dist(
self.workspace().install_path(),
self.install_path(),
TagPolicy::Preferred(tags),
&BuildOptions::default(),
)?;
Expand Down
4 changes: 2 additions & 2 deletions crates/uv-resolver/src/lock/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,9 @@ use uv_types::{BuildContext, HashStrategy};
use uv_workspace::Workspace;

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};
Expand All @@ -52,9 +52,9 @@ use crate::{
ResolverOutput,
};

mod installable;
mod map;
mod requirements_txt;
mod target;
mod tree;

/// The current version of the lockfile format.
Expand Down
6 changes: 3 additions & 3 deletions crates/uv-resolver/src/lock/requirements_txt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand All @@ -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,
Expand All @@ -51,7 +51,7 @@ impl<'lock> RequirementsTxtExport<'lock> {
let root = petgraph.add_node(Node::Root);

// Add the workspace packages to the queue.
for root_name in target.packages() {
for root_name in target.roots() {
if prune.contains(root_name) {
continue;
}
Expand Down
3 changes: 2 additions & 1 deletion crates/uv/src/commands/project/add.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -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,
Expand Down
5 changes: 3 additions & 2 deletions crates/uv/src/commands/project/export.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -215,7 +216,7 @@ pub(crate) async fn export(
match format {
ExportFormat::RequirementsTxt => {
let export = RequirementsTxtExport::from_lock(
target,
&target,
&prune,
&extras,
&dev,
Expand Down
83 changes: 83 additions & 0 deletions crates/uv/src/commands/project/install_target.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
use std::path::Path;

use itertools::Either;

use uv_normalize::PackageName;
use uv_resolver::{Installable, Lock, Package};
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<Item = &PackageName> {
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 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,
}
}
}
1 change: 1 addition & 0 deletions crates/uv/src/commands/project/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
7 changes: 4 additions & 3 deletions crates/uv/src/commands/project/remove.rs
Original file line number Diff line number Diff line change
@@ -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::{
Expand All @@ -15,15 +15,16 @@ 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};
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};
Expand Down
3 changes: 2 additions & 1 deletion crates/uv/src/commands/project/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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,
Expand Down
3 changes: 2 additions & 1 deletion crates/uv/src/commands/project/sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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,
Expand Down

0 comments on commit 7c47a45

Please sign in to comment.