From 0b08ba1e673179bb09e83b4adb2ee94471d827a6 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Tue, 26 Mar 2024 15:39:43 -0500 Subject: [PATCH] Rename `uv-traits` and split into separate modules (#2674) This is driving me a little crazy and is becoming a larger problem in #2596 where I need to move more types (like `Upgrade` and `Reinstall`) into this crate. Anything that's shared across our core resolver, install, and build crates needs to be defined in this crate to avoid cyclic dependencies. We've outgrown it being a single file with some shared traits. There are no behavioral changes here. --- Cargo.lock | 19 +- Cargo.toml | 2 +- crates/README.md | 4 +- crates/uv-build/Cargo.toml | 2 +- crates/uv-build/src/lib.rs | 2 +- crates/uv-dev/Cargo.toml | 2 +- crates/uv-dev/src/build.rs | 2 +- crates/uv-dev/src/install_many.rs | 2 +- crates/uv-dev/src/resolve_cli.rs | 2 +- crates/uv-dev/src/resolve_many.rs | 2 +- crates/uv-dispatch/Cargo.toml | 2 +- crates/uv-dispatch/src/lib.rs | 2 +- crates/uv-distribution/Cargo.toml | 2 +- .../src/distribution_database.rs | 2 +- crates/uv-distribution/src/source/mod.rs | 2 +- crates/uv-installer/Cargo.toml | 2 +- crates/uv-installer/src/downloader.rs | 2 +- crates/uv-installer/src/lib.rs | 2 +- crates/uv-installer/src/plan.rs | 2 +- crates/uv-requirements/Cargo.toml | 2 +- crates/uv-requirements/src/resolver.rs | 2 +- crates/uv-requirements/src/source_tree.rs | 2 +- crates/uv-requirements/src/upgrade.rs | 39 +- crates/uv-resolver/Cargo.toml | 2 +- crates/uv-resolver/src/finder.rs | 2 +- crates/uv-resolver/src/resolver/mod.rs | 2 +- crates/uv-resolver/src/resolver/provider.rs | 2 +- crates/uv-resolver/src/version_map.rs | 2 +- crates/uv-resolver/tests/resolver.rs | 2 +- crates/uv-traits/src/lib.rs | 535 ------------------ crates/{uv-traits => uv-types}/Cargo.toml | 3 +- crates/uv-types/src/build_options.rs | 179 ++++++ crates/uv-types/src/config_settings.rs | 176 ++++++ crates/uv-types/src/downloads.rs | 8 + crates/uv-types/src/lib.rs | 14 + crates/uv-types/src/name_specifiers.rs | 63 +++ crates/uv-types/src/package_options.rs | 74 +++ crates/uv-types/src/traits.rs | 129 +++++ crates/uv/Cargo.toml | 2 +- crates/uv/src/commands/pip_compile.rs | 7 +- crates/uv/src/commands/pip_install.rs | 6 +- crates/uv/src/commands/pip_sync.rs | 2 +- crates/uv/src/commands/venv.rs | 2 +- crates/uv/src/main.rs | 6 +- 44 files changed, 696 insertions(+), 624 deletions(-) delete mode 100644 crates/uv-traits/src/lib.rs rename crates/{uv-traits => uv-types}/Cargo.toml (93%) create mode 100644 crates/uv-types/src/build_options.rs create mode 100644 crates/uv-types/src/config_settings.rs create mode 100644 crates/uv-types/src/downloads.rs create mode 100644 crates/uv-types/src/lib.rs create mode 100644 crates/uv-types/src/name_specifiers.rs create mode 100644 crates/uv-types/src/package_options.rs create mode 100644 crates/uv-types/src/traits.rs diff --git a/Cargo.lock b/Cargo.lock index e17c82643..75262925b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4361,7 +4361,7 @@ dependencies = [ "uv-normalize", "uv-requirements", "uv-resolver", - "uv-traits", + "uv-types", "uv-virtualenv", "uv-warnings", ] @@ -4411,7 +4411,7 @@ dependencies = [ "tracing", "uv-fs", "uv-interpreter", - "uv-traits", + "uv-types", "uv-virtualenv", ] @@ -4533,7 +4533,7 @@ dependencies = [ "uv-interpreter", "uv-normalize", "uv-resolver", - "uv-traits", + "uv-types", "walkdir", ] @@ -4554,7 +4554,7 @@ dependencies = [ "uv-installer", "uv-interpreter", "uv-resolver", - "uv-traits", + "uv-types", ] [[package]] @@ -4590,7 +4590,7 @@ dependencies = [ "uv-fs", "uv-git", "uv-normalize", - "uv-traits", + "uv-types", "zip", ] @@ -4684,7 +4684,7 @@ dependencies = [ "uv-fs", "uv-interpreter", "uv-normalize", - "uv-traits", + "uv-types", "uv-warnings", "walkdir", ] @@ -4757,7 +4757,7 @@ dependencies = [ "uv-fs", "uv-normalize", "uv-resolver", - "uv-traits", + "uv-types", "uv-warnings", ] @@ -4801,18 +4801,19 @@ dependencies = [ "uv-distribution", "uv-interpreter", "uv-normalize", - "uv-traits", + "uv-types", "uv-warnings", ] [[package]] -name = "uv-traits" +name = "uv-types" version = "0.0.1" dependencies = [ "anyhow", "distribution-types", "once-map", "pep508_rs", + "rustc-hash", "serde", "serde_json", "uv-cache", diff --git a/Cargo.toml b/Cargo.toml index a0a33ff9c..c7126b7c9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,7 +43,7 @@ uv-interpreter = { path = "crates/uv-interpreter" } uv-normalize = { path = "crates/uv-normalize" } uv-requirements = { path = "crates/uv-requirements" } uv-resolver = { path = "crates/uv-resolver" } -uv-traits = { path = "crates/uv-traits" } +uv-types = { path = "crates/uv-types" } uv-trampoline = { path = "crates/uv-trampoline" } uv-version = { path = "crates/uv-version" } uv-virtualenv = { path = "crates/uv-virtualenv" } diff --git a/crates/README.md b/crates/README.md index a339db4ab..3c3969840 100644 --- a/crates/README.md +++ b/crates/README.md @@ -66,7 +66,7 @@ Development utilities for uv. ## [uv-dispatch](./uv-dispatch) A centralized `struct` for resolving and building source distributions in isolated environments. -Implements the traits defined in `uv-traits`. +Implements the traits defined in `uv-types`. ## [uv-distribution](./uv-distribution) @@ -109,7 +109,7 @@ Utilities for reading package requirements from `pyproject.toml` and `requiremen Functionality for resolving Python packages and their dependencies. -## [uv-traits](./uv-traits) +## [uv-types](./uv-types) Shared traits for uv, to avoid circular dependencies. diff --git a/crates/uv-build/Cargo.toml b/crates/uv-build/Cargo.toml index 8377e1be6..1bfb137d7 100644 --- a/crates/uv-build/Cargo.toml +++ b/crates/uv-build/Cargo.toml @@ -19,7 +19,7 @@ pep440_rs = { workspace = true } pep508_rs = { workspace = true } uv-fs = { workspace = true } uv-interpreter = { workspace = true } -uv-traits = { workspace = true, features = ["serde"] } +uv-types = { workspace = true, features = ["serde"] } uv-virtualenv = { workspace = true } anyhow = { workspace = true } diff --git a/crates/uv-build/src/lib.rs b/crates/uv-build/src/lib.rs index 9ccb4078d..4c287b447 100644 --- a/crates/uv-build/src/lib.rs +++ b/crates/uv-build/src/lib.rs @@ -30,7 +30,7 @@ use pep440_rs::{Version, VersionSpecifiers}; use pep508_rs::{PackageName, Requirement}; use uv_fs::{PythonExt, Simplified}; use uv_interpreter::{Interpreter, PythonEnvironment}; -use uv_traits::{ +use uv_types::{ BuildContext, BuildIsolation, BuildKind, ConfigSettings, SetupPyStrategy, SourceBuildTrait, }; diff --git a/crates/uv-dev/Cargo.toml b/crates/uv-dev/Cargo.toml index 8072278e5..0759a5f49 100644 --- a/crates/uv-dev/Cargo.toml +++ b/crates/uv-dev/Cargo.toml @@ -31,7 +31,7 @@ uv-installer = { workspace = true } uv-interpreter = { workspace = true } uv-normalize = { workspace = true } uv-resolver = { workspace = true } -uv-traits = { workspace = true } +uv-types = { workspace = true } # Any dependencies that are exclusively used in `uv-dev` should be listed as non-workspace # dependencies, to ensure that we're forced to think twice before including them in other crates. diff --git a/crates/uv-dev/src/build.rs b/crates/uv-dev/src/build.rs index 0644e5175..5d8efd71e 100644 --- a/crates/uv-dev/src/build.rs +++ b/crates/uv-dev/src/build.rs @@ -14,7 +14,7 @@ use uv_dispatch::BuildDispatch; use uv_installer::NoBinary; use uv_interpreter::PythonEnvironment; use uv_resolver::InMemoryIndex; -use uv_traits::{ +use uv_types::{ BuildContext, BuildIsolation, BuildKind, ConfigSettings, InFlight, NoBuild, SetupPyStrategy, }; diff --git a/crates/uv-dev/src/install_many.rs b/crates/uv-dev/src/install_many.rs index a3bc1dab2..e05b2e078 100644 --- a/crates/uv-dev/src/install_many.rs +++ b/crates/uv-dev/src/install_many.rs @@ -23,7 +23,7 @@ use uv_installer::{Downloader, NoBinary}; use uv_interpreter::PythonEnvironment; use uv_normalize::PackageName; use uv_resolver::{DistFinder, InMemoryIndex}; -use uv_traits::{BuildContext, BuildIsolation, ConfigSettings, InFlight, NoBuild, SetupPyStrategy}; +use uv_types::{BuildContext, BuildIsolation, ConfigSettings, InFlight, NoBuild, SetupPyStrategy}; #[derive(Parser)] pub(crate) struct InstallManyArgs { diff --git a/crates/uv-dev/src/resolve_cli.rs b/crates/uv-dev/src/resolve_cli.rs index eff8ea4ec..6c5c8bfc0 100644 --- a/crates/uv-dev/src/resolve_cli.rs +++ b/crates/uv-dev/src/resolve_cli.rs @@ -17,7 +17,7 @@ use uv_dispatch::BuildDispatch; use uv_installer::NoBinary; use uv_interpreter::PythonEnvironment; use uv_resolver::{InMemoryIndex, Manifest, Options, Resolver}; -use uv_traits::{BuildIsolation, ConfigSettings, InFlight, NoBuild, SetupPyStrategy}; +use uv_types::{BuildIsolation, ConfigSettings, InFlight, NoBuild, SetupPyStrategy}; #[derive(ValueEnum, Default, Clone)] pub(crate) enum ResolveCliFormat { diff --git a/crates/uv-dev/src/resolve_many.rs b/crates/uv-dev/src/resolve_many.rs index 8bd0b14a7..88325d5ee 100644 --- a/crates/uv-dev/src/resolve_many.rs +++ b/crates/uv-dev/src/resolve_many.rs @@ -20,7 +20,7 @@ use uv_installer::NoBinary; use uv_interpreter::PythonEnvironment; use uv_normalize::PackageName; use uv_resolver::InMemoryIndex; -use uv_traits::{BuildContext, BuildIsolation, ConfigSettings, InFlight, NoBuild, SetupPyStrategy}; +use uv_types::{BuildContext, BuildIsolation, ConfigSettings, InFlight, NoBuild, SetupPyStrategy}; #[derive(Parser)] pub(crate) struct ResolveManyArgs { diff --git a/crates/uv-dispatch/Cargo.toml b/crates/uv-dispatch/Cargo.toml index 8dce3a793..b2fa8d084 100644 --- a/crates/uv-dispatch/Cargo.toml +++ b/crates/uv-dispatch/Cargo.toml @@ -22,7 +22,7 @@ uv-client = { workspace = true } uv-installer = { workspace = true } uv-interpreter = { workspace = true } uv-resolver = { workspace = true } -uv-traits = { workspace = true } +uv-types = { workspace = true } anyhow = { workspace = true } futures = { workspace = true } diff --git a/crates/uv-dispatch/src/lib.rs b/crates/uv-dispatch/src/lib.rs index 047b3cd41..08961e0ed 100644 --- a/crates/uv-dispatch/src/lib.rs +++ b/crates/uv-dispatch/src/lib.rs @@ -20,7 +20,7 @@ use uv_client::{FlatIndex, RegistryClient}; use uv_installer::{Downloader, Installer, NoBinary, Plan, Planner, Reinstall, SitePackages}; use uv_interpreter::{Interpreter, PythonEnvironment}; use uv_resolver::{InMemoryIndex, Manifest, Options, Resolver}; -use uv_traits::{ +use uv_types::{ BuildContext, BuildIsolation, BuildKind, ConfigSettings, InFlight, NoBuild, SetupPyStrategy, }; diff --git a/crates/uv-distribution/Cargo.toml b/crates/uv-distribution/Cargo.toml index c3578de3b..d235b8d46 100644 --- a/crates/uv-distribution/Cargo.toml +++ b/crates/uv-distribution/Cargo.toml @@ -26,7 +26,7 @@ uv-extract = { workspace = true } uv-fs = { workspace = true, features = ["tokio"] } uv-git = { workspace = true, features = ["vendored-openssl"] } uv-normalize = { workspace = true } -uv-traits = { workspace = true } +uv-types = { workspace = true } pypi-types = { workspace = true } anyhow = { workspace = true } diff --git a/crates/uv-distribution/src/distribution_database.rs b/crates/uv-distribution/src/distribution_database.rs index ead8afb97..f818c0493 100644 --- a/crates/uv-distribution/src/distribution_database.rs +++ b/crates/uv-distribution/src/distribution_database.rs @@ -19,7 +19,7 @@ use pypi_types::Metadata23; use uv_cache::{ArchiveTarget, ArchiveTimestamp, Cache, CacheBucket, CacheEntry, WheelCache}; use uv_client::{CacheControl, CachedClientError, Connectivity, RegistryClient}; use uv_git::GitSource; -use uv_traits::{BuildContext, NoBinary, NoBuild}; +use uv_types::{BuildContext, NoBinary, NoBuild}; use crate::download::{BuiltWheel, UnzippedWheel}; use crate::locks::Locks; diff --git a/crates/uv-distribution/src/source/mod.rs b/crates/uv-distribution/src/source/mod.rs index 6507d4272..78689c401 100644 --- a/crates/uv-distribution/src/source/mod.rs +++ b/crates/uv-distribution/src/source/mod.rs @@ -33,7 +33,7 @@ use uv_client::{ }; use uv_fs::{write_atomic, LockedFile}; use uv_git::{Fetch, GitSource}; -use uv_traits::{BuildContext, BuildKind, NoBuild, SourceBuildTrait}; +use uv_types::{BuildContext, BuildKind, NoBuild, SourceBuildTrait}; use crate::error::Error; use crate::reporter::Facade; diff --git a/crates/uv-installer/Cargo.toml b/crates/uv-installer/Cargo.toml index a57891db8..97ca51abc 100644 --- a/crates/uv-installer/Cargo.toml +++ b/crates/uv-installer/Cargo.toml @@ -27,7 +27,7 @@ uv-extract = { workspace = true } uv-fs = { workspace = true } uv-interpreter = { workspace = true } uv-normalize = { workspace = true } -uv-traits = { workspace = true } +uv-types = { workspace = true } uv-warnings = { workspace = true } anyhow = { workspace = true } diff --git a/crates/uv-installer/src/downloader.rs b/crates/uv-installer/src/downloader.rs index defaefe14..8e140d241 100644 --- a/crates/uv-installer/src/downloader.rs +++ b/crates/uv-installer/src/downloader.rs @@ -15,7 +15,7 @@ use platform_tags::Tags; use uv_cache::Cache; use uv_client::RegistryClient; use uv_distribution::{DistributionDatabase, LocalWheel, Unzip}; -use uv_traits::{BuildContext, InFlight}; +use uv_types::{BuildContext, InFlight}; use crate::editable::BuiltEditable; diff --git a/crates/uv-installer/src/lib.rs b/crates/uv-installer/src/lib.rs index 08ed38c9d..1bd883960 100644 --- a/crates/uv-installer/src/lib.rs +++ b/crates/uv-installer/src/lib.rs @@ -5,7 +5,7 @@ pub use installer::{Installer, Reporter as InstallReporter}; pub use plan::{Plan, Planner, Reinstall}; pub use site_packages::{Diagnostic, SitePackages}; pub use uninstall::{uninstall, UninstallError}; -pub use uv_traits::NoBinary; +pub use uv_types::NoBinary; mod compile; mod downloader; diff --git a/crates/uv-installer/src/plan.rs b/crates/uv-installer/src/plan.rs index 857d95ba7..aeb8cd0aa 100644 --- a/crates/uv-installer/src/plan.rs +++ b/crates/uv-installer/src/plan.rs @@ -17,7 +17,7 @@ use uv_distribution::{BuiltWheelIndex, RegistryWheelIndex}; use uv_fs::Simplified; use uv_interpreter::PythonEnvironment; use uv_normalize::PackageName; -use uv_traits::NoBinary; +use uv_types::NoBinary; use crate::{ResolvedEditable, SitePackages}; diff --git a/crates/uv-requirements/Cargo.toml b/crates/uv-requirements/Cargo.toml index f2395a210..4a080a958 100644 --- a/crates/uv-requirements/Cargo.toml +++ b/crates/uv-requirements/Cargo.toml @@ -21,7 +21,7 @@ uv-distribution = { workspace = true } uv-fs = { workspace = true } uv-normalize = { workspace = true } uv-resolver = { workspace = true, features = ["clap"] } -uv-traits = { workspace = true } +uv-types = { workspace = true } uv-warnings = { workspace = true } anyhow = { workspace = true } diff --git a/crates/uv-requirements/src/resolver.rs b/crates/uv-requirements/src/resolver.rs index 639b8c891..373983966 100644 --- a/crates/uv-requirements/src/resolver.rs +++ b/crates/uv-requirements/src/resolver.rs @@ -20,7 +20,7 @@ use pypi_types::Metadata10; use uv_client::RegistryClient; use uv_distribution::{Reporter, SourceDistCachedBuilder}; use uv_normalize::PackageName; -use uv_traits::BuildContext; +use uv_types::BuildContext; /// Like [`RequirementsSpecification`], but with concrete names for all requirements. pub struct NamedRequirementsResolver { diff --git a/crates/uv-requirements/src/source_tree.rs b/crates/uv-requirements/src/source_tree.rs index f06da1c23..3fb0156cd 100644 --- a/crates/uv-requirements/src/source_tree.rs +++ b/crates/uv-requirements/src/source_tree.rs @@ -10,7 +10,7 @@ use distribution_types::{BuildableSource, PathSourceUrl, SourceUrl}; use pep508_rs::Requirement; use uv_client::RegistryClient; use uv_distribution::{Reporter, SourceDistCachedBuilder}; -use uv_traits::BuildContext; +use uv_types::BuildContext; use crate::ExtrasSpecification; diff --git a/crates/uv-requirements/src/upgrade.rs b/crates/uv-requirements/src/upgrade.rs index fc4f62f41..49a1cc1e3 100644 --- a/crates/uv-requirements/src/upgrade.rs +++ b/crates/uv-requirements/src/upgrade.rs @@ -1,48 +1,11 @@ use std::path::Path; use anyhow::Result; -use rustc_hash::FxHashSet; use requirements_txt::RequirementsTxt; use uv_client::{BaseClientBuilder, Connectivity}; -use uv_normalize::PackageName; use uv_resolver::{Preference, PreferenceError}; - -/// Whether to allow package upgrades. -#[derive(Debug)] -pub enum Upgrade { - /// Prefer pinned versions from the existing lockfile, if possible. - None, - - /// Allow package upgrades for all packages, ignoring the existing lockfile. - All, - - /// Allow package upgrades, but only for the specified packages. - Packages(FxHashSet), -} - -impl Upgrade { - /// Determine the upgrade strategy from the command-line arguments. - pub fn from_args(upgrade: bool, upgrade_package: Vec) -> Self { - if upgrade { - Self::All - } else if !upgrade_package.is_empty() { - Self::Packages(upgrade_package.into_iter().collect()) - } else { - Self::None - } - } - - /// Returns `true` if no packages should be upgraded. - pub fn is_none(&self) -> bool { - matches!(self, Self::None) - } - - /// Returns `true` if all packages should be upgraded. - pub fn is_all(&self) -> bool { - matches!(self, Self::All) - } -} +use uv_types::Upgrade; /// Load the preferred requirements from an existing lockfile, applying the upgrade strategy. pub async fn read_lockfile( diff --git a/crates/uv-resolver/Cargo.toml b/crates/uv-resolver/Cargo.toml index b6a99c38b..de2d97827 100644 --- a/crates/uv-resolver/Cargo.toml +++ b/crates/uv-resolver/Cargo.toml @@ -27,7 +27,7 @@ uv-client = { workspace = true } uv-distribution = { workspace = true } uv-interpreter = { workspace = true } uv-normalize = { workspace = true } -uv-traits = { workspace = true } +uv-types = { workspace = true } uv-warnings = { workspace = true } anstream = { workspace = true } diff --git a/crates/uv-resolver/src/finder.rs b/crates/uv-resolver/src/finder.rs index c6f6770a3..b0a428af0 100644 --- a/crates/uv-resolver/src/finder.rs +++ b/crates/uv-resolver/src/finder.rs @@ -5,7 +5,7 @@ use anyhow::Result; use futures::{stream, Stream, StreamExt, TryStreamExt}; use rustc_hash::FxHashMap; -use uv_traits::{NoBinary, NoBuild}; +use uv_types::{NoBinary, NoBuild}; use distribution_filename::DistFilename; use distribution_types::{Dist, IndexUrl, Resolution}; diff --git a/crates/uv-resolver/src/resolver/mod.rs b/crates/uv-resolver/src/resolver/mod.rs index ee829ccf7..229ef6489 100644 --- a/crates/uv-resolver/src/resolver/mod.rs +++ b/crates/uv-resolver/src/resolver/mod.rs @@ -31,7 +31,7 @@ use uv_client::{FlatIndex, RegistryClient}; use uv_distribution::DistributionDatabase; use uv_interpreter::Interpreter; use uv_normalize::PackageName; -use uv_traits::BuildContext; +use uv_types::BuildContext; use crate::candidate_selector::{CandidateDist, CandidateSelector}; use crate::constraints::Constraints; diff --git a/crates/uv-resolver/src/resolver/provider.rs b/crates/uv-resolver/src/resolver/provider.rs index 2a114fae7..8f7748344 100644 --- a/crates/uv-resolver/src/resolver/provider.rs +++ b/crates/uv-resolver/src/resolver/provider.rs @@ -10,7 +10,7 @@ use pypi_types::Metadata23; use uv_client::{FlatIndex, RegistryClient}; use uv_distribution::DistributionDatabase; use uv_normalize::PackageName; -use uv_traits::{BuildContext, NoBinary, NoBuild}; +use uv_types::{BuildContext, NoBinary, NoBuild}; use crate::python_requirement::PythonRequirement; use crate::version_map::VersionMap; diff --git a/crates/uv-resolver/src/version_map.rs b/crates/uv-resolver/src/version_map.rs index 0b269dbc1..c58b4e8b0 100644 --- a/crates/uv-resolver/src/version_map.rs +++ b/crates/uv-resolver/src/version_map.rs @@ -16,7 +16,7 @@ use pypi_types::{Hashes, Yanked}; use rkyv::{de::deserializers::SharedDeserializeMap, Deserialize}; use uv_client::{FlatDistributions, OwnedArchive, SimpleMetadata, VersionFiles}; use uv_normalize::PackageName; -use uv_traits::{NoBinary, NoBuild}; +use uv_types::{NoBinary, NoBuild}; use uv_warnings::warn_user_once; use crate::{python_requirement::PythonRequirement, yanks::AllowedYanks}; diff --git a/crates/uv-resolver/tests/resolver.rs b/crates/uv-resolver/tests/resolver.rs index 939f4b0c1..5ef1c5cca 100644 --- a/crates/uv-resolver/tests/resolver.rs +++ b/crates/uv-resolver/tests/resolver.rs @@ -20,7 +20,7 @@ use uv_resolver::{ DisplayResolutionGraph, InMemoryIndex, Manifest, Options, OptionsBuilder, PreReleaseMode, Preference, ResolutionGraph, ResolutionMode, Resolver, }; -use uv_traits::{ +use uv_types::{ BuildContext, BuildIsolation, BuildKind, NoBinary, NoBuild, SetupPyStrategy, SourceBuildTrait, }; diff --git a/crates/uv-traits/src/lib.rs b/crates/uv-traits/src/lib.rs deleted file mode 100644 index 335195c93..000000000 --- a/crates/uv-traits/src/lib.rs +++ /dev/null @@ -1,535 +0,0 @@ -//! Avoid cyclic crate dependencies between resolver, installer and builder. - -use std::collections::btree_map::Entry; -use std::collections::BTreeMap; -use std::fmt::{Display, Formatter}; -use std::future::Future; -use std::path::{Path, PathBuf}; -use std::str::FromStr; - -use anyhow::Result; - -use distribution_types::{CachedDist, DistributionId, IndexLocations, Resolution, SourceDist}; -use once_map::OnceMap; -use pep508_rs::Requirement; -use uv_cache::Cache; -use uv_interpreter::{Interpreter, PythonEnvironment}; -use uv_normalize::PackageName; - -/// Avoid cyclic crate dependencies between resolver, installer and builder. -/// -/// To resolve the dependencies of a packages, we may need to build one or more source -/// distributions. To building a source distribution, we need to create a virtual environment from -/// the same base python as we use for the root resolution, resolve the build requirements -/// (potentially which nested source distributions, recursing a level deeper), installing -/// them and then build. The installer, the resolver and the source distribution builder are each in -/// their own crate. To avoid circular crate dependencies, this type dispatches between the three -/// crates with its three main methods ([`BuildContext::resolve`], [`BuildContext::install`] and -/// [`BuildContext::setup_build`]). -/// -/// The overall main crate structure looks like this: -/// -/// ```text -/// ┌────────────────┐ -/// │ uv │ -/// └───────▲────────┘ -/// │ -/// │ -/// ┌───────┴────────┐ -/// ┌─────────►│ uv-dispatch │◄─────────┐ -/// │ └───────▲────────┘ │ -/// │ │ │ -/// │ │ │ -/// ┌───────┴────────┐ ┌───────┴────────┐ ┌────────┴───────┐ -/// │ uv-resolver │ │ uv-installer │ │ uv-build │ -/// └───────▲────────┘ └───────▲────────┘ └────────▲───────┘ -/// │ │ │ -/// └─────────────┐ │ ┌──────────────┘ -/// ┌──┴────┴────┴───┐ -/// │ uv-traits │ -/// └────────────────┘ -/// ``` -/// -/// Put in a different way, this trait allows `uv-resolver` to depend on `uv-build` and -/// `uv-build` to depend on `uv-resolver` which having actual crate dependencies between -/// them. - -pub trait BuildContext: Sync { - type SourceDistBuilder: SourceBuildTrait + Send + Sync; - - /// Return a reference to the cache. - fn cache(&self) -> &Cache; - - /// All (potentially nested) source distribution builds use the same base python and can reuse - /// it's metadata (e.g. wheel compatibility tags). - fn interpreter(&self) -> &Interpreter; - - /// Whether to enforce build isolation when building source distributions. - fn build_isolation(&self) -> BuildIsolation; - - /// Whether source distribution building is disabled. This [`BuildContext::setup_build`] calls - /// will fail in this case. This method exists to avoid fetching source distributions if we know - /// we can't build them - fn no_build(&self) -> &NoBuild; - - /// Whether using pre-built wheels is disabled. - fn no_binary(&self) -> &NoBinary; - - /// The index locations being searched. - fn index_locations(&self) -> &IndexLocations; - - /// The strategy to use when building source distributions that lack a `pyproject.toml`. - fn setup_py_strategy(&self) -> SetupPyStrategy; - - /// Resolve the given requirements into a ready-to-install set of package versions. - fn resolve<'a>( - &'a self, - requirements: &'a [Requirement], - ) -> impl Future> + Send + 'a; - - /// Install the given set of package versions into the virtual environment. The environment must - /// use the same base Python as [`BuildContext::interpreter`] - fn install<'a>( - &'a self, - resolution: &'a Resolution, - venv: &'a PythonEnvironment, - ) -> impl Future> + Send + 'a; - - /// Setup a source distribution build by installing the required dependencies. A wrapper for - /// `uv_build::SourceBuild::setup`. - /// - /// For PEP 517 builds, this calls `get_requires_for_build_wheel`. - /// - /// `package_id` is for error reporting only. - /// `dist` is for safety checks and may be null for editable builds. - fn setup_build<'a>( - &'a self, - source: &'a Path, - subdirectory: Option<&'a Path>, - package_id: &'a str, - dist: Option<&'a SourceDist>, - build_kind: BuildKind, - ) -> impl Future> + Send + 'a; -} - -/// A wrapper for `uv_build::SourceBuild` to avoid cyclical crate dependencies. -/// -/// You can either call only `wheel()` to build the wheel directly, call only `metadata()` to get -/// the metadata without performing the actual or first call `metadata()` and then `wheel()`. -pub trait SourceBuildTrait { - /// A wrapper for `uv_build::SourceBuild::get_metadata_without_build`. - /// - /// For PEP 517 builds, this calls `prepare_metadata_for_build_wheel` - /// - /// Returns the metadata directory if we're having a PEP 517 build and the - /// `prepare_metadata_for_build_wheel` hook exists - fn metadata(&mut self) -> impl Future>> + Send; - - /// A wrapper for `uv_build::SourceBuild::build`. - /// - /// For PEP 517 builds, this calls `build_wheel`. - /// - /// Returns the filename of the built wheel inside the given `wheel_dir`. - fn wheel<'a>(&'a self, wheel_dir: &'a Path) - -> impl Future> + Send + 'a; -} - -#[derive(Default)] -pub struct InFlight { - /// The in-flight distribution downloads. - pub downloads: OnceMap>, -} - -/// Whether to enforce build isolation when building source distributions. -#[derive(Debug, Copy, Clone)] -pub enum BuildIsolation<'a> { - Isolated, - Shared(&'a PythonEnvironment), -} - -impl<'a> BuildIsolation<'a> { - /// Returns `true` if build isolation is enforced. - pub fn is_isolated(&self) -> bool { - matches!(self, Self::Isolated) - } -} - -/// The strategy to use when building source distributions that lack a `pyproject.toml`. -#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] -pub enum SetupPyStrategy { - /// Perform a PEP 517 build. - #[default] - Pep517, - /// Perform a build by invoking `setuptools` directly. - Setuptools, -} - -#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] -pub enum BuildKind { - /// A regular PEP 517 wheel build - #[default] - Wheel, - /// A PEP 660 editable installation wheel build - Editable, -} - -impl Display for BuildKind { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - Self::Wheel => f.write_str("wheel"), - Self::Editable => f.write_str("editable"), - } - } -} - -#[derive(Debug, Clone)] -pub enum PackageNameSpecifier { - All, - None, - Package(PackageName), -} - -impl FromStr for PackageNameSpecifier { - type Err = uv_normalize::InvalidNameError; - - fn from_str(name: &str) -> Result { - match name { - ":all:" => Ok(Self::All), - ":none:" => Ok(Self::None), - _ => Ok(Self::Package(PackageName::from_str(name)?)), - } - } -} - -#[derive(Debug, Clone)] -pub enum PackageNameSpecifiers { - All, - None, - Packages(Vec), -} - -impl PackageNameSpecifiers { - fn from_iter(specifiers: impl Iterator) -> Self { - let mut packages = Vec::new(); - let mut all: bool = false; - - for specifier in specifiers { - match specifier { - PackageNameSpecifier::None => { - packages.clear(); - all = false; - } - PackageNameSpecifier::All => { - all = true; - } - PackageNameSpecifier::Package(name) => { - packages.push(name); - } - } - } - - if all { - Self::All - } else if packages.is_empty() { - Self::None - } else { - Self::Packages(packages) - } - } -} - -#[derive(Debug, Clone)] -pub enum NoBinary { - /// Allow installation of any wheel. - None, - - /// Do not allow installation from any wheels. - All, - - /// Do not allow installation from the specific wheels. - Packages(Vec), -} - -impl NoBinary { - /// Determine the binary installation strategy to use. - pub fn from_args(no_binary: Vec) -> Self { - let combined = PackageNameSpecifiers::from_iter(no_binary.into_iter()); - match combined { - PackageNameSpecifiers::All => Self::All, - PackageNameSpecifiers::None => Self::None, - PackageNameSpecifiers::Packages(packages) => Self::Packages(packages), - } - } -} - -impl NoBinary { - /// Returns `true` if all wheels are allowed. - pub fn is_none(&self) -> bool { - matches!(self, Self::None) - } -} - -#[derive(Debug, Clone, PartialEq)] -pub enum NoBuild { - /// Allow building wheels from any source distribution. - None, - - /// Do not allow building wheels from any source distribution. - All, - - /// Do not allow building wheels from the given package's source distributions. - Packages(Vec), -} - -impl NoBuild { - /// Determine the build strategy to use. - pub fn from_args(only_binary: Vec, no_build: bool) -> Self { - if no_build { - Self::All - } else { - let combined = PackageNameSpecifiers::from_iter(only_binary.into_iter()); - match combined { - PackageNameSpecifiers::All => Self::All, - PackageNameSpecifiers::None => Self::None, - PackageNameSpecifiers::Packages(packages) => Self::Packages(packages), - } - } - } -} - -impl NoBuild { - /// Returns `true` if all builds are allowed. - pub fn is_none(&self) -> bool { - matches!(self, Self::None) - } -} - -#[derive(Debug, Clone)] -pub struct ConfigSettingEntry { - /// The key of the setting. For example, given `key=value`, this would be `key`. - key: String, - /// The value of the setting. For example, given `key=value`, this would be `value`. - value: String, -} - -impl FromStr for ConfigSettingEntry { - type Err = anyhow::Error; - - fn from_str(s: &str) -> Result { - let Some((key, value)) = s.split_once('=') else { - return Err(anyhow::anyhow!( - "Invalid config setting: {s} (expected `KEY=VALUE`)" - )); - }; - Ok(Self { - key: key.trim().to_string(), - value: value.trim().to_string(), - }) - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -enum ConfigSettingValue { - /// The value consists of a single string. - String(String), - /// The value consists of a list of strings. - List(Vec), -} - -/// Settings to pass to a PEP 517 build backend, structured as a map from (string) key to string or -/// list of strings. -/// -/// See: -#[derive(Debug, Default, Clone)] -pub struct ConfigSettings(BTreeMap); - -impl FromIterator for ConfigSettings { - fn from_iter>(iter: T) -> Self { - let mut config = BTreeMap::default(); - for entry in iter { - match config.entry(entry.key) { - Entry::Vacant(vacant) => { - vacant.insert(ConfigSettingValue::String(entry.value)); - } - Entry::Occupied(mut occupied) => match occupied.get_mut() { - ConfigSettingValue::String(existing) => { - let existing = existing.clone(); - occupied.insert(ConfigSettingValue::List(vec![existing, entry.value])); - } - ConfigSettingValue::List(existing) => { - existing.push(entry.value); - } - }, - } - } - Self(config) - } -} - -#[cfg(feature = "serde")] -impl ConfigSettings { - /// Convert the settings to a string that can be passed directly to a PEP 517 build backend. - pub fn escape_for_python(&self) -> String { - serde_json::to_string(self).expect("Failed to serialize config settings") - } -} - -#[cfg(feature = "serde")] -impl serde::Serialize for ConfigSettings { - fn serialize(&self, serializer: S) -> Result { - use serde::ser::SerializeMap; - - let mut map = serializer.serialize_map(Some(self.0.len()))?; - for (key, value) in &self.0 { - match value { - ConfigSettingValue::String(value) => { - map.serialize_entry(&key, &value)?; - } - ConfigSettingValue::List(values) => { - map.serialize_entry(&key, &values)?; - } - } - } - map.end() - } -} - -#[cfg(test)] -mod tests { - use anyhow::Error; - - use super::*; - - #[test] - fn no_build_from_args() -> Result<(), Error> { - assert_eq!( - NoBuild::from_args(vec![PackageNameSpecifier::from_str(":all:")?], false), - NoBuild::All, - ); - assert_eq!( - NoBuild::from_args(vec![PackageNameSpecifier::from_str(":all:")?], true), - NoBuild::All, - ); - assert_eq!( - NoBuild::from_args(vec![PackageNameSpecifier::from_str(":none:")?], true), - NoBuild::All, - ); - assert_eq!( - NoBuild::from_args(vec![PackageNameSpecifier::from_str(":none:")?], false), - NoBuild::None, - ); - assert_eq!( - NoBuild::from_args( - vec![ - PackageNameSpecifier::from_str("foo")?, - PackageNameSpecifier::from_str("bar")? - ], - false - ), - NoBuild::Packages(vec![ - PackageName::from_str("foo")?, - PackageName::from_str("bar")? - ]), - ); - assert_eq!( - NoBuild::from_args( - vec![ - PackageNameSpecifier::from_str("test")?, - PackageNameSpecifier::All - ], - false - ), - NoBuild::All, - ); - assert_eq!( - NoBuild::from_args( - vec![ - PackageNameSpecifier::from_str("foo")?, - PackageNameSpecifier::from_str(":none:")?, - PackageNameSpecifier::from_str("bar")? - ], - false - ), - NoBuild::Packages(vec![PackageName::from_str("bar")?]), - ); - - Ok(()) - } - - #[test] - fn collect_config_settings() { - let settings: ConfigSettings = vec![ - ConfigSettingEntry { - key: "key".to_string(), - value: "value".to_string(), - }, - ConfigSettingEntry { - key: "key".to_string(), - value: "value2".to_string(), - }, - ConfigSettingEntry { - key: "list".to_string(), - value: "value3".to_string(), - }, - ConfigSettingEntry { - key: "list".to_string(), - value: "value4".to_string(), - }, - ] - .into_iter() - .collect(); - assert_eq!( - settings.0.get("key"), - Some(&ConfigSettingValue::List(vec![ - "value".to_string(), - "value2".to_string() - ])) - ); - assert_eq!( - settings.0.get("list"), - Some(&ConfigSettingValue::List(vec![ - "value3".to_string(), - "value4".to_string() - ])) - ); - } - - #[test] - #[cfg(feature = "serde")] - fn escape_for_python() { - let mut settings = ConfigSettings::default(); - settings.0.insert( - "key".to_string(), - ConfigSettingValue::String("value".to_string()), - ); - settings.0.insert( - "list".to_string(), - ConfigSettingValue::List(vec!["value1".to_string(), "value2".to_string()]), - ); - assert_eq!( - settings.escape_for_python(), - r#"{"key":"value","list":["value1","value2"]}"# - ); - - let mut settings = ConfigSettings::default(); - settings.0.insert( - "key".to_string(), - ConfigSettingValue::String("Hello, \"world!\"".to_string()), - ); - settings.0.insert( - "list".to_string(), - ConfigSettingValue::List(vec!["'value1'".to_string()]), - ); - assert_eq!( - settings.escape_for_python(), - r#"{"key":"Hello, \"world!\"","list":["'value1'"]}"# - ); - - let mut settings = ConfigSettings::default(); - settings.0.insert( - "key".to_string(), - ConfigSettingValue::String("val\\1 {}ue".to_string()), - ); - assert_eq!(settings.escape_for_python(), r#"{"key":"val\\1 {}ue"}"#); - } -} diff --git a/crates/uv-traits/Cargo.toml b/crates/uv-types/Cargo.toml similarity index 93% rename from crates/uv-traits/Cargo.toml rename to crates/uv-types/Cargo.toml index 9a8cc869e..7822cc1f6 100644 --- a/crates/uv-traits/Cargo.toml +++ b/crates/uv-types/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "uv-traits" +name = "uv-types" version = "0.0.1" edition = { workspace = true } rust-version = { workspace = true } @@ -21,6 +21,7 @@ uv-interpreter = { workspace = true } uv-normalize = { workspace = true } anyhow = { workspace = true } +rustc-hash = { workspace = true } serde = { workspace = true, optional = true } serde_json = { workspace = true, optional = true } diff --git a/crates/uv-types/src/build_options.rs b/crates/uv-types/src/build_options.rs new file mode 100644 index 000000000..b9b5b8481 --- /dev/null +++ b/crates/uv-types/src/build_options.rs @@ -0,0 +1,179 @@ +use std::fmt::{Display, Formatter}; + +use pep508_rs::PackageName; +use uv_interpreter::PythonEnvironment; + +use crate::{PackageNameSpecifier, PackageNameSpecifiers}; + +/// Whether to enforce build isolation when building source distributions. +#[derive(Debug, Copy, Clone)] +pub enum BuildIsolation<'a> { + Isolated, + Shared(&'a PythonEnvironment), +} + +impl<'a> BuildIsolation<'a> { + /// Returns `true` if build isolation is enforced. + pub fn is_isolated(&self) -> bool { + matches!(self, Self::Isolated) + } +} + +/// The strategy to use when building source distributions that lack a `pyproject.toml`. +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] +pub enum SetupPyStrategy { + /// Perform a PEP 517 build. + #[default] + Pep517, + /// Perform a build by invoking `setuptools` directly. + Setuptools, +} + +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] +pub enum BuildKind { + /// A regular PEP 517 wheel build + #[default] + Wheel, + /// A PEP 660 editable installation wheel build + Editable, +} + +impl Display for BuildKind { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Self::Wheel => f.write_str("wheel"), + Self::Editable => f.write_str("editable"), + } + } +} + +#[derive(Debug, Clone)] +pub enum NoBinary { + /// Allow installation of any wheel. + None, + + /// Do not allow installation from any wheels. + All, + + /// Do not allow installation from the specific wheels. + Packages(Vec), +} + +impl NoBinary { + /// Determine the binary installation strategy to use. + pub fn from_args(no_binary: Vec) -> Self { + let combined = PackageNameSpecifiers::from_iter(no_binary.into_iter()); + match combined { + PackageNameSpecifiers::All => Self::All, + PackageNameSpecifiers::None => Self::None, + PackageNameSpecifiers::Packages(packages) => Self::Packages(packages), + } + } +} + +impl NoBinary { + /// Returns `true` if all wheels are allowed. + pub fn is_none(&self) -> bool { + matches!(self, Self::None) + } +} + +#[derive(Debug, Clone, PartialEq)] +pub enum NoBuild { + /// Allow building wheels from any source distribution. + None, + + /// Do not allow building wheels from any source distribution. + All, + + /// Do not allow building wheels from the given package's source distributions. + Packages(Vec), +} + +impl NoBuild { + /// Determine the build strategy to use. + pub fn from_args(only_binary: Vec, no_build: bool) -> Self { + if no_build { + Self::All + } else { + let combined = PackageNameSpecifiers::from_iter(only_binary.into_iter()); + match combined { + PackageNameSpecifiers::All => Self::All, + PackageNameSpecifiers::None => Self::None, + PackageNameSpecifiers::Packages(packages) => Self::Packages(packages), + } + } + } +} + +impl NoBuild { + /// Returns `true` if all builds are allowed. + pub fn is_none(&self) -> bool { + matches!(self, Self::None) + } +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use anyhow::Error; + + use super::*; + + #[test] + fn no_build_from_args() -> Result<(), Error> { + assert_eq!( + NoBuild::from_args(vec![PackageNameSpecifier::from_str(":all:")?], false), + NoBuild::All, + ); + assert_eq!( + NoBuild::from_args(vec![PackageNameSpecifier::from_str(":all:")?], true), + NoBuild::All, + ); + assert_eq!( + NoBuild::from_args(vec![PackageNameSpecifier::from_str(":none:")?], true), + NoBuild::All, + ); + assert_eq!( + NoBuild::from_args(vec![PackageNameSpecifier::from_str(":none:")?], false), + NoBuild::None, + ); + assert_eq!( + NoBuild::from_args( + vec![ + PackageNameSpecifier::from_str("foo")?, + PackageNameSpecifier::from_str("bar")? + ], + false + ), + NoBuild::Packages(vec![ + PackageName::from_str("foo")?, + PackageName::from_str("bar")? + ]), + ); + assert_eq!( + NoBuild::from_args( + vec![ + PackageNameSpecifier::from_str("test")?, + PackageNameSpecifier::All + ], + false + ), + NoBuild::All, + ); + assert_eq!( + NoBuild::from_args( + vec![ + PackageNameSpecifier::from_str("foo")?, + PackageNameSpecifier::from_str(":none:")?, + PackageNameSpecifier::from_str("bar")? + ], + false + ), + NoBuild::Packages(vec![PackageName::from_str("bar")?]), + ); + + Ok(()) + } +} diff --git a/crates/uv-types/src/config_settings.rs b/crates/uv-types/src/config_settings.rs new file mode 100644 index 000000000..7b0729aeb --- /dev/null +++ b/crates/uv-types/src/config_settings.rs @@ -0,0 +1,176 @@ +use std::{ + collections::{btree_map::Entry, BTreeMap}, + str::FromStr, +}; + +#[derive(Debug, Clone)] +pub struct ConfigSettingEntry { + /// The key of the setting. For example, given `key=value`, this would be `key`. + key: String, + /// The value of the setting. For example, given `key=value`, this would be `value`. + value: String, +} + +impl FromStr for ConfigSettingEntry { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + let Some((key, value)) = s.split_once('=') else { + return Err(anyhow::anyhow!( + "Invalid config setting: {s} (expected `KEY=VALUE`)" + )); + }; + Ok(Self { + key: key.trim().to_string(), + value: value.trim().to_string(), + }) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +enum ConfigSettingValue { + /// The value consists of a single string. + String(String), + /// The value consists of a list of strings. + List(Vec), +} + +/// Settings to pass to a PEP 517 build backend, structured as a map from (string) key to string or +/// list of strings. +/// +/// See: +#[derive(Debug, Default, Clone)] +pub struct ConfigSettings(BTreeMap); + +impl FromIterator for ConfigSettings { + fn from_iter>(iter: T) -> Self { + let mut config = BTreeMap::default(); + for entry in iter { + match config.entry(entry.key) { + Entry::Vacant(vacant) => { + vacant.insert(ConfigSettingValue::String(entry.value)); + } + Entry::Occupied(mut occupied) => match occupied.get_mut() { + ConfigSettingValue::String(existing) => { + let existing = existing.clone(); + occupied.insert(ConfigSettingValue::List(vec![existing, entry.value])); + } + ConfigSettingValue::List(existing) => { + existing.push(entry.value); + } + }, + } + } + Self(config) + } +} + +#[cfg(feature = "serde")] +impl ConfigSettings { + /// Convert the settings to a string that can be passed directly to a PEP 517 build backend. + pub fn escape_for_python(&self) -> String { + serde_json::to_string(self).expect("Failed to serialize config settings") + } +} + +#[cfg(feature = "serde")] +impl serde::Serialize for ConfigSettings { + fn serialize(&self, serializer: S) -> Result { + use serde::ser::SerializeMap; + + let mut map = serializer.serialize_map(Some(self.0.len()))?; + for (key, value) in &self.0 { + match value { + ConfigSettingValue::String(value) => { + map.serialize_entry(&key, &value)?; + } + ConfigSettingValue::List(values) => { + map.serialize_entry(&key, &values)?; + } + } + } + map.end() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn collect_config_settings() { + let settings: ConfigSettings = vec![ + ConfigSettingEntry { + key: "key".to_string(), + value: "value".to_string(), + }, + ConfigSettingEntry { + key: "key".to_string(), + value: "value2".to_string(), + }, + ConfigSettingEntry { + key: "list".to_string(), + value: "value3".to_string(), + }, + ConfigSettingEntry { + key: "list".to_string(), + value: "value4".to_string(), + }, + ] + .into_iter() + .collect(); + assert_eq!( + settings.0.get("key"), + Some(&ConfigSettingValue::List(vec![ + "value".to_string(), + "value2".to_string() + ])) + ); + assert_eq!( + settings.0.get("list"), + Some(&ConfigSettingValue::List(vec![ + "value3".to_string(), + "value4".to_string() + ])) + ); + } + + #[test] + #[cfg(feature = "serde")] + fn escape_for_python() { + let mut settings = ConfigSettings::default(); + settings.0.insert( + "key".to_string(), + ConfigSettingValue::String("value".to_string()), + ); + settings.0.insert( + "list".to_string(), + ConfigSettingValue::List(vec!["value1".to_string(), "value2".to_string()]), + ); + assert_eq!( + settings.escape_for_python(), + r#"{"key":"value","list":["value1","value2"]}"# + ); + + let mut settings = ConfigSettings::default(); + settings.0.insert( + "key".to_string(), + ConfigSettingValue::String("Hello, \"world!\"".to_string()), + ); + settings.0.insert( + "list".to_string(), + ConfigSettingValue::List(vec!["'value1'".to_string()]), + ); + assert_eq!( + settings.escape_for_python(), + r#"{"key":"Hello, \"world!\"","list":["'value1'"]}"# + ); + + let mut settings = ConfigSettings::default(); + settings.0.insert( + "key".to_string(), + ConfigSettingValue::String("val\\1 {}ue".to_string()), + ); + assert_eq!(settings.escape_for_python(), r#"{"key":"val\\1 {}ue"}"#); + } +} diff --git a/crates/uv-types/src/downloads.rs b/crates/uv-types/src/downloads.rs new file mode 100644 index 000000000..28807ee57 --- /dev/null +++ b/crates/uv-types/src/downloads.rs @@ -0,0 +1,8 @@ +use distribution_types::{CachedDist, DistributionId}; +use once_map::OnceMap; + +#[derive(Default)] +pub struct InFlight { + /// The in-flight distribution downloads. + pub downloads: OnceMap>, +} diff --git a/crates/uv-types/src/lib.rs b/crates/uv-types/src/lib.rs new file mode 100644 index 000000000..64d0fbfd2 --- /dev/null +++ b/crates/uv-types/src/lib.rs @@ -0,0 +1,14 @@ +//! Fundamental types shared across `uv` crates. +pub use build_options::*; +pub use config_settings::*; +pub use downloads::*; +pub use name_specifiers::*; +pub use package_options::*; +pub use traits::*; + +mod build_options; +mod config_settings; +mod downloads; +mod name_specifiers; +mod package_options; +mod traits; diff --git a/crates/uv-types/src/name_specifiers.rs b/crates/uv-types/src/name_specifiers.rs new file mode 100644 index 000000000..a17ab5e31 --- /dev/null +++ b/crates/uv-types/src/name_specifiers.rs @@ -0,0 +1,63 @@ +use std::str::FromStr; + +use pep508_rs::PackageName; + +#[derive(Debug, Clone)] +pub enum PackageNameSpecifier { + All, + None, + Package(PackageName), +} + +impl FromStr for PackageNameSpecifier { + type Err = uv_normalize::InvalidNameError; + + fn from_str(name: &str) -> Result { + match name { + ":all:" => Ok(Self::All), + ":none:" => Ok(Self::None), + _ => Ok(Self::Package(PackageName::from_str(name)?)), + } + } +} + +/// Package name specification. +/// +/// Consumes both package names and selection directives for compatibility with pip flags +/// such as `--no-binary`. +#[derive(Debug, Clone)] +pub enum PackageNameSpecifiers { + All, + None, + Packages(Vec), +} + +impl PackageNameSpecifiers { + pub(crate) fn from_iter(specifiers: impl Iterator) -> Self { + let mut packages = Vec::new(); + let mut all: bool = false; + + for specifier in specifiers { + match specifier { + PackageNameSpecifier::None => { + packages.clear(); + all = false; + } + PackageNameSpecifier::All => { + all = true; + } + PackageNameSpecifier::Package(name) => { + packages.push(name); + } + } + } + + if all { + Self::All + } else if packages.is_empty() { + Self::None + } else { + Self::Packages(packages) + } + } +} diff --git a/crates/uv-types/src/package_options.rs b/crates/uv-types/src/package_options.rs new file mode 100644 index 000000000..777ab9989 --- /dev/null +++ b/crates/uv-types/src/package_options.rs @@ -0,0 +1,74 @@ +use pep508_rs::PackageName; +use rustc_hash::FxHashSet; + +/// Whether to reinstall packages. +#[derive(Debug, Clone)] +pub enum Reinstall { + /// Don't reinstall any packages; respect the existing installation. + None, + + /// Reinstall all packages in the plan. + All, + + /// Reinstall only the specified packages. + Packages(Vec), +} + +impl Reinstall { + /// Determine the reinstall strategy to use. + pub fn from_args(reinstall: bool, reinstall_package: Vec) -> Self { + if reinstall { + Self::All + } else if !reinstall_package.is_empty() { + Self::Packages(reinstall_package) + } else { + Self::None + } + } + + /// Returns `true` if no packages should be reinstalled. + pub fn is_none(&self) -> bool { + matches!(self, Self::None) + } + + /// Returns `true` if all packages should be reinstalled. + pub fn is_all(&self) -> bool { + matches!(self, Self::All) + } +} + +/// Whether to allow package upgrades. +#[derive(Debug)] +pub enum Upgrade { + /// Prefer pinned versions from the existing lockfile, if possible. + None, + + /// Allow package upgrades for all packages, ignoring the existing lockfile. + All, + + /// Allow package upgrades, but only for the specified packages. + Packages(FxHashSet), +} + +impl Upgrade { + /// Determine the upgrade strategy from the command-line arguments. + pub fn from_args(upgrade: bool, upgrade_package: Vec) -> Self { + if upgrade { + Self::All + } else if !upgrade_package.is_empty() { + Self::Packages(upgrade_package.into_iter().collect()) + } else { + Self::None + } + } + + /// Returns `true` if no packages should be upgraded. + pub fn is_none(&self) -> bool { + matches!(self, Self::None) + } + + /// Returns `true` if all packages should be upgraded. + pub fn is_all(&self) -> bool { + matches!(self, Self::All) + } +} diff --git a/crates/uv-types/src/traits.rs b/crates/uv-types/src/traits.rs new file mode 100644 index 000000000..4165e7141 --- /dev/null +++ b/crates/uv-types/src/traits.rs @@ -0,0 +1,129 @@ +use std::future::Future; +use std::path::{Path, PathBuf}; + +use anyhow::Result; + +use distribution_types::{IndexLocations, Resolution, SourceDist}; + +use pep508_rs::Requirement; +use uv_cache::Cache; +use uv_interpreter::{Interpreter, PythonEnvironment}; + +use crate::{BuildIsolation, BuildKind, NoBinary, NoBuild, SetupPyStrategy}; + +/// Avoids cyclic crate dependencies between resolver, installer and builder. +/// +/// To resolve the dependencies of a packages, we may need to build one or more source +/// distributions. To building a source distribution, we need to create a virtual environment from +/// the same base python as we use for the root resolution, resolve the build requirements +/// (potentially which nested source distributions, recursing a level deeper), installing +/// them and then build. The installer, the resolver and the source distribution builder are each in +/// their own crate. To avoid circular crate dependencies, this type dispatches between the three +/// crates with its three main methods ([`BuildContext::resolve`], [`BuildContext::install`] and +/// [`BuildContext::setup_build`]). +/// +/// The overall main crate structure looks like this: +/// +/// ```text +/// ┌────────────────┐ +/// │ uv │ +/// └───────▲────────┘ +/// │ +/// │ +/// ┌───────┴────────┐ +/// ┌─────────►│ uv-dispatch │◄─────────┐ +/// │ └───────▲────────┘ │ +/// │ │ │ +/// │ │ │ +/// ┌───────┴────────┐ ┌───────┴────────┐ ┌────────┴───────┐ +/// │ uv-resolver │ │ uv-installer │ │ uv-build │ +/// └───────▲────────┘ └───────▲────────┘ └────────▲───────┘ +/// │ │ │ +/// └─────────────┐ │ ┌──────────────┘ +/// ┌──┴────┴────┴───┐ +/// │ uv-types │ +/// └────────────────┘ +/// ``` +/// +/// Put in a different way, the types here allow `uv-resolver` to depend on `uv-build` and +/// `uv-build` to depend on `uv-resolver` which having actual crate dependencies between +/// them. +pub trait BuildContext: Sync { + type SourceDistBuilder: SourceBuildTrait + Send + Sync; + + /// Return a reference to the cache. + fn cache(&self) -> &Cache; + + /// All (potentially nested) source distribution builds use the same base python and can reuse + /// it's metadata (e.g. wheel compatibility tags). + fn interpreter(&self) -> &Interpreter; + + /// Whether to enforce build isolation when building source distributions. + fn build_isolation(&self) -> BuildIsolation; + + /// Whether source distribution building is disabled. This [`BuildContext::setup_build`] calls + /// will fail in this case. This method exists to avoid fetching source distributions if we know + /// we can't build them + fn no_build(&self) -> &NoBuild; + + /// Whether using pre-built wheels is disabled. + fn no_binary(&self) -> &NoBinary; + + /// The index locations being searched. + fn index_locations(&self) -> &IndexLocations; + + /// The strategy to use when building source distributions that lack a `pyproject.toml`. + fn setup_py_strategy(&self) -> SetupPyStrategy; + + /// Resolve the given requirements into a ready-to-install set of package versions. + fn resolve<'a>( + &'a self, + requirements: &'a [Requirement], + ) -> impl Future> + Send + 'a; + + /// Install the given set of package versions into the virtual environment. The environment must + /// use the same base Python as [`BuildContext::interpreter`] + fn install<'a>( + &'a self, + resolution: &'a Resolution, + venv: &'a PythonEnvironment, + ) -> impl Future> + Send + 'a; + + /// Setup a source distribution build by installing the required dependencies. A wrapper for + /// `uv_build::SourceBuild::setup`. + /// + /// For PEP 517 builds, this calls `get_requires_for_build_wheel`. + /// + /// `package_id` is for error reporting only. + /// `dist` is for safety checks and may be null for editable builds. + fn setup_build<'a>( + &'a self, + source: &'a Path, + subdirectory: Option<&'a Path>, + package_id: &'a str, + dist: Option<&'a SourceDist>, + build_kind: BuildKind, + ) -> impl Future> + Send + 'a; +} + +/// A wrapper for `uv_build::SourceBuild` to avoid cyclical crate dependencies. +/// +/// You can either call only `wheel()` to build the wheel directly, call only `metadata()` to get +/// the metadata without performing the actual or first call `metadata()` and then `wheel()`. +pub trait SourceBuildTrait { + /// A wrapper for `uv_build::SourceBuild::get_metadata_without_build`. + /// + /// For PEP 517 builds, this calls `prepare_metadata_for_build_wheel` + /// + /// Returns the metadata directory if we're having a PEP 517 build and the + /// `prepare_metadata_for_build_wheel` hook exists + fn metadata(&mut self) -> impl Future>> + Send; + + /// A wrapper for `uv_build::SourceBuild::build`. + /// + /// For PEP 517 builds, this calls `build_wheel`. + /// + /// Returns the filename of the built wheel inside the given `wheel_dir`. + fn wheel<'a>(&'a self, wheel_dir: &'a Path) + -> impl Future> + Send + 'a; +} diff --git a/crates/uv/Cargo.toml b/crates/uv/Cargo.toml index 6a69837c7..3d1c03477 100644 --- a/crates/uv/Cargo.toml +++ b/crates/uv/Cargo.toml @@ -31,7 +31,7 @@ uv-interpreter = { workspace = true } uv-normalize = { workspace = true } uv-requirements = { workspace = true } uv-resolver = { workspace = true, features = ["clap"] } -uv-traits = { workspace = true } +uv-types = { workspace = true } uv-virtualenv = { workspace = true } uv-warnings = { workspace = true } diff --git a/crates/uv/src/commands/pip_compile.rs b/crates/uv/src/commands/pip_compile.rs index 11025e98f..5ae5000fe 100644 --- a/crates/uv/src/commands/pip_compile.rs +++ b/crates/uv/src/commands/pip_compile.rs @@ -28,15 +28,14 @@ use uv_installer::{Downloader, NoBinary}; use uv_interpreter::{find_best_python, PythonEnvironment, PythonVersion}; use uv_normalize::{ExtraName, PackageName}; use uv_requirements::{ - upgrade::{read_lockfile, Upgrade}, - ExtrasSpecification, NamedRequirementsResolver, RequirementsSource, RequirementsSpecification, - SourceTreeResolver, + upgrade::read_lockfile, ExtrasSpecification, NamedRequirementsResolver, RequirementsSource, + RequirementsSpecification, SourceTreeResolver, }; use uv_resolver::{ AnnotationStyle, DependencyMode, DisplayResolutionGraph, InMemoryIndex, Manifest, OptionsBuilder, PreReleaseMode, PythonRequirement, ResolutionMode, Resolver, }; -use uv_traits::{BuildIsolation, ConfigSettings, InFlight, NoBuild, SetupPyStrategy}; +use uv_types::{BuildIsolation, ConfigSettings, InFlight, NoBuild, SetupPyStrategy, Upgrade}; use uv_warnings::warn_user; use crate::commands::reporters::{DownloadReporter, ResolverReporter}; diff --git a/crates/uv/src/commands/pip_install.rs b/crates/uv/src/commands/pip_install.rs index 9a4416f5a..6202ea71e 100644 --- a/crates/uv/src/commands/pip_install.rs +++ b/crates/uv/src/commands/pip_install.rs @@ -34,14 +34,14 @@ use uv_installer::{ use uv_interpreter::{Interpreter, PythonEnvironment}; use uv_normalize::PackageName; use uv_requirements::{ - upgrade::Upgrade, ExtrasSpecification, NamedRequirementsResolver, RequirementsSource, - RequirementsSpecification, SourceTreeResolver, + ExtrasSpecification, NamedRequirementsResolver, RequirementsSource, RequirementsSpecification, + SourceTreeResolver, }; use uv_resolver::{ DependencyMode, InMemoryIndex, Manifest, Options, OptionsBuilder, PreReleaseMode, Preference, ResolutionGraph, ResolutionMode, Resolver, }; -use uv_traits::{BuildIsolation, ConfigSettings, InFlight, NoBuild, SetupPyStrategy}; +use uv_types::{BuildIsolation, ConfigSettings, InFlight, NoBuild, SetupPyStrategy, Upgrade}; use uv_warnings::warn_user; use crate::commands::reporters::{DownloadReporter, InstallReporter, ResolverReporter}; diff --git a/crates/uv/src/commands/pip_sync.rs b/crates/uv/src/commands/pip_sync.rs index 4a61ca710..83f74e080 100644 --- a/crates/uv/src/commands/pip_sync.rs +++ b/crates/uv/src/commands/pip_sync.rs @@ -27,7 +27,7 @@ use uv_requirements::{ SourceTreeResolver, }; use uv_resolver::InMemoryIndex; -use uv_traits::{BuildIsolation, ConfigSettings, InFlight, NoBuild, SetupPyStrategy}; +use uv_types::{BuildIsolation, ConfigSettings, InFlight, NoBuild, SetupPyStrategy}; use uv_warnings::warn_user; use crate::commands::reporters::{ diff --git a/crates/uv/src/commands/venv.rs b/crates/uv/src/commands/venv.rs index fd64d9021..17d099952 100644 --- a/crates/uv/src/commands/venv.rs +++ b/crates/uv/src/commands/venv.rs @@ -21,7 +21,7 @@ use uv_fs::Simplified; use uv_installer::NoBinary; use uv_interpreter::{find_default_python, find_requested_python, Error}; use uv_resolver::{InMemoryIndex, OptionsBuilder}; -use uv_traits::{BuildContext, BuildIsolation, ConfigSettings, InFlight, NoBuild, SetupPyStrategy}; +use uv_types::{BuildContext, BuildIsolation, ConfigSettings, InFlight, NoBuild, SetupPyStrategy}; use crate::commands::ExitStatus; use crate::printer::Printer; diff --git a/crates/uv/src/main.rs b/crates/uv/src/main.rs index 9f7939c66..977968f97 100644 --- a/crates/uv/src/main.rs +++ b/crates/uv/src/main.rs @@ -19,10 +19,10 @@ use uv_client::Connectivity; use uv_installer::{NoBinary, Reinstall}; use uv_interpreter::PythonVersion; use uv_normalize::{ExtraName, PackageName}; -use uv_requirements::{upgrade::Upgrade, ExtrasSpecification, RequirementsSource}; +use uv_requirements::{ExtrasSpecification, RequirementsSource}; use uv_resolver::{AnnotationStyle, DependencyMode, PreReleaseMode, ResolutionMode}; -use uv_traits::{ - ConfigSettingEntry, ConfigSettings, NoBuild, PackageNameSpecifier, SetupPyStrategy, +use uv_types::{ + ConfigSettingEntry, ConfigSettings, NoBuild, PackageNameSpecifier, SetupPyStrategy, Upgrade, }; use crate::commands::{extra_name_with_clap_error, ExitStatus, ListFormat, VersionFormat};