mirror of
https://github.com/astral-sh/uv.git
synced 2025-07-07 21:35:00 +00:00
Add support for tool.uv
into distribution building (#3904)
With the change, we remove the special casing of workspace dependencies and resolve `tool.uv` for all git and directory distributions. This gives us support for non-editable workspace dependencies and path dependencies in other workspaces. It removes a lot of special casing around workspaces. These changes are the groundwork for supporting `tool.uv` with dynamic metadata. The basis for this change is moving `Requirement` from `distribution-types` to `pypi-types` and the lowering logic from `uv-requirements` to `uv-distribution`. This changes should be split out in separate PRs. I've included an example workspace `albatross-root-workspace2` where `bird-feeder` depends on `a` from another workspace `ab`. There's a bunch of failing tests and regressed error messages that still need fixing. It does fix the audited package count for the workspace tests.
This commit is contained in:
parent
09f55482a0
commit
081f20c53e
69 changed files with 1159 additions and 1680 deletions
24
Cargo.lock
generated
24
Cargo.lock
generated
|
@ -399,6 +399,7 @@ dependencies = [
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"pep508_rs",
|
"pep508_rs",
|
||||||
"platform-tags",
|
"platform-tags",
|
||||||
|
"pypi-types",
|
||||||
"tokio",
|
"tokio",
|
||||||
"uv-cache",
|
"uv-cache",
|
||||||
"uv-client",
|
"uv-client",
|
||||||
|
@ -1103,7 +1104,6 @@ dependencies = [
|
||||||
"cache-key",
|
"cache-key",
|
||||||
"distribution-filename",
|
"distribution-filename",
|
||||||
"fs-err",
|
"fs-err",
|
||||||
"indexmap",
|
|
||||||
"itertools 0.13.0",
|
"itertools 0.13.0",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"pep440_rs",
|
"pep440_rs",
|
||||||
|
@ -4567,10 +4567,10 @@ version = "0.0.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"clap",
|
"clap",
|
||||||
"distribution-types",
|
|
||||||
"either",
|
"either",
|
||||||
"pep508_rs",
|
"pep508_rs",
|
||||||
"platform-tags",
|
"platform-tags",
|
||||||
|
"pypi-types",
|
||||||
"rustc-hash",
|
"rustc-hash",
|
||||||
"schemars",
|
"schemars",
|
||||||
"serde",
|
"serde",
|
||||||
|
@ -4615,10 +4615,10 @@ dependencies = [
|
||||||
"uv-client",
|
"uv-client",
|
||||||
"uv-configuration",
|
"uv-configuration",
|
||||||
"uv-dispatch",
|
"uv-dispatch",
|
||||||
|
"uv-distribution",
|
||||||
"uv-fs",
|
"uv-fs",
|
||||||
"uv-installer",
|
"uv-installer",
|
||||||
"uv-interpreter",
|
"uv-interpreter",
|
||||||
"uv-requirements",
|
|
||||||
"uv-resolver",
|
"uv-resolver",
|
||||||
"uv-types",
|
"uv-types",
|
||||||
"uv-workspace",
|
"uv-workspace",
|
||||||
|
@ -4634,6 +4634,7 @@ dependencies = [
|
||||||
"futures",
|
"futures",
|
||||||
"install-wheel-rs",
|
"install-wheel-rs",
|
||||||
"itertools 0.13.0",
|
"itertools 0.13.0",
|
||||||
|
"pypi-types",
|
||||||
"rustc-hash",
|
"rustc-hash",
|
||||||
"tracing",
|
"tracing",
|
||||||
"uv-build",
|
"uv-build",
|
||||||
|
@ -4657,22 +4658,28 @@ dependencies = [
|
||||||
"distribution-types",
|
"distribution-types",
|
||||||
"fs-err",
|
"fs-err",
|
||||||
"futures",
|
"futures",
|
||||||
|
"glob",
|
||||||
|
"insta",
|
||||||
"install-wheel-rs",
|
"install-wheel-rs",
|
||||||
"nanoid",
|
"nanoid",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
|
"path-absolutize",
|
||||||
"pep440_rs",
|
"pep440_rs",
|
||||||
"pep508_rs",
|
"pep508_rs",
|
||||||
"platform-tags",
|
"platform-tags",
|
||||||
"pypi-types",
|
"pypi-types",
|
||||||
|
"regex",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"reqwest-middleware",
|
"reqwest-middleware",
|
||||||
"rmp-serde",
|
"rmp-serde",
|
||||||
"rustc-hash",
|
"rustc-hash",
|
||||||
|
"schemars",
|
||||||
"serde",
|
"serde",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
|
"toml",
|
||||||
"tracing",
|
"tracing",
|
||||||
"url",
|
"url",
|
||||||
"uv-cache",
|
"uv-cache",
|
||||||
|
@ -4683,6 +4690,7 @@ dependencies = [
|
||||||
"uv-git",
|
"uv-git",
|
||||||
"uv-normalize",
|
"uv-normalize",
|
||||||
"uv-types",
|
"uv-types",
|
||||||
|
"uv-warnings",
|
||||||
"zip",
|
"zip",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -4848,22 +4856,12 @@ dependencies = [
|
||||||
"distribution-types",
|
"distribution-types",
|
||||||
"fs-err",
|
"fs-err",
|
||||||
"futures",
|
"futures",
|
||||||
"glob",
|
|
||||||
"indexmap",
|
|
||||||
"indoc",
|
|
||||||
"insta",
|
|
||||||
"path-absolutize",
|
|
||||||
"pep440_rs",
|
|
||||||
"pep508_rs",
|
"pep508_rs",
|
||||||
"pypi-types",
|
"pypi-types",
|
||||||
"regex",
|
|
||||||
"requirements-txt",
|
"requirements-txt",
|
||||||
"rustc-hash",
|
"rustc-hash",
|
||||||
"same-file",
|
|
||||||
"schemars",
|
|
||||||
"serde",
|
"serde",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tokio",
|
|
||||||
"toml",
|
"toml",
|
||||||
"tracing",
|
"tracing",
|
||||||
"url",
|
"url",
|
||||||
|
|
|
@ -30,17 +30,18 @@ harness = false
|
||||||
[dependencies]
|
[dependencies]
|
||||||
distribution-filename = { workspace = true }
|
distribution-filename = { workspace = true }
|
||||||
distribution-types = { workspace = true }
|
distribution-types = { workspace = true }
|
||||||
|
install-wheel-rs = { workspace = true }
|
||||||
pep508_rs = { workspace = true }
|
pep508_rs = { workspace = true }
|
||||||
platform-tags = { workspace = true }
|
platform-tags = { workspace = true }
|
||||||
|
pypi-types = { workspace = true }
|
||||||
uv-cache = { workspace = true }
|
uv-cache = { workspace = true }
|
||||||
uv-client = { workspace = true }
|
uv-client = { workspace = true }
|
||||||
uv-dispatch = { workspace = true }
|
|
||||||
uv-configuration = { workspace = true }
|
uv-configuration = { workspace = true }
|
||||||
|
uv-dispatch = { workspace = true }
|
||||||
uv-distribution = { workspace = true }
|
uv-distribution = { workspace = true }
|
||||||
uv-interpreter = { workspace = true }
|
uv-interpreter = { workspace = true }
|
||||||
uv-resolver = { workspace = true }
|
uv-resolver = { workspace = true }
|
||||||
uv-types = { workspace = true }
|
uv-types = { workspace = true }
|
||||||
install-wheel-rs = { workspace = true }
|
|
||||||
|
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
codspeed-criterion-compat = { version = "2.6.0", default-features = false, optional = true }
|
codspeed-criterion-compat = { version = "2.6.0", default-features = false, optional = true }
|
||||||
|
|
|
@ -2,7 +2,7 @@ use std::str::FromStr;
|
||||||
|
|
||||||
use bench::criterion::black_box;
|
use bench::criterion::black_box;
|
||||||
use bench::criterion::{criterion_group, criterion_main, measurement::WallTime, Criterion};
|
use bench::criterion::{criterion_group, criterion_main, measurement::WallTime, Criterion};
|
||||||
use distribution_types::Requirement;
|
use pypi_types::Requirement;
|
||||||
use uv_cache::Cache;
|
use uv_cache::Cache;
|
||||||
use uv_client::RegistryClientBuilder;
|
use uv_client::RegistryClientBuilder;
|
||||||
use uv_interpreter::PythonEnvironment;
|
use uv_interpreter::PythonEnvironment;
|
||||||
|
@ -80,7 +80,9 @@ mod resolver {
|
||||||
use platform_tags::{Arch, Os, Platform, Tags};
|
use platform_tags::{Arch, Os, Platform, Tags};
|
||||||
use uv_cache::Cache;
|
use uv_cache::Cache;
|
||||||
use uv_client::RegistryClient;
|
use uv_client::RegistryClient;
|
||||||
use uv_configuration::{Concurrency, ConfigSettings, NoBinary, NoBuild, SetupPyStrategy};
|
use uv_configuration::{
|
||||||
|
Concurrency, ConfigSettings, NoBinary, NoBuild, PreviewMode, SetupPyStrategy,
|
||||||
|
};
|
||||||
use uv_dispatch::BuildDispatch;
|
use uv_dispatch::BuildDispatch;
|
||||||
use uv_distribution::DistributionDatabase;
|
use uv_distribution::DistributionDatabase;
|
||||||
use uv_interpreter::PythonEnvironment;
|
use uv_interpreter::PythonEnvironment;
|
||||||
|
@ -149,6 +151,7 @@ mod resolver {
|
||||||
&NoBuild::None,
|
&NoBuild::None,
|
||||||
&NoBinary::None,
|
&NoBinary::None,
|
||||||
concurrency,
|
concurrency,
|
||||||
|
PreviewMode::Disabled,
|
||||||
);
|
);
|
||||||
|
|
||||||
let resolver = Resolver::new(
|
let resolver = Resolver::new(
|
||||||
|
@ -162,7 +165,12 @@ mod resolver {
|
||||||
&hashes,
|
&hashes,
|
||||||
&build_context,
|
&build_context,
|
||||||
installed_packages,
|
installed_packages,
|
||||||
DistributionDatabase::new(client, &build_context, concurrency.downloads),
|
DistributionDatabase::new(
|
||||||
|
client,
|
||||||
|
&build_context,
|
||||||
|
concurrency.downloads,
|
||||||
|
PreviewMode::Disabled,
|
||||||
|
),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
Ok(resolver.resolve().await?)
|
Ok(resolver.resolve().await?)
|
||||||
|
|
|
@ -25,7 +25,6 @@ uv-normalize = { workspace = true }
|
||||||
|
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
fs-err = { workspace = true }
|
fs-err = { workspace = true }
|
||||||
indexmap = { workspace = true }
|
|
||||||
itertools = { workspace = true }
|
itertools = { workspace = true }
|
||||||
once_cell = { workspace = true }
|
once_cell = { workspace = true }
|
||||||
rkyv = { workspace = true }
|
rkyv = { workspace = true }
|
||||||
|
|
|
@ -58,7 +58,6 @@ pub use crate::id::*;
|
||||||
pub use crate::index_url::*;
|
pub use crate::index_url::*;
|
||||||
pub use crate::installed::*;
|
pub use crate::installed::*;
|
||||||
pub use crate::prioritized_distribution::*;
|
pub use crate::prioritized_distribution::*;
|
||||||
pub use crate::requirement::*;
|
|
||||||
pub use crate::resolution::*;
|
pub use crate::resolution::*;
|
||||||
pub use crate::resolved::*;
|
pub use crate::resolved::*;
|
||||||
pub use crate::specified_requirement::*;
|
pub use crate::specified_requirement::*;
|
||||||
|
@ -76,7 +75,6 @@ mod id;
|
||||||
mod index_url;
|
mod index_url;
|
||||||
mod installed;
|
mod installed;
|
||||||
mod prioritized_distribution;
|
mod prioritized_distribution;
|
||||||
mod requirement;
|
|
||||||
mod resolution;
|
mod resolution;
|
||||||
mod resolved;
|
mod resolved;
|
||||||
mod specified_requirement;
|
mod specified_requirement;
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
|
use pypi_types::{Requirement, RequirementSource};
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
use uv_normalize::{ExtraName, PackageName};
|
use uv_normalize::{ExtraName, PackageName};
|
||||||
|
|
||||||
use crate::{
|
use crate::{BuiltDist, Diagnostic, Dist, Name, ResolvedDist, SourceDist};
|
||||||
BuiltDist, Diagnostic, Dist, Name, Requirement, RequirementSource, ResolvedDist, SourceDist,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// A set of packages pinned at specific versions.
|
/// A set of packages pinned at specific versions.
|
||||||
#[derive(Debug, Default, Clone)]
|
#[derive(Debug, Default, Clone)]
|
||||||
|
|
|
@ -2,9 +2,10 @@ use std::borrow::Cow;
|
||||||
use std::fmt::{Display, Formatter};
|
use std::fmt::{Display, Formatter};
|
||||||
|
|
||||||
use pep508_rs::{MarkerEnvironment, UnnamedRequirement};
|
use pep508_rs::{MarkerEnvironment, UnnamedRequirement};
|
||||||
|
use pypi_types::{Requirement, RequirementSource};
|
||||||
use uv_normalize::ExtraName;
|
use uv_normalize::ExtraName;
|
||||||
|
|
||||||
use crate::{Requirement, RequirementSource, VerbatimParsedUrl};
|
use crate::VerbatimParsedUrl;
|
||||||
|
|
||||||
/// An [`UnresolvedRequirement`] with additional metadata from `requirements.txt`, currently only
|
/// An [`UnresolvedRequirement`] with additional metadata from `requirements.txt`, currently only
|
||||||
/// hashes but in the future also editable and similar information.
|
/// hashes but in the future also editable and similar information.
|
||||||
|
|
|
@ -1541,6 +1541,25 @@ pub enum MarkerTree {
|
||||||
Or(Vec<MarkerTree>),
|
Or(Vec<MarkerTree>),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for MarkerTree {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let s = String::deserialize(deserializer)?;
|
||||||
|
FromStr::from_str(&s).map_err(de::Error::custom)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Serialize for MarkerTree {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: Serializer,
|
||||||
|
{
|
||||||
|
serializer.serialize_str(&self.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl FromStr for MarkerTree {
|
impl FromStr for MarkerTree {
|
||||||
type Err = Pep508Error;
|
type Err = Pep508Error;
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ pub use direct_url::*;
|
||||||
pub use lenient_requirement::*;
|
pub use lenient_requirement::*;
|
||||||
pub use metadata::*;
|
pub use metadata::*;
|
||||||
pub use parsed_url::*;
|
pub use parsed_url::*;
|
||||||
|
pub use requirement::*;
|
||||||
pub use scheme::*;
|
pub use scheme::*;
|
||||||
pub use simple_json::*;
|
pub use simple_json::*;
|
||||||
|
|
||||||
|
@ -11,5 +12,6 @@ mod direct_url;
|
||||||
mod lenient_requirement;
|
mod lenient_requirement;
|
||||||
mod metadata;
|
mod metadata;
|
||||||
mod parsed_url;
|
mod parsed_url;
|
||||||
|
mod requirement;
|
||||||
mod scheme;
|
mod scheme;
|
||||||
mod simple_json;
|
mod simple_json;
|
||||||
|
|
|
@ -6,10 +6,11 @@ use url::Url;
|
||||||
|
|
||||||
use pep440_rs::VersionSpecifiers;
|
use pep440_rs::VersionSpecifiers;
|
||||||
use pep508_rs::{MarkerEnvironment, MarkerTree, RequirementOrigin, VerbatimUrl, VersionOrUrl};
|
use pep508_rs::{MarkerEnvironment, MarkerTree, RequirementOrigin, VerbatimUrl, VersionOrUrl};
|
||||||
use pypi_types::{ParsedUrl, VerbatimParsedUrl};
|
|
||||||
use uv_git::{GitReference, GitSha};
|
use uv_git::{GitReference, GitSha};
|
||||||
use uv_normalize::{ExtraName, PackageName};
|
use uv_normalize::{ExtraName, PackageName};
|
||||||
|
|
||||||
|
use crate::{ParsedUrl, VerbatimParsedUrl};
|
||||||
|
|
||||||
/// The requirements of a distribution, an extension over PEP 508's requirements.
|
/// The requirements of a distribution, an extension over PEP 508's requirements.
|
||||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||||
pub struct Requirements {
|
pub struct Requirements {
|
|
@ -44,12 +44,12 @@ use tracing::instrument;
|
||||||
use unscanny::{Pattern, Scanner};
|
use unscanny::{Pattern, Scanner};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use distribution_types::{Requirement, UnresolvedRequirement, UnresolvedRequirementSpecification};
|
use distribution_types::{UnresolvedRequirement, UnresolvedRequirementSpecification};
|
||||||
use pep508_rs::{
|
use pep508_rs::{
|
||||||
expand_env_vars, split_scheme, strip_host, Extras, MarkerTree, Pep508Error, Pep508ErrorSource,
|
expand_env_vars, split_scheme, strip_host, Extras, MarkerTree, Pep508Error, Pep508ErrorSource,
|
||||||
RequirementOrigin, Scheme, UnnamedRequirement, VerbatimUrl,
|
RequirementOrigin, Scheme, UnnamedRequirement, VerbatimUrl,
|
||||||
};
|
};
|
||||||
use pypi_types::{ParsedPathUrl, ParsedUrl, VerbatimParsedUrl};
|
use pypi_types::{ParsedPathUrl, ParsedUrl, Requirement, VerbatimParsedUrl};
|
||||||
#[cfg(feature = "http")]
|
#[cfg(feature = "http")]
|
||||||
use uv_client::BaseClient;
|
use uv_client::BaseClient;
|
||||||
use uv_client::BaseClientBuilder;
|
use uv_client::BaseClientBuilder;
|
||||||
|
|
|
@ -25,10 +25,10 @@ use tokio::process::Command;
|
||||||
use tokio::sync::{Mutex, Semaphore};
|
use tokio::sync::{Mutex, Semaphore};
|
||||||
use tracing::{debug, info_span, instrument, Instrument};
|
use tracing::{debug, info_span, instrument, Instrument};
|
||||||
|
|
||||||
use distribution_types::{Requirement, Resolution};
|
use distribution_types::Resolution;
|
||||||
use pep440_rs::Version;
|
use pep440_rs::Version;
|
||||||
use pep508_rs::PackageName;
|
use pep508_rs::PackageName;
|
||||||
use pypi_types::VerbatimParsedUrl;
|
use pypi_types::{Requirement, VerbatimParsedUrl};
|
||||||
use uv_configuration::{BuildKind, ConfigSettings, SetupPyStrategy};
|
use uv_configuration::{BuildKind, ConfigSettings, SetupPyStrategy};
|
||||||
use uv_fs::{PythonExt, Simplified};
|
use uv_fs::{PythonExt, Simplified};
|
||||||
use uv_interpreter::{Interpreter, PythonEnvironment};
|
use uv_interpreter::{Interpreter, PythonEnvironment};
|
||||||
|
|
|
@ -13,9 +13,9 @@ license = { workspace = true }
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
distribution-types = { workspace = true }
|
|
||||||
pep508_rs = { workspace = true }
|
pep508_rs = { workspace = true }
|
||||||
platform-tags = { workspace = true }
|
platform-tags = { workspace = true }
|
||||||
|
pypi-types = { workspace = true }
|
||||||
uv-auth = { workspace = true }
|
uv-auth = { workspace = true }
|
||||||
uv-normalize = { workspace = true }
|
uv-normalize = { workspace = true }
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
use std::hash::BuildHasherDefault;
|
use std::hash::BuildHasherDefault;
|
||||||
|
|
||||||
use distribution_types::Requirement;
|
|
||||||
use rustc_hash::FxHashMap;
|
use rustc_hash::FxHashMap;
|
||||||
|
|
||||||
|
use pypi_types::Requirement;
|
||||||
use uv_normalize::PackageName;
|
use uv_normalize::PackageName;
|
||||||
|
|
||||||
/// A set of constraints for a set of requirements.
|
/// A set of constraints for a set of requirements.
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
use std::hash::BuildHasherDefault;
|
use std::hash::BuildHasherDefault;
|
||||||
|
|
||||||
use either::Either;
|
use either::Either;
|
||||||
|
use pypi_types::Requirement;
|
||||||
use rustc_hash::FxHashMap;
|
use rustc_hash::FxHashMap;
|
||||||
|
|
||||||
use distribution_types::Requirement;
|
|
||||||
use uv_normalize::PackageName;
|
use uv_normalize::PackageName;
|
||||||
|
|
||||||
/// A set of overrides for a set of requirements.
|
/// A set of overrides for a set of requirements.
|
||||||
|
|
|
@ -25,11 +25,11 @@ uv-build = { workspace = true }
|
||||||
uv-cache = { workspace = true, features = ["clap"] }
|
uv-cache = { workspace = true, features = ["clap"] }
|
||||||
uv-client = { workspace = true }
|
uv-client = { workspace = true }
|
||||||
uv-configuration = { workspace = true }
|
uv-configuration = { workspace = true }
|
||||||
|
uv-distribution = { workspace = true, features = ["schemars"] }
|
||||||
uv-dispatch = { workspace = true }
|
uv-dispatch = { workspace = true }
|
||||||
uv-fs = { workspace = true }
|
uv-fs = { workspace = true }
|
||||||
uv-installer = { workspace = true }
|
uv-installer = { workspace = true }
|
||||||
uv-interpreter = { workspace = true }
|
uv-interpreter = { workspace = true }
|
||||||
uv-requirements = { workspace = true, features = ["schemars"] }
|
|
||||||
uv-resolver = { workspace = true }
|
uv-resolver = { workspace = true }
|
||||||
uv-types = { workspace = true }
|
uv-types = { workspace = true }
|
||||||
uv-workspace = { workspace = true, features = ["schemars"] }
|
uv-workspace = { workspace = true, features = ["schemars"] }
|
||||||
|
|
|
@ -11,7 +11,7 @@ use uv_build::{SourceBuild, SourceBuildContext};
|
||||||
use uv_cache::{Cache, CacheArgs};
|
use uv_cache::{Cache, CacheArgs};
|
||||||
use uv_client::RegistryClientBuilder;
|
use uv_client::RegistryClientBuilder;
|
||||||
use uv_configuration::{
|
use uv_configuration::{
|
||||||
BuildKind, Concurrency, ConfigSettings, NoBinary, NoBuild, SetupPyStrategy,
|
BuildKind, Concurrency, ConfigSettings, NoBinary, NoBuild, PreviewMode, SetupPyStrategy,
|
||||||
};
|
};
|
||||||
use uv_dispatch::BuildDispatch;
|
use uv_dispatch::BuildDispatch;
|
||||||
use uv_interpreter::PythonEnvironment;
|
use uv_interpreter::PythonEnvironment;
|
||||||
|
@ -80,6 +80,7 @@ pub(crate) async fn build(args: BuildArgs) -> Result<PathBuf> {
|
||||||
&NoBuild::None,
|
&NoBuild::None,
|
||||||
&NoBinary::None,
|
&NoBinary::None,
|
||||||
concurrency,
|
concurrency,
|
||||||
|
PreviewMode::Enabled,
|
||||||
);
|
);
|
||||||
|
|
||||||
let builder = SourceBuild::setup(
|
let builder = SourceBuild::setup(
|
||||||
|
|
|
@ -20,7 +20,7 @@ struct ToolUv {
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
options: Options,
|
options: Options,
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
dep_spec: uv_requirements::pyproject::ToolUv,
|
dep_spec: uv_distribution::pyproject::ToolUv,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(clap::Args)]
|
#[derive(clap::Args)]
|
||||||
|
|
|
@ -16,13 +16,14 @@ workspace = true
|
||||||
[dependencies]
|
[dependencies]
|
||||||
distribution-types = { workspace = true }
|
distribution-types = { workspace = true }
|
||||||
install-wheel-rs = { workspace = true }
|
install-wheel-rs = { workspace = true }
|
||||||
|
pypi-types = { workspace = true }
|
||||||
uv-build = { workspace = true }
|
uv-build = { workspace = true }
|
||||||
uv-cache = { workspace = true }
|
uv-cache = { workspace = true }
|
||||||
uv-client = { workspace = true }
|
uv-client = { workspace = true }
|
||||||
uv-configuration = { workspace = true }
|
uv-configuration = { workspace = true }
|
||||||
|
uv-distribution = { workspace = true }
|
||||||
uv-installer = { workspace = true }
|
uv-installer = { workspace = true }
|
||||||
uv-interpreter = { workspace = true }
|
uv-interpreter = { workspace = true }
|
||||||
uv-distribution = { workspace = true }
|
|
||||||
uv-resolver = { workspace = true }
|
uv-resolver = { workspace = true }
|
||||||
uv-types = { workspace = true }
|
uv-types = { workspace = true }
|
||||||
|
|
||||||
|
|
|
@ -11,12 +11,13 @@ use itertools::Itertools;
|
||||||
use rustc_hash::FxHashMap;
|
use rustc_hash::FxHashMap;
|
||||||
use tracing::{debug, instrument};
|
use tracing::{debug, instrument};
|
||||||
|
|
||||||
use distribution_types::{CachedDist, IndexLocations, Name, Requirement, Resolution, SourceDist};
|
use distribution_types::{CachedDist, IndexLocations, Name, Resolution, SourceDist};
|
||||||
|
use pypi_types::Requirement;
|
||||||
use uv_build::{SourceBuild, SourceBuildContext};
|
use uv_build::{SourceBuild, SourceBuildContext};
|
||||||
use uv_cache::Cache;
|
use uv_cache::Cache;
|
||||||
use uv_client::RegistryClient;
|
use uv_client::RegistryClient;
|
||||||
use uv_configuration::Concurrency;
|
|
||||||
use uv_configuration::{BuildKind, ConfigSettings, NoBinary, NoBuild, Reinstall, SetupPyStrategy};
|
use uv_configuration::{BuildKind, ConfigSettings, NoBinary, NoBuild, Reinstall, SetupPyStrategy};
|
||||||
|
use uv_configuration::{Concurrency, PreviewMode};
|
||||||
use uv_distribution::DistributionDatabase;
|
use uv_distribution::DistributionDatabase;
|
||||||
use uv_installer::{Downloader, Installer, Plan, Planner, SitePackages};
|
use uv_installer::{Downloader, Installer, Plan, Planner, SitePackages};
|
||||||
use uv_interpreter::{Interpreter, PythonEnvironment};
|
use uv_interpreter::{Interpreter, PythonEnvironment};
|
||||||
|
@ -43,6 +44,7 @@ pub struct BuildDispatch<'a> {
|
||||||
options: Options,
|
options: Options,
|
||||||
build_extra_env_vars: FxHashMap<OsString, OsString>,
|
build_extra_env_vars: FxHashMap<OsString, OsString>,
|
||||||
concurrency: Concurrency,
|
concurrency: Concurrency,
|
||||||
|
preview_mode: PreviewMode,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> BuildDispatch<'a> {
|
impl<'a> BuildDispatch<'a> {
|
||||||
|
@ -62,6 +64,7 @@ impl<'a> BuildDispatch<'a> {
|
||||||
no_build: &'a NoBuild,
|
no_build: &'a NoBuild,
|
||||||
no_binary: &'a NoBinary,
|
no_binary: &'a NoBinary,
|
||||||
concurrency: Concurrency,
|
concurrency: Concurrency,
|
||||||
|
preview_mode: PreviewMode,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
client,
|
client,
|
||||||
|
@ -81,6 +84,7 @@ impl<'a> BuildDispatch<'a> {
|
||||||
source_build_context: SourceBuildContext::default(),
|
source_build_context: SourceBuildContext::default(),
|
||||||
options: Options::default(),
|
options: Options::default(),
|
||||||
build_extra_env_vars: FxHashMap::default(),
|
build_extra_env_vars: FxHashMap::default(),
|
||||||
|
preview_mode,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -138,7 +142,12 @@ impl<'a> BuildContext for BuildDispatch<'a> {
|
||||||
&HashStrategy::None,
|
&HashStrategy::None,
|
||||||
self,
|
self,
|
||||||
EmptyInstalledPackages,
|
EmptyInstalledPackages,
|
||||||
DistributionDatabase::new(self.client, self, self.concurrency.downloads),
|
DistributionDatabase::new(
|
||||||
|
self.client,
|
||||||
|
self,
|
||||||
|
self.concurrency.downloads,
|
||||||
|
self.preview_mode,
|
||||||
|
),
|
||||||
)?;
|
)?;
|
||||||
let graph = resolver.resolve().await.with_context(|| {
|
let graph = resolver.resolve().await.with_context(|| {
|
||||||
format!(
|
format!(
|
||||||
|
@ -220,7 +229,12 @@ impl<'a> BuildContext for BuildDispatch<'a> {
|
||||||
self.cache,
|
self.cache,
|
||||||
tags,
|
tags,
|
||||||
&HashStrategy::None,
|
&HashStrategy::None,
|
||||||
DistributionDatabase::new(self.client, self, self.concurrency.downloads),
|
DistributionDatabase::new(
|
||||||
|
self.client,
|
||||||
|
self,
|
||||||
|
self.concurrency.downloads,
|
||||||
|
self.preview_mode,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
debug!(
|
debug!(
|
||||||
|
|
|
@ -23,27 +23,39 @@ platform-tags = { workspace = true }
|
||||||
pypi-types = { workspace = true }
|
pypi-types = { workspace = true }
|
||||||
uv-cache = { workspace = true }
|
uv-cache = { workspace = true }
|
||||||
uv-client = { workspace = true }
|
uv-client = { workspace = true }
|
||||||
|
uv-configuration = { workspace = true }
|
||||||
uv-extract = { workspace = true }
|
uv-extract = { workspace = true }
|
||||||
uv-fs = { workspace = true, features = ["tokio"] }
|
uv-fs = { workspace = true, features = ["tokio"] }
|
||||||
uv-git = { workspace = true }
|
uv-git = { workspace = true }
|
||||||
uv-normalize = { workspace = true }
|
uv-normalize = { workspace = true }
|
||||||
uv-types = { workspace = true }
|
uv-types = { workspace = true }
|
||||||
uv-configuration = { workspace = true }
|
uv-warnings = { workspace = true }
|
||||||
|
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
fs-err = { workspace = true }
|
fs-err = { workspace = true }
|
||||||
futures = { workspace = true }
|
futures = { workspace = true }
|
||||||
|
glob = { workspace = true }
|
||||||
nanoid = { workspace = true }
|
nanoid = { workspace = true }
|
||||||
once_cell = { workspace = true }
|
once_cell = { workspace = true }
|
||||||
|
path-absolutize = { workspace = true }
|
||||||
reqwest = { workspace = true }
|
reqwest = { workspace = true }
|
||||||
reqwest-middleware = { workspace = true }
|
reqwest-middleware = { workspace = true }
|
||||||
rmp-serde = { workspace = true }
|
rmp-serde = { workspace = true }
|
||||||
rustc-hash = { workspace = true }
|
rustc-hash = { workspace = true }
|
||||||
|
schemars = { workspace = true, optional = true }
|
||||||
serde = { workspace = true, features = ["derive"] }
|
serde = { workspace = true, features = ["derive"] }
|
||||||
tempfile = { workspace = true }
|
tempfile = { workspace = true }
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
tokio = { workspace = true }
|
tokio = { workspace = true }
|
||||||
tokio-util = { workspace = true, features = ["compat"] }
|
tokio-util = { workspace = true, features = ["compat"] }
|
||||||
|
toml = { workspace = true }
|
||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
url = { workspace = true }
|
url = { workspace = true }
|
||||||
zip = { workspace = true }
|
zip = { workspace = true }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
insta = { version = "1.39.0", features = ["filters", "json", "redactions"] }
|
||||||
|
regex = { workspace = true }
|
||||||
|
|
||||||
|
[features]
|
||||||
|
schemars = ["dep:schemars"]
|
||||||
|
|
|
@ -25,7 +25,7 @@ use uv_cache::{ArchiveId, ArchiveTimestamp, CacheBucket, CacheEntry, Timestamp,
|
||||||
use uv_client::{
|
use uv_client::{
|
||||||
CacheControl, CachedClientError, Connectivity, DataWithCachePolicy, RegistryClient,
|
CacheControl, CachedClientError, Connectivity, DataWithCachePolicy, RegistryClient,
|
||||||
};
|
};
|
||||||
use uv_configuration::{NoBinary, NoBuild};
|
use uv_configuration::{NoBinary, NoBuild, PreviewMode};
|
||||||
use uv_extract::hash::Hasher;
|
use uv_extract::hash::Hasher;
|
||||||
use uv_fs::write_atomic;
|
use uv_fs::write_atomic;
|
||||||
use uv_types::BuildContext;
|
use uv_types::BuildContext;
|
||||||
|
@ -33,7 +33,7 @@ use uv_types::BuildContext;
|
||||||
use crate::archive::Archive;
|
use crate::archive::Archive;
|
||||||
use crate::locks::Locks;
|
use crate::locks::Locks;
|
||||||
use crate::source::SourceDistributionBuilder;
|
use crate::source::SourceDistributionBuilder;
|
||||||
use crate::{ArchiveMetadata, Error, LocalWheel, Reporter};
|
use crate::{ArchiveMetadata, Error, LocalWheel, Metadata, Reporter};
|
||||||
|
|
||||||
/// A cached high-level interface to convert distributions (a requirement resolved to a location)
|
/// A cached high-level interface to convert distributions (a requirement resolved to a location)
|
||||||
/// to a wheel or wheel metadata.
|
/// to a wheel or wheel metadata.
|
||||||
|
@ -60,10 +60,11 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> {
|
||||||
client: &'a RegistryClient,
|
client: &'a RegistryClient,
|
||||||
build_context: &'a Context,
|
build_context: &'a Context,
|
||||||
concurrent_downloads: usize,
|
concurrent_downloads: usize,
|
||||||
|
preview_mode: PreviewMode,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
build_context,
|
build_context,
|
||||||
builder: SourceDistributionBuilder::new(build_context),
|
builder: SourceDistributionBuilder::new(build_context, preview_mode),
|
||||||
locks: Rc::new(Locks::default()),
|
locks: Rc::new(Locks::default()),
|
||||||
client: ManagedClient::new(client, concurrent_downloads),
|
client: ManagedClient::new(client, concurrent_downloads),
|
||||||
reporter: None,
|
reporter: None,
|
||||||
|
@ -364,7 +365,10 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> {
|
||||||
let wheel = self.get_wheel(dist, hashes).await?;
|
let wheel = self.get_wheel(dist, hashes).await?;
|
||||||
let metadata = wheel.metadata()?;
|
let metadata = wheel.metadata()?;
|
||||||
let hashes = wheel.hashes;
|
let hashes = wheel.hashes;
|
||||||
return Ok(ArchiveMetadata { metadata, hashes });
|
return Ok(ArchiveMetadata {
|
||||||
|
metadata: Metadata::from_metadata23(metadata),
|
||||||
|
hashes,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let result = self
|
let result = self
|
||||||
|
@ -373,7 +377,7 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> {
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
match result {
|
match result {
|
||||||
Ok(metadata) => Ok(ArchiveMetadata::from(metadata)),
|
Ok(metadata) => Ok(ArchiveMetadata::from_metadata23(metadata)),
|
||||||
Err(err) if err.is_http_streaming_unsupported() => {
|
Err(err) if err.is_http_streaming_unsupported() => {
|
||||||
warn!("Streaming unsupported when fetching metadata for {dist}; downloading wheel directly ({err})");
|
warn!("Streaming unsupported when fetching metadata for {dist}; downloading wheel directly ({err})");
|
||||||
|
|
||||||
|
@ -382,7 +386,10 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> {
|
||||||
let wheel = self.get_wheel(dist, hashes).await?;
|
let wheel = self.get_wheel(dist, hashes).await?;
|
||||||
let metadata = wheel.metadata()?;
|
let metadata = wheel.metadata()?;
|
||||||
let hashes = wheel.hashes;
|
let hashes = wheel.hashes;
|
||||||
Ok(ArchiveMetadata { metadata, hashes })
|
Ok(ArchiveMetadata {
|
||||||
|
metadata: Metadata::from_metadata23(metadata),
|
||||||
|
hashes,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
Err(err) => Err(err.into()),
|
Err(err) => Err(err.into()),
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ use std::path::PathBuf;
|
||||||
use tokio::task::JoinError;
|
use tokio::task::JoinError;
|
||||||
use zip::result::ZipError;
|
use zip::result::ZipError;
|
||||||
|
|
||||||
|
use crate::MetadataLoweringError;
|
||||||
use distribution_filename::WheelFilenameError;
|
use distribution_filename::WheelFilenameError;
|
||||||
use pep440_rs::Version;
|
use pep440_rs::Version;
|
||||||
use pypi_types::HashDigest;
|
use pypi_types::HashDigest;
|
||||||
|
@ -77,6 +78,8 @@ pub enum Error {
|
||||||
DynamicPyprojectToml(#[source] pypi_types::MetadataError),
|
DynamicPyprojectToml(#[source] pypi_types::MetadataError),
|
||||||
#[error("Unsupported scheme in URL: {0}")]
|
#[error("Unsupported scheme in URL: {0}")]
|
||||||
UnsupportedScheme(String),
|
UnsupportedScheme(String),
|
||||||
|
#[error(transparent)]
|
||||||
|
MetadataLowering(#[from] MetadataLoweringError),
|
||||||
|
|
||||||
/// A generic request middleware error happened while making a request.
|
/// A generic request middleware error happened while making a request.
|
||||||
/// Refer to the error message for more details.
|
/// Refer to the error message for more details.
|
||||||
|
|
|
@ -1,11 +1,21 @@
|
||||||
pub use archive::Archive;
|
use std::collections::BTreeMap;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use archive::Archive;
|
||||||
pub use distribution_database::{DistributionDatabase, HttpArchivePointer, LocalArchivePointer};
|
pub use distribution_database::{DistributionDatabase, HttpArchivePointer, LocalArchivePointer};
|
||||||
pub use download::LocalWheel;
|
pub use download::LocalWheel;
|
||||||
pub use error::Error;
|
pub use error::Error;
|
||||||
pub use git::{git_url_to_precise, is_same_reference};
|
pub use git::{git_url_to_precise, is_same_reference};
|
||||||
pub use index::{BuiltWheelIndex, RegistryWheelIndex};
|
pub use index::{BuiltWheelIndex, RegistryWheelIndex};
|
||||||
|
use pep440_rs::{Version, VersionSpecifiers};
|
||||||
use pypi_types::{HashDigest, Metadata23};
|
use pypi_types::{HashDigest, Metadata23};
|
||||||
pub use reporter::Reporter;
|
pub use reporter::Reporter;
|
||||||
|
use requirement_lowering::{lower_requirement, LoweringError};
|
||||||
|
use uv_configuration::PreviewMode;
|
||||||
|
use uv_normalize::{ExtraName, PackageName};
|
||||||
|
pub use workspace::{ProjectWorkspace, Workspace, WorkspaceError, WorkspaceMember};
|
||||||
|
|
||||||
mod archive;
|
mod archive;
|
||||||
mod distribution_database;
|
mod distribution_database;
|
||||||
|
@ -14,20 +24,120 @@ mod error;
|
||||||
mod git;
|
mod git;
|
||||||
mod index;
|
mod index;
|
||||||
mod locks;
|
mod locks;
|
||||||
|
pub mod pyproject;
|
||||||
mod reporter;
|
mod reporter;
|
||||||
|
mod requirement_lowering;
|
||||||
mod source;
|
mod source;
|
||||||
|
mod workspace;
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum MetadataLoweringError {
|
||||||
|
#[error(transparent)]
|
||||||
|
Workspace(#[from] WorkspaceError),
|
||||||
|
#[error(transparent)]
|
||||||
|
Lowering(#[from] LoweringError),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Metadata {
|
||||||
|
// Mandatory fields
|
||||||
|
pub name: PackageName,
|
||||||
|
pub version: Version,
|
||||||
|
// Optional fields
|
||||||
|
pub requires_dist: Vec<pypi_types::Requirement>,
|
||||||
|
pub requires_python: Option<VersionSpecifiers>,
|
||||||
|
pub provides_extras: Vec<ExtraName>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Metadata {
|
||||||
|
/// Lower without considering `tool.uv` in `pyproject.toml`, used for index and other archive
|
||||||
|
/// dependencies.
|
||||||
|
pub fn from_metadata23(metadata: Metadata23) -> Self {
|
||||||
|
Self {
|
||||||
|
name: metadata.name,
|
||||||
|
version: metadata.version,
|
||||||
|
requires_dist: metadata
|
||||||
|
.requires_dist
|
||||||
|
.into_iter()
|
||||||
|
.map(pypi_types::Requirement::from)
|
||||||
|
.collect(),
|
||||||
|
requires_python: metadata.requires_python,
|
||||||
|
provides_extras: metadata.provides_extras,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lower by considering `tool.uv` in `pyproject.toml` if present, used for Git and directory
|
||||||
|
/// dependencies.
|
||||||
|
pub async fn from_workspace(
|
||||||
|
metadata: Metadata23,
|
||||||
|
project_root: &Path,
|
||||||
|
preview_mode: PreviewMode,
|
||||||
|
) -> Result<Self, MetadataLoweringError> {
|
||||||
|
// TODO(konsti): Limit discovery for Git checkouts to Git root.
|
||||||
|
// TODO(konsti): Cache workspace discovery.
|
||||||
|
let Some(project_workspace) =
|
||||||
|
ProjectWorkspace::from_maybe_project_root(project_root).await?
|
||||||
|
else {
|
||||||
|
return Ok(Self::from_metadata23(metadata));
|
||||||
|
};
|
||||||
|
|
||||||
|
let empty = BTreeMap::default();
|
||||||
|
let sources = project_workspace
|
||||||
|
.current_project()
|
||||||
|
.pyproject_toml()
|
||||||
|
.tool
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|tool| tool.uv.as_ref())
|
||||||
|
.and_then(|uv| uv.sources.as_ref())
|
||||||
|
.unwrap_or(&empty);
|
||||||
|
|
||||||
|
let requires_dist = metadata
|
||||||
|
.requires_dist
|
||||||
|
.into_iter()
|
||||||
|
.map(|requirement| {
|
||||||
|
lower_requirement(
|
||||||
|
requirement,
|
||||||
|
&metadata.name,
|
||||||
|
project_workspace.project_root(),
|
||||||
|
sources,
|
||||||
|
project_workspace.workspace(),
|
||||||
|
preview_mode,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect::<Result<_, _>>()?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
name: metadata.name,
|
||||||
|
version: metadata.version,
|
||||||
|
requires_dist,
|
||||||
|
requires_python: metadata.requires_python,
|
||||||
|
provides_extras: metadata.provides_extras,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// The metadata associated with an archive.
|
/// The metadata associated with an archive.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ArchiveMetadata {
|
pub struct ArchiveMetadata {
|
||||||
/// The [`Metadata23`] for the underlying distribution.
|
/// The [`Metadata`] for the underlying distribution.
|
||||||
pub metadata: Metadata23,
|
pub metadata: Metadata,
|
||||||
/// The hashes of the source or built archive.
|
/// The hashes of the source or built archive.
|
||||||
pub hashes: Vec<HashDigest>,
|
pub hashes: Vec<HashDigest>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Metadata23> for ArchiveMetadata {
|
impl ArchiveMetadata {
|
||||||
fn from(metadata: Metadata23) -> Self {
|
/// Lower without considering `tool.uv` in `pyproject.toml`, used for index and other archive
|
||||||
|
/// dependencies.
|
||||||
|
pub fn from_metadata23(metadata: Metadata23) -> Self {
|
||||||
|
Self {
|
||||||
|
metadata: Metadata::from_metadata23(metadata),
|
||||||
|
hashes: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Metadata> for ArchiveMetadata {
|
||||||
|
fn from(metadata: Metadata) -> Self {
|
||||||
Self {
|
Self {
|
||||||
metadata,
|
metadata,
|
||||||
hashes: vec![],
|
hashes: vec![],
|
||||||
|
|
199
crates/uv-distribution/src/pyproject.rs
Normal file
199
crates/uv-distribution/src/pyproject.rs
Normal file
|
@ -0,0 +1,199 @@
|
||||||
|
//! Reads the following fields from `pyproject.toml`:
|
||||||
|
//!
|
||||||
|
//! * `project.{dependencies,optional-dependencies}`
|
||||||
|
//! * `tool.uv.sources`
|
||||||
|
//! * `tool.uv.workspace`
|
||||||
|
//!
|
||||||
|
//! Then lowers them into a dependency specification.
|
||||||
|
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
use std::ops::Deref;
|
||||||
|
|
||||||
|
use glob::Pattern;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use thiserror::Error;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
use pep508_rs::Pep508Error;
|
||||||
|
use pypi_types::VerbatimParsedUrl;
|
||||||
|
use uv_normalize::{ExtraName, PackageName};
|
||||||
|
|
||||||
|
use crate::LoweringError;
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum Pep621Error {
|
||||||
|
#[error(transparent)]
|
||||||
|
Pep508(#[from] Box<Pep508Error<VerbatimParsedUrl>>),
|
||||||
|
#[error("Must specify a `[project]` section alongside `[tool.uv.sources]`")]
|
||||||
|
MissingProjectSection,
|
||||||
|
#[error("pyproject.toml section is declared as dynamic, but must be static: `{0}`")]
|
||||||
|
DynamicNotAllowed(&'static str),
|
||||||
|
#[error("Failed to parse entry for: `{0}`")]
|
||||||
|
LoweringError(PackageName, #[source] LoweringError),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Pep508Error<VerbatimParsedUrl>> for Pep621Error {
|
||||||
|
fn from(error: Pep508Error<VerbatimParsedUrl>) -> Self {
|
||||||
|
Self::Pep508(Box::new(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A `pyproject.toml` as specified in PEP 517.
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
pub struct PyProjectToml {
|
||||||
|
/// PEP 621-compliant project metadata.
|
||||||
|
pub project: Option<Project>,
|
||||||
|
/// Tool-specific metadata.
|
||||||
|
pub tool: Option<Tool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// PEP 621 project metadata (`project`).
|
||||||
|
///
|
||||||
|
/// See <https://packaging.python.org/en/latest/specifications/pyproject-toml>.
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
pub struct Project {
|
||||||
|
/// The name of the project
|
||||||
|
pub name: PackageName,
|
||||||
|
/// The optional dependencies of the project.
|
||||||
|
pub optional_dependencies: Option<BTreeMap<ExtraName, Vec<String>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||||
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
|
pub struct Tool {
|
||||||
|
pub uv: Option<ToolUv>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||||
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
|
pub struct ToolUv {
|
||||||
|
pub sources: Option<BTreeMap<PackageName, Source>>,
|
||||||
|
pub workspace: Option<ToolUvWorkspace>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||||
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
|
pub struct ToolUvWorkspace {
|
||||||
|
pub members: Option<Vec<SerdePattern>>,
|
||||||
|
pub exclude: Option<Vec<SerdePattern>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// (De)serialize globs as strings.
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct SerdePattern(#[serde(with = "serde_from_and_to_string")] pub Pattern);
|
||||||
|
|
||||||
|
#[cfg(feature = "schemars")]
|
||||||
|
impl schemars::JsonSchema for SerdePattern {
|
||||||
|
fn schema_name() -> String {
|
||||||
|
<String as schemars::JsonSchema>::schema_name()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
|
||||||
|
<String as schemars::JsonSchema>::json_schema(gen)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for SerdePattern {
|
||||||
|
type Target = Pattern;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A `tool.uv.sources` value.
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||||
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
|
#[serde(untagged, deny_unknown_fields)]
|
||||||
|
pub enum Source {
|
||||||
|
/// A remote Git repository, available over HTTPS or SSH.
|
||||||
|
///
|
||||||
|
/// Example:
|
||||||
|
/// ```toml
|
||||||
|
/// flask = { git = "https://github.com/pallets/flask", tag = "3.0.0" }
|
||||||
|
/// ```
|
||||||
|
Git {
|
||||||
|
/// The repository URL (without the `git+` prefix).
|
||||||
|
git: Url,
|
||||||
|
/// The path to the directory with the `pyproject.toml`, if it's not in the archive root.
|
||||||
|
subdirectory: Option<String>,
|
||||||
|
// Only one of the three may be used; we'll validate this later and emit a custom error.
|
||||||
|
rev: Option<String>,
|
||||||
|
tag: Option<String>,
|
||||||
|
branch: Option<String>,
|
||||||
|
},
|
||||||
|
/// A remote `http://` or `https://` URL, either a wheel (`.whl`) or a source distribution
|
||||||
|
/// (`.zip`, `.tar.gz`).
|
||||||
|
///
|
||||||
|
/// Example:
|
||||||
|
/// ```toml
|
||||||
|
/// flask = { url = "https://files.pythonhosted.org/packages/61/80/ffe1da13ad9300f87c93af113edd0638c75138c42a0994becfacac078c06/flask-3.0.3-py3-none-any.whl" }
|
||||||
|
/// ```
|
||||||
|
Url {
|
||||||
|
url: Url,
|
||||||
|
/// For source distributions, the path to the directory with the `pyproject.toml`, if it's
|
||||||
|
/// not in the archive root.
|
||||||
|
subdirectory: Option<String>,
|
||||||
|
},
|
||||||
|
/// The path to a dependency, either a wheel (a `.whl` file), source distribution (a `.zip` or
|
||||||
|
/// `.tag.gz` file), or source tree (i.e., a directory containing a `pyproject.toml` or
|
||||||
|
/// `setup.py` file in the root).
|
||||||
|
Path {
|
||||||
|
path: String,
|
||||||
|
/// `false` by default.
|
||||||
|
editable: Option<bool>,
|
||||||
|
},
|
||||||
|
/// A dependency pinned to a specific index, e.g., `torch` after setting `torch` to `https://download.pytorch.org/whl/cu118`.
|
||||||
|
Registry {
|
||||||
|
// TODO(konstin): The string is more-or-less a placeholder
|
||||||
|
index: String,
|
||||||
|
},
|
||||||
|
/// A dependency on another package in the workspace.
|
||||||
|
Workspace {
|
||||||
|
/// When set to `false`, the package will be fetched from the remote index, rather than
|
||||||
|
/// included as a workspace package.
|
||||||
|
workspace: bool,
|
||||||
|
/// `true` by default.
|
||||||
|
editable: Option<bool>,
|
||||||
|
},
|
||||||
|
/// A catch-all variant used to emit precise error messages when deserializing.
|
||||||
|
CatchAll {
|
||||||
|
git: String,
|
||||||
|
subdirectory: Option<String>,
|
||||||
|
rev: Option<String>,
|
||||||
|
tag: Option<String>,
|
||||||
|
branch: Option<String>,
|
||||||
|
url: String,
|
||||||
|
patch: String,
|
||||||
|
index: String,
|
||||||
|
workspace: bool,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
/// <https://github.com/serde-rs/serde/issues/1316#issue-332908452>
|
||||||
|
mod serde_from_and_to_string {
|
||||||
|
use std::fmt::Display;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use serde::{de, Deserialize, Deserializer, Serializer};
|
||||||
|
|
||||||
|
pub(super) fn serialize<T, S>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
T: Display,
|
||||||
|
S: Serializer,
|
||||||
|
{
|
||||||
|
serializer.collect_str(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn deserialize<'de, T, D>(deserializer: D) -> Result<T, D::Error>
|
||||||
|
where
|
||||||
|
T: FromStr,
|
||||||
|
T::Err: Display,
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
String::deserialize(deserializer)?
|
||||||
|
.parse()
|
||||||
|
.map_err(de::Error::custom)
|
||||||
|
}
|
||||||
|
}
|
241
crates/uv-distribution/src/requirement_lowering.rs
Normal file
241
crates/uv-distribution/src/requirement_lowering.rs
Normal file
|
@ -0,0 +1,241 @@
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
use std::io;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use path_absolutize::Absolutize;
|
||||||
|
use thiserror::Error;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
use pep440_rs::VersionSpecifiers;
|
||||||
|
use pep508_rs::{VerbatimUrl, VersionOrUrl};
|
||||||
|
use pypi_types::{Requirement, RequirementSource, VerbatimParsedUrl};
|
||||||
|
use uv_configuration::PreviewMode;
|
||||||
|
use uv_fs::Simplified;
|
||||||
|
use uv_git::GitReference;
|
||||||
|
use uv_normalize::PackageName;
|
||||||
|
use uv_warnings::warn_user_once;
|
||||||
|
|
||||||
|
use crate::pyproject::Source;
|
||||||
|
use crate::Workspace;
|
||||||
|
|
||||||
|
/// An error parsing and merging `tool.uv.sources` with
|
||||||
|
/// `project.{dependencies,optional-dependencies}`.
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum LoweringError {
|
||||||
|
#[error("Package is not included as workspace package in `tool.uv.workspace`")]
|
||||||
|
UndeclaredWorkspacePackage,
|
||||||
|
#[error("Can only specify one of: `rev`, `tag`, or `branch`")]
|
||||||
|
MoreThanOneGitRef,
|
||||||
|
#[error("Unable to combine options in `tool.uv.sources`")]
|
||||||
|
InvalidEntry,
|
||||||
|
#[error(transparent)]
|
||||||
|
InvalidUrl(#[from] url::ParseError),
|
||||||
|
#[error(transparent)]
|
||||||
|
InvalidVerbatimUrl(#[from] pep508_rs::VerbatimUrlError),
|
||||||
|
#[error("Can't combine URLs from both `project.dependencies` and `tool.uv.sources`")]
|
||||||
|
ConflictingUrls,
|
||||||
|
#[error("Could not normalize path: `{}`", _0.user_display())]
|
||||||
|
Absolutize(PathBuf, #[source] io::Error),
|
||||||
|
#[error("Fragments are not allowed in URLs: `{0}`")]
|
||||||
|
ForbiddenFragment(Url),
|
||||||
|
#[error("`workspace = false` is not yet supported")]
|
||||||
|
WorkspaceFalse,
|
||||||
|
#[error("`tool.uv.sources` is a preview feature; use `--preview` or set `UV_PREVIEW=1` to enable it")]
|
||||||
|
MissingPreview,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Combine `project.dependencies` or `project.optional-dependencies` with `tool.uv.sources`.
|
||||||
|
pub(crate) fn lower_requirement(
|
||||||
|
requirement: pep508_rs::Requirement<VerbatimParsedUrl>,
|
||||||
|
project_name: &PackageName,
|
||||||
|
project_dir: &Path,
|
||||||
|
project_sources: &BTreeMap<PackageName, Source>,
|
||||||
|
workspace: &Workspace,
|
||||||
|
preview: PreviewMode,
|
||||||
|
) -> Result<Requirement, LoweringError> {
|
||||||
|
let source = project_sources
|
||||||
|
.get(&requirement.name)
|
||||||
|
.or(workspace.sources().get(&requirement.name))
|
||||||
|
.cloned();
|
||||||
|
|
||||||
|
let workspace_package_declared =
|
||||||
|
// We require that when you use a package that's part of the workspace, ...
|
||||||
|
!workspace.packages().contains_key(&requirement.name)
|
||||||
|
// ... it must be declared as a workspace dependency (`workspace = true`), ...
|
||||||
|
|| matches!(
|
||||||
|
source,
|
||||||
|
Some(Source::Workspace {
|
||||||
|
// By using toml, we technically support `workspace = false`.
|
||||||
|
workspace: true,
|
||||||
|
..
|
||||||
|
})
|
||||||
|
)
|
||||||
|
// ... except for recursive self-inclusion (extras that activate other extras), e.g.
|
||||||
|
// `framework[machine_learning]` depends on `framework[cuda]`.
|
||||||
|
|| &requirement.name == project_name;
|
||||||
|
if !workspace_package_declared {
|
||||||
|
return Err(LoweringError::UndeclaredWorkspacePackage);
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(source) = source else {
|
||||||
|
let has_sources = !project_sources.is_empty() || !workspace.sources().is_empty();
|
||||||
|
// Support recursive editable inclusions.
|
||||||
|
if has_sources && requirement.version_or_url.is_none() && &requirement.name != project_name
|
||||||
|
{
|
||||||
|
warn_user_once!(
|
||||||
|
"Missing version constraint (e.g., a lower bound) for `{}`",
|
||||||
|
requirement.name
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Ok(Requirement::from(requirement));
|
||||||
|
};
|
||||||
|
|
||||||
|
if preview.is_disabled() {
|
||||||
|
return Err(LoweringError::MissingPreview);
|
||||||
|
}
|
||||||
|
|
||||||
|
let source = match source {
|
||||||
|
Source::Git {
|
||||||
|
git,
|
||||||
|
subdirectory,
|
||||||
|
rev,
|
||||||
|
tag,
|
||||||
|
branch,
|
||||||
|
} => {
|
||||||
|
if matches!(requirement.version_or_url, Some(VersionOrUrl::Url(_))) {
|
||||||
|
return Err(LoweringError::ConflictingUrls);
|
||||||
|
}
|
||||||
|
let reference = match (rev, tag, branch) {
|
||||||
|
(None, None, None) => GitReference::DefaultBranch,
|
||||||
|
(Some(rev), None, None) => {
|
||||||
|
if rev.starts_with("refs/") {
|
||||||
|
GitReference::NamedRef(rev.clone())
|
||||||
|
} else if rev.len() == 40 {
|
||||||
|
GitReference::FullCommit(rev.clone())
|
||||||
|
} else {
|
||||||
|
GitReference::ShortCommit(rev.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(None, Some(tag), None) => GitReference::Tag(tag),
|
||||||
|
(None, None, Some(branch)) => GitReference::Branch(branch),
|
||||||
|
_ => return Err(LoweringError::MoreThanOneGitRef),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create a PEP 508-compatible URL.
|
||||||
|
let mut url = Url::parse(&format!("git+{git}"))?;
|
||||||
|
if let Some(rev) = reference.as_str() {
|
||||||
|
url.set_path(&format!("{}@{}", url.path(), rev));
|
||||||
|
}
|
||||||
|
if let Some(subdirectory) = &subdirectory {
|
||||||
|
url.set_fragment(Some(&format!("subdirectory={subdirectory}")));
|
||||||
|
}
|
||||||
|
let url = VerbatimUrl::from_url(url);
|
||||||
|
|
||||||
|
let repository = git.clone();
|
||||||
|
|
||||||
|
RequirementSource::Git {
|
||||||
|
url,
|
||||||
|
repository,
|
||||||
|
reference,
|
||||||
|
precise: None,
|
||||||
|
subdirectory: subdirectory.map(PathBuf::from),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Source::Url { url, subdirectory } => {
|
||||||
|
if matches!(requirement.version_or_url, Some(VersionOrUrl::Url(_))) {
|
||||||
|
return Err(LoweringError::ConflictingUrls);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut verbatim_url = url.clone();
|
||||||
|
if verbatim_url.fragment().is_some() {
|
||||||
|
return Err(LoweringError::ForbiddenFragment(url));
|
||||||
|
}
|
||||||
|
if let Some(subdirectory) = &subdirectory {
|
||||||
|
verbatim_url.set_fragment(Some(subdirectory));
|
||||||
|
}
|
||||||
|
|
||||||
|
let verbatim_url = VerbatimUrl::from_url(verbatim_url);
|
||||||
|
RequirementSource::Url {
|
||||||
|
location: url,
|
||||||
|
subdirectory: subdirectory.map(PathBuf::from),
|
||||||
|
url: verbatim_url,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Source::Path { path, editable } => {
|
||||||
|
if matches!(requirement.version_or_url, Some(VersionOrUrl::Url(_))) {
|
||||||
|
return Err(LoweringError::ConflictingUrls);
|
||||||
|
}
|
||||||
|
path_source(path, project_dir, editable.unwrap_or(false))?
|
||||||
|
}
|
||||||
|
Source::Registry { index } => match requirement.version_or_url {
|
||||||
|
None => {
|
||||||
|
warn_user_once!(
|
||||||
|
"Missing version constraint (e.g., a lower bound) for `{}`",
|
||||||
|
requirement.name
|
||||||
|
);
|
||||||
|
RequirementSource::Registry {
|
||||||
|
specifier: VersionSpecifiers::empty(),
|
||||||
|
index: Some(index),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(VersionOrUrl::VersionSpecifier(version)) => RequirementSource::Registry {
|
||||||
|
specifier: version,
|
||||||
|
index: Some(index),
|
||||||
|
},
|
||||||
|
Some(VersionOrUrl::Url(_)) => return Err(LoweringError::ConflictingUrls),
|
||||||
|
},
|
||||||
|
Source::Workspace {
|
||||||
|
workspace: is_workspace,
|
||||||
|
editable,
|
||||||
|
} => {
|
||||||
|
if !is_workspace {
|
||||||
|
return Err(LoweringError::WorkspaceFalse);
|
||||||
|
}
|
||||||
|
if matches!(requirement.version_or_url, Some(VersionOrUrl::Url(_))) {
|
||||||
|
return Err(LoweringError::ConflictingUrls);
|
||||||
|
}
|
||||||
|
let path = workspace
|
||||||
|
.packages()
|
||||||
|
.get(&requirement.name)
|
||||||
|
.ok_or(LoweringError::UndeclaredWorkspacePackage)?
|
||||||
|
.clone();
|
||||||
|
path_source(path.root(), workspace.root(), editable.unwrap_or(true))?
|
||||||
|
}
|
||||||
|
Source::CatchAll { .. } => {
|
||||||
|
// Emit a dedicated error message, which is an improvement over Serde's default error.
|
||||||
|
return Err(LoweringError::InvalidEntry);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(Requirement {
|
||||||
|
name: requirement.name,
|
||||||
|
extras: requirement.extras,
|
||||||
|
marker: requirement.marker,
|
||||||
|
source,
|
||||||
|
origin: requirement.origin,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert a path string to a path section.
|
||||||
|
fn path_source(
|
||||||
|
path: impl AsRef<Path>,
|
||||||
|
project_dir: &Path,
|
||||||
|
editable: bool,
|
||||||
|
) -> Result<RequirementSource, LoweringError> {
|
||||||
|
let url = VerbatimUrl::parse_path(path.as_ref(), project_dir)?
|
||||||
|
.with_given(path.as_ref().to_string_lossy().to_string());
|
||||||
|
let path_buf = path.as_ref().to_path_buf();
|
||||||
|
let path_buf = path_buf
|
||||||
|
.absolutize_from(project_dir)
|
||||||
|
.map_err(|err| LoweringError::Absolutize(path.as_ref().to_path_buf(), err))?
|
||||||
|
.to_path_buf();
|
||||||
|
//if !editable {
|
||||||
|
// // TODO(konsti): Support this. Currently we support `{ workspace = true }`, but we don't
|
||||||
|
// // support `{ workspace = true, editable = false }` since we only collect editables.
|
||||||
|
// return Err(LoweringError::NonEditableWorkspaceDependency);
|
||||||
|
//}
|
||||||
|
Ok(RequirementSource::Path {
|
||||||
|
path: path_buf,
|
||||||
|
url,
|
||||||
|
editable,
|
||||||
|
})
|
||||||
|
}
|
|
@ -29,7 +29,7 @@ use uv_cache::{
|
||||||
use uv_client::{
|
use uv_client::{
|
||||||
CacheControl, CachedClientError, Connectivity, DataWithCachePolicy, RegistryClient,
|
CacheControl, CachedClientError, Connectivity, DataWithCachePolicy, RegistryClient,
|
||||||
};
|
};
|
||||||
use uv_configuration::{BuildKind, NoBuild};
|
use uv_configuration::{BuildKind, NoBuild, PreviewMode};
|
||||||
use uv_extract::hash::Hasher;
|
use uv_extract::hash::Hasher;
|
||||||
use uv_fs::{write_atomic, LockedFile};
|
use uv_fs::{write_atomic, LockedFile};
|
||||||
use uv_types::{BuildContext, SourceBuildTrait};
|
use uv_types::{BuildContext, SourceBuildTrait};
|
||||||
|
@ -39,7 +39,7 @@ use crate::error::Error;
|
||||||
use crate::git::{fetch_git_archive, resolve_precise};
|
use crate::git::{fetch_git_archive, resolve_precise};
|
||||||
use crate::source::built_wheel_metadata::BuiltWheelMetadata;
|
use crate::source::built_wheel_metadata::BuiltWheelMetadata;
|
||||||
use crate::source::revision::Revision;
|
use crate::source::revision::Revision;
|
||||||
use crate::{ArchiveMetadata, Reporter};
|
use crate::{ArchiveMetadata, Metadata, Reporter};
|
||||||
|
|
||||||
mod built_wheel_metadata;
|
mod built_wheel_metadata;
|
||||||
mod revision;
|
mod revision;
|
||||||
|
@ -48,6 +48,7 @@ mod revision;
|
||||||
pub(crate) struct SourceDistributionBuilder<'a, T: BuildContext> {
|
pub(crate) struct SourceDistributionBuilder<'a, T: BuildContext> {
|
||||||
build_context: &'a T,
|
build_context: &'a T,
|
||||||
reporter: Option<Arc<dyn Reporter>>,
|
reporter: Option<Arc<dyn Reporter>>,
|
||||||
|
preview_mode: PreviewMode,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The name of the file that contains the revision ID for a remote distribution, encoded via `MsgPack`.
|
/// The name of the file that contains the revision ID for a remote distribution, encoded via `MsgPack`.
|
||||||
|
@ -61,10 +62,11 @@ pub(crate) const METADATA: &str = "metadata.msgpack";
|
||||||
|
|
||||||
impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
|
impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
|
||||||
/// Initialize a [`SourceDistributionBuilder`] from a [`BuildContext`].
|
/// Initialize a [`SourceDistributionBuilder`] from a [`BuildContext`].
|
||||||
pub(crate) fn new(build_context: &'a T) -> Self {
|
pub(crate) fn new(build_context: &'a T, preview_mode: PreviewMode) -> Self {
|
||||||
Self {
|
Self {
|
||||||
build_context,
|
build_context,
|
||||||
reporter: None,
|
reporter: None,
|
||||||
|
preview_mode,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -492,7 +494,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
|
||||||
if let Some(metadata) = read_cached_metadata(&metadata_entry).await? {
|
if let Some(metadata) = read_cached_metadata(&metadata_entry).await? {
|
||||||
debug!("Using cached metadata for: {source}");
|
debug!("Using cached metadata for: {source}");
|
||||||
return Ok(ArchiveMetadata {
|
return Ok(ArchiveMetadata {
|
||||||
metadata,
|
metadata: Metadata::from_metadata23(metadata),
|
||||||
hashes: revision.into_hashes(),
|
hashes: revision.into_hashes(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -515,7 +517,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
|
||||||
.map_err(Error::CacheWrite)?;
|
.map_err(Error::CacheWrite)?;
|
||||||
|
|
||||||
return Ok(ArchiveMetadata {
|
return Ok(ArchiveMetadata {
|
||||||
metadata,
|
metadata: Metadata::from_metadata23(metadata),
|
||||||
hashes: revision.into_hashes(),
|
hashes: revision.into_hashes(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -542,7 +544,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(ArchiveMetadata {
|
Ok(ArchiveMetadata {
|
||||||
metadata,
|
metadata: Metadata::from_metadata23(metadata),
|
||||||
hashes: revision.into_hashes(),
|
hashes: revision.into_hashes(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -720,7 +722,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
|
||||||
if let Some(metadata) = read_cached_metadata(&metadata_entry).await? {
|
if let Some(metadata) = read_cached_metadata(&metadata_entry).await? {
|
||||||
debug!("Using cached metadata for: {source}");
|
debug!("Using cached metadata for: {source}");
|
||||||
return Ok(ArchiveMetadata {
|
return Ok(ArchiveMetadata {
|
||||||
metadata,
|
metadata: Metadata::from_metadata23(metadata),
|
||||||
hashes: revision.into_hashes(),
|
hashes: revision.into_hashes(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -742,7 +744,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
|
||||||
.map_err(Error::CacheWrite)?;
|
.map_err(Error::CacheWrite)?;
|
||||||
|
|
||||||
return Ok(ArchiveMetadata {
|
return Ok(ArchiveMetadata {
|
||||||
metadata,
|
metadata: Metadata::from_metadata23(metadata),
|
||||||
hashes: revision.into_hashes(),
|
hashes: revision.into_hashes(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -769,7 +771,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
|
||||||
.map_err(Error::CacheWrite)?;
|
.map_err(Error::CacheWrite)?;
|
||||||
|
|
||||||
Ok(ArchiveMetadata {
|
Ok(ArchiveMetadata {
|
||||||
metadata,
|
metadata: Metadata::from_metadata23(metadata),
|
||||||
hashes: revision.into_hashes(),
|
hashes: revision.into_hashes(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -929,7 +931,10 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
|
||||||
let metadata_entry = cache_shard.entry(METADATA);
|
let metadata_entry = cache_shard.entry(METADATA);
|
||||||
if let Some(metadata) = read_cached_metadata(&metadata_entry).await? {
|
if let Some(metadata) = read_cached_metadata(&metadata_entry).await? {
|
||||||
debug!("Using cached metadata for: {source}");
|
debug!("Using cached metadata for: {source}");
|
||||||
return Ok(ArchiveMetadata::from(metadata));
|
return Ok(ArchiveMetadata::from(
|
||||||
|
Metadata::from_workspace(metadata, resource.path.as_ref(), self.preview_mode)
|
||||||
|
.await?,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the backend supports `prepare_metadata_for_build_wheel`, use it.
|
// If the backend supports `prepare_metadata_for_build_wheel`, use it.
|
||||||
|
@ -946,7 +951,10 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
|
||||||
.await
|
.await
|
||||||
.map_err(Error::CacheWrite)?;
|
.map_err(Error::CacheWrite)?;
|
||||||
|
|
||||||
return Ok(ArchiveMetadata::from(metadata));
|
return Ok(ArchiveMetadata::from(
|
||||||
|
Metadata::from_workspace(metadata, resource.path.as_ref(), self.preview_mode)
|
||||||
|
.await?,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, we need to build a wheel.
|
// Otherwise, we need to build a wheel.
|
||||||
|
@ -970,7 +978,9 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
|
||||||
.await
|
.await
|
||||||
.map_err(Error::CacheWrite)?;
|
.map_err(Error::CacheWrite)?;
|
||||||
|
|
||||||
Ok(ArchiveMetadata::from(metadata))
|
Ok(ArchiveMetadata::from(
|
||||||
|
Metadata::from_workspace(metadata, resource.path.as_ref(), self.preview_mode).await?,
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the [`Revision`] for a local source tree, refreshing it if necessary.
|
/// Return the [`Revision`] for a local source tree, refreshing it if necessary.
|
||||||
|
@ -1137,7 +1147,9 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
|
||||||
{
|
{
|
||||||
if let Some(metadata) = read_cached_metadata(&metadata_entry).await? {
|
if let Some(metadata) = read_cached_metadata(&metadata_entry).await? {
|
||||||
debug!("Using cached metadata for: {source}");
|
debug!("Using cached metadata for: {source}");
|
||||||
return Ok(ArchiveMetadata::from(metadata));
|
return Ok(ArchiveMetadata::from(
|
||||||
|
Metadata::from_workspace(metadata, fetch.path(), self.preview_mode).await?,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1155,7 +1167,9 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
|
||||||
.await
|
.await
|
||||||
.map_err(Error::CacheWrite)?;
|
.map_err(Error::CacheWrite)?;
|
||||||
|
|
||||||
return Ok(ArchiveMetadata::from(metadata));
|
return Ok(ArchiveMetadata::from(
|
||||||
|
Metadata::from_workspace(metadata, fetch.path(), self.preview_mode).await?,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, we need to build a wheel.
|
// Otherwise, we need to build a wheel.
|
||||||
|
@ -1179,7 +1193,9 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
|
||||||
.await
|
.await
|
||||||
.map_err(Error::CacheWrite)?;
|
.map_err(Error::CacheWrite)?;
|
||||||
|
|
||||||
Ok(ArchiveMetadata::from(metadata))
|
Ok(ArchiveMetadata::from(
|
||||||
|
Metadata::from_workspace(metadata, fetch.path(), self.preview_mode).await?,
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Download and unzip a source distribution into the cache from an HTTP response.
|
/// Download and unzip a source distribution into the cache from an HTTP response.
|
||||||
|
@ -1592,7 +1608,7 @@ async fn read_pyproject_toml(
|
||||||
/// Read an existing cached [`Metadata23`], if it exists.
|
/// Read an existing cached [`Metadata23`], if it exists.
|
||||||
async fn read_cached_metadata(cache_entry: &CacheEntry) -> Result<Option<Metadata23>, Error> {
|
async fn read_cached_metadata(cache_entry: &CacheEntry) -> Result<Option<Metadata23>, Error> {
|
||||||
match fs::read(&cache_entry.path()).await {
|
match fs::read(&cache_entry.path()).await {
|
||||||
Ok(cached) => Ok(Some(rmp_serde::from_slice::<Metadata23>(&cached)?)),
|
Ok(cached) => Ok(Some(rmp_serde::from_slice(&cached)?)),
|
||||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None),
|
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None),
|
||||||
Err(err) => Err(Error::CacheRead(err)),
|
Err(err) => Err(Error::CacheRead(err)),
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,12 +3,12 @@
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use distribution_types::{Requirement, RequirementSource};
|
|
||||||
use glob::{glob, GlobError, PatternError};
|
use glob::{glob, GlobError, PatternError};
|
||||||
use pep508_rs::{VerbatimUrl, VerbatimUrlError};
|
|
||||||
use rustc_hash::FxHashSet;
|
use rustc_hash::FxHashSet;
|
||||||
use tracing::{debug, trace};
|
use tracing::{debug, trace};
|
||||||
|
|
||||||
|
use pep508_rs::VerbatimUrl;
|
||||||
|
use pypi_types::{Requirement, RequirementSource};
|
||||||
use uv_fs::{absolutize_path, Simplified};
|
use uv_fs::{absolutize_path, Simplified};
|
||||||
use uv_normalize::{ExtraName, PackageName};
|
use uv_normalize::{ExtraName, PackageName};
|
||||||
use uv_warnings::warn_user;
|
use uv_warnings::warn_user;
|
||||||
|
@ -29,12 +29,8 @@ pub enum WorkspaceError {
|
||||||
Toml(PathBuf, #[source] Box<toml::de::Error>),
|
Toml(PathBuf, #[source] Box<toml::de::Error>),
|
||||||
#[error("No `project` table found in: `{}`", _0.simplified_display())]
|
#[error("No `project` table found in: `{}`", _0.simplified_display())]
|
||||||
MissingProject(PathBuf),
|
MissingProject(PathBuf),
|
||||||
#[error("pyproject.toml section is declared as dynamic, but must be static: `{0}`")]
|
|
||||||
DynamicNotAllowed(&'static str),
|
|
||||||
#[error("Failed to normalize workspace member path")]
|
#[error("Failed to normalize workspace member path")]
|
||||||
Normalize(#[source] std::io::Error),
|
Normalize(#[source] std::io::Error),
|
||||||
#[error("Failed to normalize workspace member path")]
|
|
||||||
VerbatimUrl(#[from] VerbatimUrlError),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A workspace, consisting of a root directory and members. See [`ProjectWorkspace`].
|
/// A workspace, consisting of a root directory and members. See [`ProjectWorkspace`].
|
||||||
|
@ -371,6 +367,7 @@ impl ProjectWorkspace {
|
||||||
let mut seen = FxHashSet::default();
|
let mut seen = FxHashSet::default();
|
||||||
for member_glob in workspace_definition.members.unwrap_or_default() {
|
for member_glob in workspace_definition.members.unwrap_or_default() {
|
||||||
let absolute_glob = workspace_root
|
let absolute_glob = workspace_root
|
||||||
|
.simplified()
|
||||||
.join(member_glob.as_str())
|
.join(member_glob.as_str())
|
||||||
.to_string_lossy()
|
.to_string_lossy()
|
||||||
.to_string();
|
.to_string();
|
||||||
|
@ -427,8 +424,8 @@ impl ProjectWorkspace {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
/// Used in tests.
|
||||||
pub(crate) fn dummy(root: &Path, project_name: &PackageName) -> Self {
|
pub fn dummy(root: &Path, project_name: &PackageName) -> Self {
|
||||||
// This doesn't necessarily match the exact test case, but we don't use the other fields
|
// This doesn't necessarily match the exact test case, but we don't use the other fields
|
||||||
// for the test cases atm.
|
// for the test cases atm.
|
||||||
let root_member = WorkspaceMember {
|
let root_member = WorkspaceMember {
|
||||||
|
@ -436,9 +433,7 @@ impl ProjectWorkspace {
|
||||||
pyproject_toml: PyProjectToml {
|
pyproject_toml: PyProjectToml {
|
||||||
project: Some(crate::pyproject::Project {
|
project: Some(crate::pyproject::Project {
|
||||||
name: project_name.clone(),
|
name: project_name.clone(),
|
||||||
dependencies: None,
|
|
||||||
optional_dependencies: None,
|
optional_dependencies: None,
|
||||||
dynamic: None,
|
|
||||||
}),
|
}),
|
||||||
tool: None,
|
tool: None,
|
||||||
},
|
},
|
||||||
|
@ -605,6 +600,7 @@ fn is_excluded_from_workspace(
|
||||||
) -> Result<bool, WorkspaceError> {
|
) -> Result<bool, WorkspaceError> {
|
||||||
for exclude_glob in workspace.exclude.iter().flatten() {
|
for exclude_glob in workspace.exclude.iter().flatten() {
|
||||||
let absolute_glob = workspace_root
|
let absolute_glob = workspace_root
|
||||||
|
.simplified()
|
||||||
.join(exclude_glob.as_str())
|
.join(exclude_glob.as_str())
|
||||||
.to_string_lossy()
|
.to_string_lossy()
|
||||||
.to_string();
|
.to_string();
|
|
@ -12,23 +12,6 @@ pub use crate::path::*;
|
||||||
pub mod cachedir;
|
pub mod cachedir;
|
||||||
mod path;
|
mod path;
|
||||||
|
|
||||||
/// Reads data from the path and requires that it be valid UTF-8.
|
|
||||||
///
|
|
||||||
/// If the file path is `-`, then contents are read from stdin instead.
|
|
||||||
#[cfg(feature = "tokio")]
|
|
||||||
pub async fn read_to_string(path: impl AsRef<Path>) -> std::io::Result<String> {
|
|
||||||
use std::io::Read;
|
|
||||||
|
|
||||||
let path = path.as_ref();
|
|
||||||
if path == Path::new("-") {
|
|
||||||
let mut buf = String::with_capacity(1024);
|
|
||||||
std::io::stdin().read_to_string(&mut buf)?;
|
|
||||||
Ok(buf)
|
|
||||||
} else {
|
|
||||||
fs_err::tokio::read_to_string(path).await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Reads data from the path and requires that it be valid UTF-8 or UTF-16.
|
/// Reads data from the path and requires that it be valid UTF-8 or UTF-16.
|
||||||
///
|
///
|
||||||
/// This uses BOM sniffing to determine if the data should be transcoded
|
/// This uses BOM sniffing to determine if the data should be transcoded
|
||||||
|
|
|
@ -11,9 +11,10 @@ use distribution_filename::WheelFilename;
|
||||||
use distribution_types::{
|
use distribution_types::{
|
||||||
CachedDirectUrlDist, CachedDist, DirectUrlBuiltDist, DirectUrlSourceDist, DirectorySourceDist,
|
CachedDirectUrlDist, CachedDist, DirectUrlBuiltDist, DirectUrlSourceDist, DirectorySourceDist,
|
||||||
Error, GitSourceDist, Hashed, IndexLocations, InstalledDist, Name, PathBuiltDist,
|
Error, GitSourceDist, Hashed, IndexLocations, InstalledDist, Name, PathBuiltDist,
|
||||||
PathSourceDist, RemoteSource, Requirement, RequirementSource, Verbatim,
|
PathSourceDist, RemoteSource, Verbatim,
|
||||||
};
|
};
|
||||||
use platform_tags::Tags;
|
use platform_tags::Tags;
|
||||||
|
use pypi_types::{Requirement, RequirementSource};
|
||||||
use uv_cache::{ArchiveTimestamp, Cache, CacheBucket, WheelCache};
|
use uv_cache::{ArchiveTimestamp, Cache, CacheBucket, WheelCache};
|
||||||
use uv_configuration::{NoBinary, Reinstall};
|
use uv_configuration::{NoBinary, Reinstall};
|
||||||
use uv_distribution::{
|
use uv_distribution::{
|
||||||
|
|
|
@ -6,8 +6,8 @@ use serde::Deserialize;
|
||||||
use tracing::{debug, trace};
|
use tracing::{debug, trace};
|
||||||
|
|
||||||
use cache_key::{CanonicalUrl, RepositoryUrl};
|
use cache_key::{CanonicalUrl, RepositoryUrl};
|
||||||
use distribution_types::{InstalledDirectUrlDist, InstalledDist, RequirementSource};
|
use distribution_types::{InstalledDirectUrlDist, InstalledDist};
|
||||||
use pypi_types::{DirInfo, DirectUrl, VcsInfo, VcsKind};
|
use pypi_types::{DirInfo, DirectUrl, RequirementSource, VcsInfo, VcsKind};
|
||||||
use uv_cache::{ArchiveTarget, ArchiveTimestamp};
|
use uv_cache::{ArchiveTarget, ArchiveTimestamp};
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone)]
|
#[derive(Debug, Copy, Clone)]
|
||||||
|
|
|
@ -8,11 +8,10 @@ use rustc_hash::{FxHashMap, FxHashSet};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use distribution_types::{
|
use distribution_types::{
|
||||||
Diagnostic, InstalledDist, Name, Requirement, UnresolvedRequirement,
|
Diagnostic, InstalledDist, Name, UnresolvedRequirement, UnresolvedRequirementSpecification,
|
||||||
UnresolvedRequirementSpecification,
|
|
||||||
};
|
};
|
||||||
use pep440_rs::{Version, VersionSpecifiers};
|
use pep440_rs::{Version, VersionSpecifiers};
|
||||||
use pypi_types::VerbatimParsedUrl;
|
use pypi_types::{Requirement, VerbatimParsedUrl};
|
||||||
use uv_interpreter::PythonEnvironment;
|
use uv_interpreter::PythonEnvironment;
|
||||||
use uv_normalize::PackageName;
|
use uv_normalize::PackageName;
|
||||||
use uv_types::InstalledPackagesProvider;
|
use uv_types::InstalledPackagesProvider;
|
||||||
|
|
|
@ -13,7 +13,6 @@ license.workspace = true
|
||||||
cache-key = { workspace = true }
|
cache-key = { workspace = true }
|
||||||
distribution-filename = { workspace = true }
|
distribution-filename = { workspace = true }
|
||||||
distribution-types = { workspace = true }
|
distribution-types = { workspace = true }
|
||||||
pep440_rs = { workspace = true }
|
|
||||||
pep508_rs = { workspace = true }
|
pep508_rs = { workspace = true }
|
||||||
pypi-types = { workspace = true }
|
pypi-types = { workspace = true }
|
||||||
requirements-txt = { workspace = true, features = ["http"] }
|
requirements-txt = { workspace = true, features = ["http"] }
|
||||||
|
@ -34,26 +33,12 @@ console = { workspace = true }
|
||||||
ctrlc = { workspace = true }
|
ctrlc = { workspace = true }
|
||||||
fs-err = { workspace = true, features = ["tokio"] }
|
fs-err = { workspace = true, features = ["tokio"] }
|
||||||
futures = { workspace = true }
|
futures = { workspace = true }
|
||||||
glob = { workspace = true }
|
|
||||||
indexmap = { workspace = true }
|
|
||||||
path-absolutize = { workspace = true }
|
|
||||||
rustc-hash = { workspace = true }
|
rustc-hash = { workspace = true }
|
||||||
same-file = { workspace = true }
|
|
||||||
schemars = { workspace = true, optional = true }
|
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
toml = { workspace = true }
|
toml = { workspace = true }
|
||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
url = { workspace = true }
|
url = { workspace = true }
|
||||||
|
|
||||||
[features]
|
|
||||||
schemars = ["dep:schemars"]
|
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
indoc = "2.0.5"
|
|
||||||
insta = { version = "1.38.0", features = ["filters", "redactions", "json"] }
|
|
||||||
regex = { workspace = true }
|
|
||||||
tokio = { workspace = true }
|
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|
|
@ -3,14 +3,11 @@ pub use crate::source_tree::*;
|
||||||
pub use crate::sources::*;
|
pub use crate::sources::*;
|
||||||
pub use crate::specification::*;
|
pub use crate::specification::*;
|
||||||
pub use crate::unnamed::*;
|
pub use crate::unnamed::*;
|
||||||
pub use crate::workspace::*;
|
|
||||||
|
|
||||||
mod confirm;
|
mod confirm;
|
||||||
mod lookahead;
|
mod lookahead;
|
||||||
pub mod pyproject;
|
|
||||||
mod source_tree;
|
mod source_tree;
|
||||||
mod sources;
|
mod sources;
|
||||||
mod specification;
|
mod specification;
|
||||||
mod unnamed;
|
mod unnamed;
|
||||||
pub mod upgrade;
|
pub mod upgrade;
|
||||||
mod workspace;
|
|
||||||
|
|
|
@ -6,11 +6,9 @@ use rustc_hash::FxHashSet;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use tracing::trace;
|
use tracing::trace;
|
||||||
|
|
||||||
use distribution_types::{
|
use distribution_types::{BuiltDist, Dist, DistributionMetadata, GitSourceDist, SourceDist};
|
||||||
BuiltDist, Dist, DistributionMetadata, GitSourceDist, Requirement, RequirementSource,
|
|
||||||
SourceDist,
|
|
||||||
};
|
|
||||||
use pep508_rs::MarkerEnvironment;
|
use pep508_rs::MarkerEnvironment;
|
||||||
|
use pypi_types::{Requirement, RequirementSource};
|
||||||
use uv_configuration::{Constraints, Overrides};
|
use uv_configuration::{Constraints, Overrides};
|
||||||
use uv_distribution::{DistributionDatabase, Reporter};
|
use uv_distribution::{DistributionDatabase, Reporter};
|
||||||
use uv_git::GitUrl;
|
use uv_git::GitUrl;
|
||||||
|
|
|
@ -1,957 +0,0 @@
|
||||||
//! Reads the following fields from `pyproject.toml`:
|
|
||||||
//!
|
|
||||||
//! * `project.{dependencies,optional-dependencies}`
|
|
||||||
//! * `tool.uv.sources`
|
|
||||||
//! * `tool.uv.workspace`
|
|
||||||
//!
|
|
||||||
//! Then lowers them into a dependency specification.
|
|
||||||
|
|
||||||
use std::collections::BTreeMap;
|
|
||||||
use std::io;
|
|
||||||
use std::ops::Deref;
|
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use glob::Pattern;
|
|
||||||
use indexmap::IndexMap;
|
|
||||||
use path_absolutize::Absolutize;
|
|
||||||
use rustc_hash::FxHashSet;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use thiserror::Error;
|
|
||||||
use url::Url;
|
|
||||||
|
|
||||||
use distribution_types::{Requirement, RequirementSource, Requirements};
|
|
||||||
use pep440_rs::VersionSpecifiers;
|
|
||||||
use pep508_rs::{Pep508Error, RequirementOrigin, VerbatimUrl, VersionOrUrl};
|
|
||||||
use pypi_types::VerbatimParsedUrl;
|
|
||||||
use uv_configuration::{ExtrasSpecification, PreviewMode};
|
|
||||||
use uv_fs::Simplified;
|
|
||||||
use uv_git::GitReference;
|
|
||||||
use uv_normalize::{ExtraName, PackageName};
|
|
||||||
use uv_warnings::warn_user_once;
|
|
||||||
|
|
||||||
use crate::Workspace;
|
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
|
||||||
pub enum Pep621Error {
|
|
||||||
#[error(transparent)]
|
|
||||||
Pep508(#[from] Box<Pep508Error<VerbatimParsedUrl>>),
|
|
||||||
#[error("Must specify a `[project]` section alongside `[tool.uv.sources]`")]
|
|
||||||
MissingProjectSection,
|
|
||||||
#[error("pyproject.toml section is declared as dynamic, but must be static: `{0}`")]
|
|
||||||
DynamicNotAllowed(&'static str),
|
|
||||||
#[error("Failed to parse entry for: `{0}`")]
|
|
||||||
LoweringError(PackageName, #[source] LoweringError),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Pep508Error<VerbatimParsedUrl>> for Pep621Error {
|
|
||||||
fn from(error: Pep508Error<VerbatimParsedUrl>) -> Self {
|
|
||||||
Self::Pep508(Box::new(error))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// An error parsing and merging `tool.uv.sources` with
|
|
||||||
/// `project.{dependencies,optional-dependencies}`.
|
|
||||||
#[derive(Debug, Error)]
|
|
||||||
pub enum LoweringError {
|
|
||||||
#[error("Unsupported path (can't convert to URL): `{}`", _0.user_display())]
|
|
||||||
PathToUrl(PathBuf),
|
|
||||||
#[error("Package is not included as workspace package in `tool.uv.workspace`")]
|
|
||||||
UndeclaredWorkspacePackage,
|
|
||||||
#[error("Can only specify one of rev, tag, or branch")]
|
|
||||||
MoreThanOneGitRef,
|
|
||||||
#[error("Unable to combine options in `tool.uv.sources`")]
|
|
||||||
InvalidEntry,
|
|
||||||
#[error(transparent)]
|
|
||||||
InvalidUrl(#[from] url::ParseError),
|
|
||||||
#[error(transparent)]
|
|
||||||
InvalidVerbatimUrl(#[from] pep508_rs::VerbatimUrlError),
|
|
||||||
#[error("Can't combine URLs from both `project.dependencies` and `tool.uv.sources`")]
|
|
||||||
ConflictingUrls,
|
|
||||||
#[error("Could not normalize path: `{}`", _0.user_display())]
|
|
||||||
AbsolutizeError(PathBuf, #[source] io::Error),
|
|
||||||
#[error("Fragments are not allowed in URLs: `{0}`")]
|
|
||||||
ForbiddenFragment(Url),
|
|
||||||
#[error("`workspace = false` is not yet supported")]
|
|
||||||
WorkspaceFalse,
|
|
||||||
#[error("`tool.uv.sources` is a preview feature; use `--preview` or set `UV_PREVIEW=1` to enable it")]
|
|
||||||
MissingPreview,
|
|
||||||
#[error("`editable = false` is not yet supported")]
|
|
||||||
NonEditableWorkspaceDependency,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A `pyproject.toml` as specified in PEP 517.
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
|
||||||
#[serde(rename_all = "kebab-case")]
|
|
||||||
pub struct PyProjectToml {
|
|
||||||
/// PEP 621-compliant project metadata.
|
|
||||||
pub project: Option<Project>,
|
|
||||||
/// Tool-specific metadata.
|
|
||||||
pub tool: Option<Tool>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// PEP 621 project metadata (`project`).
|
|
||||||
///
|
|
||||||
/// This is a subset of the full metadata specification, and only includes the fields that are
|
|
||||||
/// relevant for extracting static requirements.
|
|
||||||
///
|
|
||||||
/// See <https://packaging.python.org/en/latest/specifications/pyproject-toml>.
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
|
||||||
#[serde(rename_all = "kebab-case")]
|
|
||||||
pub struct Project {
|
|
||||||
/// The name of the project
|
|
||||||
pub name: PackageName,
|
|
||||||
/// Project dependencies
|
|
||||||
pub dependencies: Option<Vec<String>>,
|
|
||||||
/// Optional dependencies
|
|
||||||
pub optional_dependencies: Option<IndexMap<ExtraName, Vec<String>>>,
|
|
||||||
/// Specifies which fields listed by PEP 621 were intentionally unspecified
|
|
||||||
/// so another tool can/will provide such metadata dynamically.
|
|
||||||
pub dynamic: Option<Vec<String>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
|
||||||
pub struct Tool {
|
|
||||||
pub uv: Option<ToolUv>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
|
||||||
pub struct ToolUv {
|
|
||||||
pub sources: Option<BTreeMap<PackageName, Source>>,
|
|
||||||
pub workspace: Option<ToolUvWorkspace>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
|
||||||
pub struct ToolUvWorkspace {
|
|
||||||
pub members: Option<Vec<SerdePattern>>,
|
|
||||||
pub exclude: Option<Vec<SerdePattern>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// (De)serialize globs as strings.
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
|
||||||
pub struct SerdePattern(#[serde(with = "serde_from_and_to_string")] pub Pattern);
|
|
||||||
|
|
||||||
#[cfg(feature = "schemars")]
|
|
||||||
impl schemars::JsonSchema for SerdePattern {
|
|
||||||
fn schema_name() -> String {
|
|
||||||
<String as schemars::JsonSchema>::schema_name()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
|
|
||||||
<String as schemars::JsonSchema>::json_schema(gen)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Deref for SerdePattern {
|
|
||||||
type Target = Pattern;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A `tool.uv.sources` value.
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
|
||||||
#[serde(untagged, deny_unknown_fields)]
|
|
||||||
pub enum Source {
|
|
||||||
/// A remote Git repository, available over HTTPS or SSH.
|
|
||||||
///
|
|
||||||
/// Example:
|
|
||||||
/// ```toml
|
|
||||||
/// flask = { git = "https://github.com/pallets/flask", tag = "3.0.0" }
|
|
||||||
/// ```
|
|
||||||
Git {
|
|
||||||
/// The repository URL (without the `git+` prefix).
|
|
||||||
git: Url,
|
|
||||||
/// The path to the directory with the `pyproject.toml`, if it's not in the archive root.
|
|
||||||
subdirectory: Option<String>,
|
|
||||||
// Only one of the three may be used; we'll validate this later and emit a custom error.
|
|
||||||
rev: Option<String>,
|
|
||||||
tag: Option<String>,
|
|
||||||
branch: Option<String>,
|
|
||||||
},
|
|
||||||
/// A remote `http://` or `https://` URL, either a wheel (`.whl`) or a source distribution
|
|
||||||
/// (`.zip`, `.tar.gz`).
|
|
||||||
///
|
|
||||||
/// Example:
|
|
||||||
/// ```toml
|
|
||||||
/// flask = { url = "https://files.pythonhosted.org/packages/61/80/ffe1da13ad9300f87c93af113edd0638c75138c42a0994becfacac078c06/flask-3.0.3-py3-none-any.whl" }
|
|
||||||
/// ```
|
|
||||||
Url {
|
|
||||||
url: Url,
|
|
||||||
/// For source distributions, the path to the directory with the `pyproject.toml`, if it's
|
|
||||||
/// not in the archive root.
|
|
||||||
subdirectory: Option<String>,
|
|
||||||
},
|
|
||||||
/// The path to a dependency, either a wheel (a `.whl` file), source distribution (a `.zip` or
|
|
||||||
/// `.tag.gz` file), or source tree (i.e., a directory containing a `pyproject.toml` or
|
|
||||||
/// `setup.py` file in the root).
|
|
||||||
Path {
|
|
||||||
path: String,
|
|
||||||
/// `false` by default.
|
|
||||||
editable: Option<bool>,
|
|
||||||
},
|
|
||||||
/// A dependency pinned to a specific index, e.g., `torch` after setting `torch` to `https://download.pytorch.org/whl/cu118`.
|
|
||||||
Registry {
|
|
||||||
// TODO(konstin): The string is more-or-less a placeholder
|
|
||||||
index: String,
|
|
||||||
},
|
|
||||||
/// A dependency on another package in the workspace.
|
|
||||||
Workspace {
|
|
||||||
/// When set to `false`, the package will be fetched from the remote index, rather than
|
|
||||||
/// included as a workspace package.
|
|
||||||
workspace: bool,
|
|
||||||
/// `true` by default.
|
|
||||||
editable: Option<bool>,
|
|
||||||
},
|
|
||||||
/// A catch-all variant used to emit precise error messages when deserializing.
|
|
||||||
CatchAll {
|
|
||||||
git: String,
|
|
||||||
subdirectory: Option<String>,
|
|
||||||
rev: Option<String>,
|
|
||||||
tag: Option<String>,
|
|
||||||
branch: Option<String>,
|
|
||||||
url: String,
|
|
||||||
patch: String,
|
|
||||||
index: String,
|
|
||||||
workspace: bool,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The PEP 621 project metadata, with static requirements extracted in advance, joined
|
|
||||||
/// with `tool.uv.sources`.
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub(crate) struct Pep621Metadata {
|
|
||||||
/// The name of the project.
|
|
||||||
pub(crate) name: PackageName,
|
|
||||||
/// The requirements extracted from the project.
|
|
||||||
pub(crate) requirements: Vec<Requirement>,
|
|
||||||
/// The extras used to collect requirements.
|
|
||||||
pub(crate) used_extras: FxHashSet<ExtraName>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Pep621Metadata {
|
|
||||||
/// Extract the static [`Pep621Metadata`] from a [`Project`] and [`ExtrasSpecification`], if
|
|
||||||
/// possible.
|
|
||||||
///
|
|
||||||
/// If the project specifies dynamic dependencies, or if the project specifies dynamic optional
|
|
||||||
/// dependencies and the extras are requested, the requirements cannot be extracted.
|
|
||||||
///
|
|
||||||
/// Returns an error if the requirements are not valid PEP 508 requirements.
|
|
||||||
pub(crate) fn try_from(
|
|
||||||
pyproject: &PyProjectToml,
|
|
||||||
extras: &ExtrasSpecification,
|
|
||||||
pyproject_path: &Path,
|
|
||||||
project_dir: &Path,
|
|
||||||
workspace: &Workspace,
|
|
||||||
preview: PreviewMode,
|
|
||||||
) -> Result<Option<Self>, Pep621Error> {
|
|
||||||
let project_sources = pyproject
|
|
||||||
.tool
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|tool| tool.uv.as_ref())
|
|
||||||
.and_then(|uv| uv.sources.clone());
|
|
||||||
|
|
||||||
let has_sources = project_sources.is_some() || !workspace.sources().is_empty();
|
|
||||||
|
|
||||||
let Some(project) = &pyproject.project else {
|
|
||||||
return if has_sources {
|
|
||||||
Err(Pep621Error::MissingProjectSection)
|
|
||||||
} else {
|
|
||||||
Ok(None)
|
|
||||||
};
|
|
||||||
};
|
|
||||||
if let Some(dynamic) = project.dynamic.as_ref() {
|
|
||||||
// If the project specifies dynamic dependencies, we can't extract the requirements.
|
|
||||||
if dynamic.iter().any(|field| field == "dependencies") {
|
|
||||||
return if has_sources {
|
|
||||||
Err(Pep621Error::DynamicNotAllowed("project.dependencies"))
|
|
||||||
} else {
|
|
||||||
Ok(None)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
// If we requested extras, and the project specifies dynamic optional dependencies, we can't
|
|
||||||
// extract the requirements.
|
|
||||||
if !extras.is_empty() && dynamic.iter().any(|field| field == "optional-dependencies") {
|
|
||||||
return if has_sources {
|
|
||||||
Err(Pep621Error::DynamicNotAllowed(
|
|
||||||
"project.optional-dependencies",
|
|
||||||
))
|
|
||||||
} else {
|
|
||||||
Ok(None)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let requirements = lower_requirements(
|
|
||||||
project.dependencies.as_deref(),
|
|
||||||
project.optional_dependencies.as_ref(),
|
|
||||||
pyproject_path,
|
|
||||||
&project.name,
|
|
||||||
project_dir,
|
|
||||||
&project_sources.unwrap_or_default(),
|
|
||||||
workspace,
|
|
||||||
preview,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
// Parse out the project requirements.
|
|
||||||
let mut requirements_with_extras = requirements.dependencies;
|
|
||||||
|
|
||||||
// Include any optional dependencies specified in `extras`.
|
|
||||||
let mut used_extras = FxHashSet::default();
|
|
||||||
if !extras.is_empty() {
|
|
||||||
// Include the optional dependencies if the extras are requested.
|
|
||||||
for (extra, optional_requirements) in &requirements.optional_dependencies {
|
|
||||||
if extras.contains(extra) {
|
|
||||||
used_extras.insert(extra.clone());
|
|
||||||
requirements_with_extras.extend(flatten_extra(
|
|
||||||
&project.name,
|
|
||||||
optional_requirements,
|
|
||||||
&requirements.optional_dependencies,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Some(Self {
|
|
||||||
name: project.name.clone(),
|
|
||||||
requirements: requirements_with_extras,
|
|
||||||
used_extras,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
pub(crate) fn lower_requirements(
|
|
||||||
dependencies: Option<&[String]>,
|
|
||||||
optional_dependencies: Option<&IndexMap<ExtraName, Vec<String>>>,
|
|
||||||
pyproject_path: &Path,
|
|
||||||
project_name: &PackageName,
|
|
||||||
project_dir: &Path,
|
|
||||||
project_sources: &BTreeMap<PackageName, Source>,
|
|
||||||
workspace: &Workspace,
|
|
||||||
preview: PreviewMode,
|
|
||||||
) -> Result<Requirements, Pep621Error> {
|
|
||||||
let dependencies = dependencies
|
|
||||||
.into_iter()
|
|
||||||
.flatten()
|
|
||||||
.map(|dependency| {
|
|
||||||
let requirement = pep508_rs::Requirement::from_str(dependency)?.with_origin(
|
|
||||||
RequirementOrigin::Project(pyproject_path.to_path_buf(), project_name.clone()),
|
|
||||||
);
|
|
||||||
let name = requirement.name.clone();
|
|
||||||
lower_requirement(
|
|
||||||
requirement,
|
|
||||||
project_name,
|
|
||||||
project_dir,
|
|
||||||
project_sources,
|
|
||||||
workspace,
|
|
||||||
preview,
|
|
||||||
)
|
|
||||||
.map_err(|err| Pep621Error::LoweringError(name, err))
|
|
||||||
})
|
|
||||||
.collect::<Result<_, Pep621Error>>()?;
|
|
||||||
let optional_dependencies = optional_dependencies
|
|
||||||
.into_iter()
|
|
||||||
.flatten()
|
|
||||||
.map(|(extra_name, dependencies)| {
|
|
||||||
let dependencies: Vec<_> = dependencies
|
|
||||||
.iter()
|
|
||||||
.map(|dependency| {
|
|
||||||
let requirement = pep508_rs::Requirement::from_str(dependency)?.with_origin(
|
|
||||||
RequirementOrigin::Project(
|
|
||||||
pyproject_path.to_path_buf(),
|
|
||||||
project_name.clone(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
let name = requirement.name.clone();
|
|
||||||
lower_requirement(
|
|
||||||
requirement,
|
|
||||||
project_name,
|
|
||||||
project_dir,
|
|
||||||
project_sources,
|
|
||||||
workspace,
|
|
||||||
preview,
|
|
||||||
)
|
|
||||||
.map_err(|err| Pep621Error::LoweringError(name, err))
|
|
||||||
})
|
|
||||||
.collect::<Result<_, Pep621Error>>()?;
|
|
||||||
Ok((extra_name.clone(), dependencies))
|
|
||||||
})
|
|
||||||
.collect::<Result<_, Pep621Error>>()?;
|
|
||||||
Ok(Requirements {
|
|
||||||
dependencies,
|
|
||||||
optional_dependencies,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Combine `project.dependencies` or `project.optional-dependencies` with `tool.uv.sources`.
|
|
||||||
pub(crate) fn lower_requirement(
|
|
||||||
requirement: pep508_rs::Requirement<VerbatimParsedUrl>,
|
|
||||||
project_name: &PackageName,
|
|
||||||
project_dir: &Path,
|
|
||||||
project_sources: &BTreeMap<PackageName, Source>,
|
|
||||||
workspace: &Workspace,
|
|
||||||
preview: PreviewMode,
|
|
||||||
) -> Result<Requirement, LoweringError> {
|
|
||||||
let source = project_sources
|
|
||||||
.get(&requirement.name)
|
|
||||||
.or(workspace.sources().get(&requirement.name))
|
|
||||||
.cloned();
|
|
||||||
|
|
||||||
let workspace_package_declared =
|
|
||||||
// We require that when you use a package that's part of the workspace, ...
|
|
||||||
!workspace.packages().contains_key(&requirement.name)
|
|
||||||
// ... it must be declared as a workspace dependency (`workspace = true`), ...
|
|
||||||
|| matches!(
|
|
||||||
source,
|
|
||||||
Some(Source::Workspace {
|
|
||||||
// By using toml, we technically support `workspace = false`.
|
|
||||||
workspace: true,
|
|
||||||
..
|
|
||||||
})
|
|
||||||
)
|
|
||||||
// ... except for recursive self-inclusion (extras that activate other extras), e.g.
|
|
||||||
// `framework[machine_learning]` depends on `framework[cuda]`.
|
|
||||||
|| &requirement.name == project_name;
|
|
||||||
if !workspace_package_declared {
|
|
||||||
return Err(LoweringError::UndeclaredWorkspacePackage);
|
|
||||||
}
|
|
||||||
|
|
||||||
let Some(source) = source else {
|
|
||||||
let has_sources = !project_sources.is_empty() || !workspace.sources().is_empty();
|
|
||||||
// Support recursive editable inclusions.
|
|
||||||
if has_sources && requirement.version_or_url.is_none() && &requirement.name != project_name
|
|
||||||
{
|
|
||||||
warn_user_once!(
|
|
||||||
"Missing version constraint (e.g., a lower bound) for `{}`",
|
|
||||||
requirement.name
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return Ok(Requirement::from(requirement));
|
|
||||||
};
|
|
||||||
|
|
||||||
if preview.is_disabled() {
|
|
||||||
return Err(LoweringError::MissingPreview);
|
|
||||||
}
|
|
||||||
|
|
||||||
let source = match source {
|
|
||||||
Source::Git {
|
|
||||||
git,
|
|
||||||
subdirectory,
|
|
||||||
rev,
|
|
||||||
tag,
|
|
||||||
branch,
|
|
||||||
} => {
|
|
||||||
if matches!(requirement.version_or_url, Some(VersionOrUrl::Url(_))) {
|
|
||||||
return Err(LoweringError::ConflictingUrls);
|
|
||||||
}
|
|
||||||
let reference = match (rev, tag, branch) {
|
|
||||||
(None, None, None) => GitReference::DefaultBranch,
|
|
||||||
(Some(rev), None, None) => {
|
|
||||||
if rev.starts_with("refs/") {
|
|
||||||
GitReference::NamedRef(rev.clone())
|
|
||||||
} else if rev.len() == 40 {
|
|
||||||
GitReference::FullCommit(rev.clone())
|
|
||||||
} else {
|
|
||||||
GitReference::ShortCommit(rev.clone())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
(None, Some(tag), None) => GitReference::Tag(tag),
|
|
||||||
(None, None, Some(branch)) => GitReference::Branch(branch),
|
|
||||||
_ => return Err(LoweringError::MoreThanOneGitRef),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create a PEP 508-compatible URL.
|
|
||||||
let mut url = Url::parse(&format!("git+{git}"))?;
|
|
||||||
if let Some(rev) = reference.as_str() {
|
|
||||||
url.set_path(&format!("{}@{}", url.path(), rev));
|
|
||||||
}
|
|
||||||
if let Some(subdirectory) = &subdirectory {
|
|
||||||
url.set_fragment(Some(&format!("subdirectory={subdirectory}")));
|
|
||||||
}
|
|
||||||
let url = VerbatimUrl::from_url(url);
|
|
||||||
|
|
||||||
let repository = git.clone();
|
|
||||||
|
|
||||||
RequirementSource::Git {
|
|
||||||
url,
|
|
||||||
repository,
|
|
||||||
reference,
|
|
||||||
precise: None,
|
|
||||||
subdirectory: subdirectory.map(PathBuf::from),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Source::Url { url, subdirectory } => {
|
|
||||||
if matches!(requirement.version_or_url, Some(VersionOrUrl::Url(_))) {
|
|
||||||
return Err(LoweringError::ConflictingUrls);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut verbatim_url = url.clone();
|
|
||||||
if verbatim_url.fragment().is_some() {
|
|
||||||
return Err(LoweringError::ForbiddenFragment(url));
|
|
||||||
}
|
|
||||||
if let Some(subdirectory) = &subdirectory {
|
|
||||||
verbatim_url.set_fragment(Some(subdirectory));
|
|
||||||
}
|
|
||||||
|
|
||||||
let verbatim_url = VerbatimUrl::from_url(verbatim_url);
|
|
||||||
RequirementSource::Url {
|
|
||||||
location: url,
|
|
||||||
subdirectory: subdirectory.map(PathBuf::from),
|
|
||||||
url: verbatim_url,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Source::Path { path, editable } => {
|
|
||||||
if matches!(requirement.version_or_url, Some(VersionOrUrl::Url(_))) {
|
|
||||||
return Err(LoweringError::ConflictingUrls);
|
|
||||||
}
|
|
||||||
path_source(path, project_dir, editable.unwrap_or(false))?
|
|
||||||
}
|
|
||||||
Source::Registry { index } => match requirement.version_or_url {
|
|
||||||
None => {
|
|
||||||
warn_user_once!(
|
|
||||||
"Missing version constraint (e.g., a lower bound) for `{}`",
|
|
||||||
requirement.name
|
|
||||||
);
|
|
||||||
RequirementSource::Registry {
|
|
||||||
specifier: VersionSpecifiers::empty(),
|
|
||||||
index: Some(index),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some(VersionOrUrl::VersionSpecifier(version)) => RequirementSource::Registry {
|
|
||||||
specifier: version,
|
|
||||||
index: Some(index),
|
|
||||||
},
|
|
||||||
Some(VersionOrUrl::Url(_)) => return Err(LoweringError::ConflictingUrls),
|
|
||||||
},
|
|
||||||
Source::Workspace {
|
|
||||||
workspace: is_workspace,
|
|
||||||
editable,
|
|
||||||
} => {
|
|
||||||
if !is_workspace {
|
|
||||||
return Err(LoweringError::WorkspaceFalse);
|
|
||||||
}
|
|
||||||
if matches!(requirement.version_or_url, Some(VersionOrUrl::Url(_))) {
|
|
||||||
return Err(LoweringError::ConflictingUrls);
|
|
||||||
}
|
|
||||||
let path = workspace
|
|
||||||
.packages()
|
|
||||||
.get(&requirement.name)
|
|
||||||
.ok_or(LoweringError::UndeclaredWorkspacePackage)?
|
|
||||||
.clone();
|
|
||||||
path_source(path.root(), workspace.root(), editable.unwrap_or(true))?
|
|
||||||
}
|
|
||||||
Source::CatchAll { .. } => {
|
|
||||||
// Emit a dedicated error message, which is an improvement over Serde's default error.
|
|
||||||
return Err(LoweringError::InvalidEntry);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Ok(Requirement {
|
|
||||||
name: requirement.name,
|
|
||||||
extras: requirement.extras,
|
|
||||||
marker: requirement.marker,
|
|
||||||
source,
|
|
||||||
origin: requirement.origin,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Convert a path string to a path section.
|
|
||||||
fn path_source(
|
|
||||||
path: impl AsRef<Path>,
|
|
||||||
project_dir: &Path,
|
|
||||||
editable: bool,
|
|
||||||
) -> Result<RequirementSource, LoweringError> {
|
|
||||||
let url = VerbatimUrl::parse_path(path.as_ref(), project_dir)?
|
|
||||||
.with_given(path.as_ref().to_string_lossy().to_string());
|
|
||||||
let path_buf = path.as_ref().to_path_buf();
|
|
||||||
let path_buf = path_buf
|
|
||||||
.absolutize_from(project_dir)
|
|
||||||
.map_err(|err| LoweringError::AbsolutizeError(path.as_ref().to_path_buf(), err))?
|
|
||||||
.to_path_buf();
|
|
||||||
if !editable {
|
|
||||||
// TODO(konsti): Support this. Currently we support `{ workspace = true }`, but we don't
|
|
||||||
// support `{ workspace = true, editable = false }` since we only collect editables.
|
|
||||||
return Err(LoweringError::NonEditableWorkspaceDependency);
|
|
||||||
}
|
|
||||||
Ok(RequirementSource::Path {
|
|
||||||
path: path_buf,
|
|
||||||
url,
|
|
||||||
editable,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Given an extra in a project that may contain references to the project itself, flatten it into
|
|
||||||
/// a list of requirements.
|
|
||||||
///
|
|
||||||
/// For example:
|
|
||||||
/// ```toml
|
|
||||||
/// [project]
|
|
||||||
/// name = "my-project"
|
|
||||||
/// version = "0.0.1"
|
|
||||||
/// dependencies = [
|
|
||||||
/// "tomli",
|
|
||||||
/// ]
|
|
||||||
///
|
|
||||||
/// [project.optional-dependencies]
|
|
||||||
/// test = [
|
|
||||||
/// "pep517",
|
|
||||||
/// ]
|
|
||||||
/// dev = [
|
|
||||||
/// "my-project[test]",
|
|
||||||
/// ]
|
|
||||||
/// ```
|
|
||||||
fn flatten_extra(
|
|
||||||
project_name: &PackageName,
|
|
||||||
requirements: &[Requirement],
|
|
||||||
extras: &IndexMap<ExtraName, Vec<Requirement>>,
|
|
||||||
) -> Vec<Requirement> {
|
|
||||||
fn inner(
|
|
||||||
project_name: &PackageName,
|
|
||||||
requirements: &[Requirement],
|
|
||||||
extras: &IndexMap<ExtraName, Vec<Requirement>>,
|
|
||||||
seen: &mut FxHashSet<ExtraName>,
|
|
||||||
) -> Vec<Requirement> {
|
|
||||||
let mut flattened = Vec::with_capacity(requirements.len());
|
|
||||||
for requirement in requirements {
|
|
||||||
if requirement.name == *project_name {
|
|
||||||
for extra in &requirement.extras {
|
|
||||||
// Avoid infinite recursion on mutually recursive extras.
|
|
||||||
if !seen.insert(extra.clone()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Flatten the extra requirements.
|
|
||||||
for (other_extra, extra_requirements) in extras {
|
|
||||||
if other_extra == extra {
|
|
||||||
flattened.extend(inner(project_name, extra_requirements, extras, seen));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
flattened.push(requirement.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
flattened
|
|
||||||
}
|
|
||||||
|
|
||||||
inner(
|
|
||||||
project_name,
|
|
||||||
requirements,
|
|
||||||
extras,
|
|
||||||
&mut FxHashSet::default(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <https://github.com/serde-rs/serde/issues/1316#issue-332908452>
|
|
||||||
mod serde_from_and_to_string {
|
|
||||||
use std::fmt::Display;
|
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use serde::{de, Deserialize, Deserializer, Serializer};
|
|
||||||
|
|
||||||
pub(super) fn serialize<T, S>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
|
|
||||||
where
|
|
||||||
T: Display,
|
|
||||||
S: Serializer,
|
|
||||||
{
|
|
||||||
serializer.collect_str(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn deserialize<'de, T, D>(deserializer: D) -> Result<T, D::Error>
|
|
||||||
where
|
|
||||||
T: FromStr,
|
|
||||||
T::Err: Display,
|
|
||||||
D: Deserializer<'de>,
|
|
||||||
{
|
|
||||||
String::deserialize(deserializer)?
|
|
||||||
.parse()
|
|
||||||
.map_err(de::Error::custom)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod test {
|
|
||||||
use std::path::Path;
|
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use anyhow::Context;
|
|
||||||
use indoc::indoc;
|
|
||||||
use insta::assert_snapshot;
|
|
||||||
|
|
||||||
use uv_configuration::{ExtrasSpecification, PreviewMode};
|
|
||||||
use uv_fs::Simplified;
|
|
||||||
use uv_normalize::PackageName;
|
|
||||||
|
|
||||||
use crate::ProjectWorkspace;
|
|
||||||
use crate::RequirementsSpecification;
|
|
||||||
|
|
||||||
fn from_source(
|
|
||||||
contents: &str,
|
|
||||||
path: impl AsRef<Path>,
|
|
||||||
extras: &ExtrasSpecification,
|
|
||||||
) -> anyhow::Result<RequirementsSpecification> {
|
|
||||||
let path = uv_fs::absolutize_path(path.as_ref())?;
|
|
||||||
let project_workspace =
|
|
||||||
ProjectWorkspace::dummy(path.as_ref(), &PackageName::from_str("foo").unwrap());
|
|
||||||
let pyproject_toml =
|
|
||||||
toml::from_str(contents).context("Failed to parse: `pyproject.toml`")?;
|
|
||||||
RequirementsSpecification::parse_direct_pyproject_toml(
|
|
||||||
&pyproject_toml,
|
|
||||||
project_workspace.workspace(),
|
|
||||||
extras,
|
|
||||||
path.as_ref(),
|
|
||||||
PreviewMode::Enabled,
|
|
||||||
)
|
|
||||||
.with_context(|| format!("Failed to parse: `{}`", path.user_display()))?
|
|
||||||
.context("Missing workspace")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn format_err(input: &str) -> String {
|
|
||||||
let err = from_source(input, "pyproject.toml", &ExtrasSpecification::None).unwrap_err();
|
|
||||||
let mut causes = err.chain();
|
|
||||||
let mut message = String::new();
|
|
||||||
message.push_str(&format!("error: {}\n", causes.next().unwrap()));
|
|
||||||
for err in causes {
|
|
||||||
message.push_str(&format!(" Caused by: {err}\n"));
|
|
||||||
}
|
|
||||||
message
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn conflict_project_and_sources() {
|
|
||||||
let input = indoc! {r#"
|
|
||||||
[project]
|
|
||||||
name = "foo"
|
|
||||||
version = "0.0.0"
|
|
||||||
dependencies = [
|
|
||||||
"tqdm @ git+https://github.com/tqdm/tqdm",
|
|
||||||
]
|
|
||||||
|
|
||||||
[tool.uv.sources]
|
|
||||||
tqdm = { url = "https://files.pythonhosted.org/packages/a5/d6/502a859bac4ad5e274255576cd3e15ca273cdb91731bc39fb840dd422ee9/tqdm-4.66.0-py3-none-any.whl" }
|
|
||||||
"#};
|
|
||||||
|
|
||||||
assert_snapshot!(format_err(input), @r###"
|
|
||||||
error: Failed to parse: `pyproject.toml`
|
|
||||||
Caused by: Failed to parse entry for: `tqdm`
|
|
||||||
Caused by: Can't combine URLs from both `project.dependencies` and `tool.uv.sources`
|
|
||||||
"###);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn too_many_git_specs() {
|
|
||||||
let input = indoc! {r#"
|
|
||||||
[project]
|
|
||||||
name = "foo"
|
|
||||||
version = "0.0.0"
|
|
||||||
dependencies = [
|
|
||||||
"tqdm",
|
|
||||||
]
|
|
||||||
|
|
||||||
[tool.uv.sources]
|
|
||||||
tqdm = { git = "https://github.com/tqdm/tqdm", rev = "baaaaaab", tag = "v1.0.0" }
|
|
||||||
"#};
|
|
||||||
|
|
||||||
assert_snapshot!(format_err(input), @r###"
|
|
||||||
error: Failed to parse: `pyproject.toml`
|
|
||||||
Caused by: Failed to parse entry for: `tqdm`
|
|
||||||
Caused by: Can only specify one of rev, tag, or branch
|
|
||||||
"###);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn too_many_git_typo() {
|
|
||||||
let input = indoc! {r#"
|
|
||||||
[project]
|
|
||||||
name = "foo"
|
|
||||||
version = "0.0.0"
|
|
||||||
dependencies = [
|
|
||||||
"tqdm",
|
|
||||||
]
|
|
||||||
|
|
||||||
[tool.uv.sources]
|
|
||||||
tqdm = { git = "https://github.com/tqdm/tqdm", ref = "baaaaaab" }
|
|
||||||
"#};
|
|
||||||
|
|
||||||
// TODO(konsti): This should tell you the set of valid fields
|
|
||||||
assert_snapshot!(format_err(input), @r###"
|
|
||||||
error: Failed to parse: `pyproject.toml`
|
|
||||||
Caused by: TOML parse error at line 9, column 8
|
|
||||||
|
|
|
||||||
9 | tqdm = { git = "https://github.com/tqdm/tqdm", ref = "baaaaaab" }
|
|
||||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
data did not match any variant of untagged enum Source
|
|
||||||
|
|
||||||
"###);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn you_cant_mix_those() {
|
|
||||||
let input = indoc! {r#"
|
|
||||||
[project]
|
|
||||||
name = "foo"
|
|
||||||
version = "0.0.0"
|
|
||||||
dependencies = [
|
|
||||||
"tqdm",
|
|
||||||
]
|
|
||||||
|
|
||||||
[tool.uv.sources]
|
|
||||||
tqdm = { path = "tqdm", index = "torch" }
|
|
||||||
"#};
|
|
||||||
|
|
||||||
// TODO(konsti): This should tell you the set of valid fields
|
|
||||||
assert_snapshot!(format_err(input), @r###"
|
|
||||||
error: Failed to parse: `pyproject.toml`
|
|
||||||
Caused by: TOML parse error at line 9, column 8
|
|
||||||
|
|
|
||||||
9 | tqdm = { path = "tqdm", index = "torch" }
|
|
||||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
data did not match any variant of untagged enum Source
|
|
||||||
|
|
||||||
"###);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn missing_constraint() {
|
|
||||||
let input = indoc! {r#"
|
|
||||||
[project]
|
|
||||||
name = "foo"
|
|
||||||
version = "0.0.0"
|
|
||||||
dependencies = [
|
|
||||||
"tqdm",
|
|
||||||
]
|
|
||||||
"#};
|
|
||||||
assert!(from_source(input, "pyproject.toml", &ExtrasSpecification::None).is_ok());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn invalid_syntax() {
|
|
||||||
let input = indoc! {r#"
|
|
||||||
[project]
|
|
||||||
name = "foo"
|
|
||||||
version = "0.0.0"
|
|
||||||
dependencies = [
|
|
||||||
"tqdm ==4.66.0",
|
|
||||||
]
|
|
||||||
|
|
||||||
[tool.uv.sources]
|
|
||||||
tqdm = { url = invalid url to tqdm-4.66.0-py3-none-any.whl" }
|
|
||||||
"#};
|
|
||||||
|
|
||||||
assert_snapshot!(format_err(input), @r###"
|
|
||||||
error: Failed to parse: `pyproject.toml`
|
|
||||||
Caused by: TOML parse error at line 9, column 16
|
|
||||||
|
|
|
||||||
9 | tqdm = { url = invalid url to tqdm-4.66.0-py3-none-any.whl" }
|
|
||||||
| ^
|
|
||||||
invalid string
|
|
||||||
expected `"`, `'`
|
|
||||||
|
|
||||||
"###);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn invalid_url() {
|
|
||||||
let input = indoc! {r#"
|
|
||||||
[project]
|
|
||||||
name = "foo"
|
|
||||||
version = "0.0.0"
|
|
||||||
dependencies = [
|
|
||||||
"tqdm ==4.66.0",
|
|
||||||
]
|
|
||||||
|
|
||||||
[tool.uv.sources]
|
|
||||||
tqdm = { url = "§invalid#+#*Ä" }
|
|
||||||
"#};
|
|
||||||
|
|
||||||
assert_snapshot!(format_err(input), @r###"
|
|
||||||
error: Failed to parse: `pyproject.toml`
|
|
||||||
Caused by: TOML parse error at line 9, column 8
|
|
||||||
|
|
|
||||||
9 | tqdm = { url = "§invalid#+#*Ä" }
|
|
||||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
data did not match any variant of untagged enum Source
|
|
||||||
|
|
||||||
"###);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn workspace_and_url_spec() {
|
|
||||||
let input = indoc! {r#"
|
|
||||||
[project]
|
|
||||||
name = "foo"
|
|
||||||
version = "0.0.0"
|
|
||||||
dependencies = [
|
|
||||||
"tqdm @ git+https://github.com/tqdm/tqdm",
|
|
||||||
]
|
|
||||||
|
|
||||||
[tool.uv.sources]
|
|
||||||
tqdm = { workspace = true }
|
|
||||||
"#};
|
|
||||||
|
|
||||||
assert_snapshot!(format_err(input), @r###"
|
|
||||||
error: Failed to parse: `pyproject.toml`
|
|
||||||
Caused by: Failed to parse entry for: `tqdm`
|
|
||||||
Caused by: Can't combine URLs from both `project.dependencies` and `tool.uv.sources`
|
|
||||||
"###);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn missing_workspace_package() {
|
|
||||||
let input = indoc! {r#"
|
|
||||||
[project]
|
|
||||||
name = "foo"
|
|
||||||
version = "0.0.0"
|
|
||||||
dependencies = [
|
|
||||||
"tqdm ==4.66.0",
|
|
||||||
]
|
|
||||||
|
|
||||||
[tool.uv.sources]
|
|
||||||
tqdm = { workspace = true }
|
|
||||||
"#};
|
|
||||||
|
|
||||||
assert_snapshot!(format_err(input), @r###"
|
|
||||||
error: Failed to parse: `pyproject.toml`
|
|
||||||
Caused by: Failed to parse entry for: `tqdm`
|
|
||||||
Caused by: Package is not included as workspace package in `tool.uv.workspace`
|
|
||||||
"###);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn cant_be_dynamic() {
|
|
||||||
let input = indoc! {r#"
|
|
||||||
[project]
|
|
||||||
name = "foo"
|
|
||||||
version = "0.0.0"
|
|
||||||
dynamic = [
|
|
||||||
"dependencies"
|
|
||||||
]
|
|
||||||
|
|
||||||
[tool.uv.sources]
|
|
||||||
tqdm = { workspace = true }
|
|
||||||
"#};
|
|
||||||
|
|
||||||
assert_snapshot!(format_err(input), @r###"
|
|
||||||
error: Failed to parse: `pyproject.toml`
|
|
||||||
Caused by: pyproject.toml section is declared as dynamic, but must be static: `project.dependencies`
|
|
||||||
"###);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn missing_project_section() {
|
|
||||||
let input = indoc! {"
|
|
||||||
[tool.uv.sources]
|
|
||||||
tqdm = { workspace = true }
|
|
||||||
"};
|
|
||||||
|
|
||||||
assert_snapshot!(format_err(input), @r###"
|
|
||||||
error: Failed to parse: `pyproject.toml`
|
|
||||||
Caused by: Must specify a `[project]` section alongside `[tool.uv.sources]`
|
|
||||||
"###);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -7,17 +7,26 @@ use futures::stream::FuturesOrdered;
|
||||||
use futures::TryStreamExt;
|
use futures::TryStreamExt;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use distribution_types::{
|
use distribution_types::{BuildableSource, DirectorySourceUrl, HashPolicy, SourceUrl, VersionId};
|
||||||
BuildableSource, DirectorySourceUrl, HashPolicy, Requirement, SourceUrl, VersionId,
|
|
||||||
};
|
|
||||||
use pep508_rs::RequirementOrigin;
|
use pep508_rs::RequirementOrigin;
|
||||||
use pypi_types::VerbatimParsedUrl;
|
use pypi_types::Requirement;
|
||||||
use uv_configuration::ExtrasSpecification;
|
use uv_configuration::ExtrasSpecification;
|
||||||
use uv_distribution::{DistributionDatabase, Reporter};
|
use uv_distribution::{DistributionDatabase, Reporter};
|
||||||
use uv_fs::Simplified;
|
use uv_fs::Simplified;
|
||||||
|
use uv_normalize::{ExtraName, PackageName};
|
||||||
use uv_resolver::{InMemoryIndex, MetadataResponse};
|
use uv_resolver::{InMemoryIndex, MetadataResponse};
|
||||||
use uv_types::{BuildContext, HashStrategy};
|
use uv_types::{BuildContext, HashStrategy};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct SourceTreeResolution {
|
||||||
|
/// The requirements sourced from the source trees.
|
||||||
|
pub requirements: Vec<Requirement>,
|
||||||
|
/// The names of the projects that were resolved.
|
||||||
|
pub project: PackageName,
|
||||||
|
/// The extras used when resolving the requirements.
|
||||||
|
pub extras: Vec<ExtraName>,
|
||||||
|
}
|
||||||
|
|
||||||
/// A resolver for requirements specified via source trees.
|
/// A resolver for requirements specified via source trees.
|
||||||
///
|
///
|
||||||
/// Used, e.g., to determine the input requirements when a user specifies a `pyproject.toml`
|
/// Used, e.g., to determine the input requirements when a user specifies a `pyproject.toml`
|
||||||
|
@ -63,26 +72,19 @@ impl<'a, Context: BuildContext> SourceTreeResolver<'a, Context> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resolve the requirements from the provided source trees.
|
/// Resolve the requirements from the provided source trees.
|
||||||
pub async fn resolve(self) -> Result<Vec<Requirement>> {
|
pub async fn resolve(self) -> Result<Vec<SourceTreeResolution>> {
|
||||||
let requirements: Vec<_> = self
|
let resolutions: Vec<_> = self
|
||||||
.source_trees
|
.source_trees
|
||||||
.iter()
|
.iter()
|
||||||
.map(|source_tree| async { self.resolve_source_tree(source_tree).await })
|
.map(|source_tree| async { self.resolve_source_tree(source_tree).await })
|
||||||
.collect::<FuturesOrdered<_>>()
|
.collect::<FuturesOrdered<_>>()
|
||||||
.try_collect()
|
.try_collect()
|
||||||
.await?;
|
.await?;
|
||||||
Ok(requirements
|
Ok(resolutions)
|
||||||
.into_iter()
|
|
||||||
.flatten()
|
|
||||||
.map(Requirement::from)
|
|
||||||
.collect())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Infer the package name for a given "unnamed" requirement.
|
/// Infer the dependencies for a directory dependency.
|
||||||
async fn resolve_source_tree(
|
async fn resolve_source_tree(&self, path: &Path) -> Result<SourceTreeResolution> {
|
||||||
&self,
|
|
||||||
path: &Path,
|
|
||||||
) -> Result<Vec<pep508_rs::Requirement<VerbatimParsedUrl>>> {
|
|
||||||
// Convert to a buildable source.
|
// Convert to a buildable source.
|
||||||
let source_tree = fs_err::canonicalize(path).with_context(|| {
|
let source_tree = fs_err::canonicalize(path).with_context(|| {
|
||||||
format!(
|
format!(
|
||||||
|
@ -151,40 +153,59 @@ impl<'a, Context: BuildContext> SourceTreeResolver<'a, Context> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Extract the origin.
|
|
||||||
let origin = RequirementOrigin::Project(path.to_path_buf(), metadata.name.clone());
|
let origin = RequirementOrigin::Project(path.to_path_buf(), metadata.name.clone());
|
||||||
|
|
||||||
|
// Determine the extras to include when resolving the requirements.
|
||||||
|
let extras = match self.extras {
|
||||||
|
ExtrasSpecification::All => metadata.provides_extras.as_slice(),
|
||||||
|
ExtrasSpecification::None => &[],
|
||||||
|
ExtrasSpecification::Some(extras) => extras,
|
||||||
|
};
|
||||||
|
|
||||||
// Determine the appropriate requirements to return based on the extras. This involves
|
// Determine the appropriate requirements to return based on the extras. This involves
|
||||||
// evaluating the `extras` expression in any markers, but preserving the remaining marker
|
// evaluating the `extras` expression in any markers, but preserving the remaining marker
|
||||||
// conditions.
|
// conditions.
|
||||||
match self.extras {
|
let mut requirements: Vec<Requirement> = metadata
|
||||||
ExtrasSpecification::None => Ok(metadata
|
.requires_dist
|
||||||
.requires_dist
|
.into_iter()
|
||||||
.into_iter()
|
.map(|requirement| Requirement {
|
||||||
.map(|requirement| requirement.with_origin(origin.clone()))
|
origin: Some(origin.clone()),
|
||||||
.collect()),
|
marker: requirement
|
||||||
ExtrasSpecification::All => Ok(metadata
|
.marker
|
||||||
.requires_dist
|
.and_then(|marker| marker.simplify_extras(extras)),
|
||||||
.into_iter()
|
..requirement
|
||||||
.map(|requirement| pep508_rs::Requirement {
|
})
|
||||||
origin: Some(origin.clone()),
|
.collect();
|
||||||
marker: requirement
|
|
||||||
.marker
|
// Resolve any recursive extras.
|
||||||
.and_then(|marker| marker.simplify_extras(&metadata.provides_extras)),
|
loop {
|
||||||
..requirement
|
// Find the first recursive requirement.
|
||||||
})
|
// TODO(charlie): Respect markers on recursive extras.
|
||||||
.collect()),
|
let Some(index) = requirements.iter().position(|requirement| {
|
||||||
ExtrasSpecification::Some(extras) => Ok(metadata
|
requirement.name == metadata.name && requirement.marker.is_none()
|
||||||
.requires_dist
|
}) else {
|
||||||
.into_iter()
|
break;
|
||||||
.map(|requirement| pep508_rs::Requirement {
|
};
|
||||||
origin: Some(origin.clone()),
|
|
||||||
marker: requirement
|
// Remove the requirement that points to us.
|
||||||
.marker
|
let recursive = requirements.remove(index);
|
||||||
.and_then(|marker| marker.simplify_extras(extras)),
|
|
||||||
..requirement
|
// Re-simplify the requirements.
|
||||||
})
|
for requirement in &mut requirements {
|
||||||
.collect()),
|
requirement.marker = requirement
|
||||||
|
.marker
|
||||||
|
.take()
|
||||||
|
.and_then(|marker| marker.simplify_extras(&recursive.extras));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let project = metadata.name;
|
||||||
|
let extras = metadata.provides_extras;
|
||||||
|
|
||||||
|
Ok(SourceTreeResolution {
|
||||||
|
requirements,
|
||||||
|
project,
|
||||||
|
extras,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,33 +27,29 @@
|
||||||
//! * `setup.py` or `setup.cfg` instead of `pyproject.toml`: Directory is an entry in
|
//! * `setup.py` or `setup.cfg` instead of `pyproject.toml`: Directory is an entry in
|
||||||
//! `source_trees`.
|
//! `source_trees`.
|
||||||
|
|
||||||
use std::collections::VecDeque;
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use path_absolutize::Absolutize;
|
|
||||||
use rustc_hash::FxHashSet;
|
use rustc_hash::FxHashSet;
|
||||||
use same_file::is_same_file;
|
use tracing::instrument;
|
||||||
use tracing::{debug, instrument, trace};
|
|
||||||
|
|
||||||
use cache_key::CanonicalUrl;
|
use cache_key::CanonicalUrl;
|
||||||
use distribution_types::{
|
use distribution_types::{
|
||||||
FlatIndexLocation, IndexUrl, Requirement, RequirementSource, UnresolvedRequirement,
|
FlatIndexLocation, IndexUrl, UnresolvedRequirement, UnresolvedRequirementSpecification,
|
||||||
UnresolvedRequirementSpecification,
|
|
||||||
};
|
};
|
||||||
use pep508_rs::{UnnamedRequirement, UnnamedRequirementUrl};
|
use pep508_rs::{UnnamedRequirement, UnnamedRequirementUrl};
|
||||||
|
use pypi_types::Requirement;
|
||||||
use pypi_types::VerbatimParsedUrl;
|
use pypi_types::VerbatimParsedUrl;
|
||||||
use requirements_txt::{
|
use requirements_txt::{
|
||||||
EditableRequirement, FindLink, RequirementEntry, RequirementsTxt, RequirementsTxtRequirement,
|
EditableRequirement, FindLink, RequirementEntry, RequirementsTxt, RequirementsTxtRequirement,
|
||||||
};
|
};
|
||||||
use uv_client::BaseClientBuilder;
|
use uv_client::BaseClientBuilder;
|
||||||
use uv_configuration::{ExtrasSpecification, NoBinary, NoBuild, PreviewMode};
|
use uv_configuration::{NoBinary, NoBuild};
|
||||||
|
use uv_distribution::pyproject::PyProjectToml;
|
||||||
use uv_fs::Simplified;
|
use uv_fs::Simplified;
|
||||||
use uv_normalize::{ExtraName, PackageName};
|
use uv_normalize::{ExtraName, PackageName};
|
||||||
|
|
||||||
use crate::pyproject::{Pep621Metadata, PyProjectToml};
|
use crate::RequirementsSource;
|
||||||
use crate::ProjectWorkspace;
|
|
||||||
use crate::{RequirementsSource, Workspace, WorkspaceError};
|
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
pub struct RequirementsSpecification {
|
pub struct RequirementsSpecification {
|
||||||
|
@ -88,10 +84,7 @@ impl RequirementsSpecification {
|
||||||
#[instrument(skip_all, level = tracing::Level::DEBUG, fields(source = % source))]
|
#[instrument(skip_all, level = tracing::Level::DEBUG, fields(source = % source))]
|
||||||
pub async fn from_source(
|
pub async fn from_source(
|
||||||
source: &RequirementsSource,
|
source: &RequirementsSource,
|
||||||
extras: &ExtrasSpecification,
|
|
||||||
client_builder: &BaseClientBuilder<'_>,
|
client_builder: &BaseClientBuilder<'_>,
|
||||||
workspace: Option<&Workspace>,
|
|
||||||
preview: PreviewMode,
|
|
||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
Ok(match source {
|
Ok(match source {
|
||||||
RequirementsSource::Package(name) => {
|
RequirementsSource::Package(name) => {
|
||||||
|
@ -108,9 +101,22 @@ impl RequirementsSpecification {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
RequirementsSource::Editable(name) => {
|
RequirementsSource::Editable(name) => {
|
||||||
Self::from_editable_source(name, extras, workspace, preview).await?
|
let requirement = EditableRequirement::parse(name, None, std::env::current_dir()?)
|
||||||
|
.with_context(|| format!("Failed to parse: `{name}`"))?;
|
||||||
|
Self {
|
||||||
|
requirements: vec![UnresolvedRequirementSpecification::from(requirement)],
|
||||||
|
..Self::default()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
RequirementsSource::RequirementsTxt(path) => {
|
RequirementsSource::RequirementsTxt(path) => {
|
||||||
|
if !(path == Path::new("-")
|
||||||
|
|| path.starts_with("http://")
|
||||||
|
|| path.starts_with("https://")
|
||||||
|
|| path.is_file())
|
||||||
|
{
|
||||||
|
return Err(anyhow::anyhow!("File not found: `{}`", path.user_display()));
|
||||||
|
}
|
||||||
|
|
||||||
let requirements_txt =
|
let requirements_txt =
|
||||||
RequirementsTxt::parse(path, std::env::current_dir()?, client_builder).await?;
|
RequirementsTxt::parse(path, std::env::current_dir()?, client_builder).await?;
|
||||||
Self {
|
Self {
|
||||||
|
@ -151,317 +157,68 @@ impl RequirementsSpecification {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
RequirementsSource::PyprojectToml(path) => {
|
RequirementsSource::PyprojectToml(path) => {
|
||||||
Self::from_pyproject_toml_source(path, extras, preview).await?
|
let contents = match fs_err::tokio::read_to_string(&path).await {
|
||||||
|
Ok(contents) => contents,
|
||||||
|
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
|
||||||
|
return Err(anyhow::anyhow!("File not found: `{}`", path.user_display()));
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"Failed to read `{}`: {}",
|
||||||
|
path.user_display(),
|
||||||
|
err
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let _ = toml::from_str::<PyProjectToml>(&contents)
|
||||||
|
.with_context(|| format!("Failed to parse: `{}`", path.user_display()))?;
|
||||||
|
|
||||||
|
Self {
|
||||||
|
source_trees: vec![path.clone()],
|
||||||
|
..Self::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RequirementsSource::SetupPy(path) | RequirementsSource::SetupCfg(path) => {
|
||||||
|
if !path.is_file() {
|
||||||
|
return Err(anyhow::anyhow!("File not found: `{}`", path.user_display()));
|
||||||
|
}
|
||||||
|
|
||||||
|
Self {
|
||||||
|
source_trees: vec![path.clone()],
|
||||||
|
..Self::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RequirementsSource::SourceTree(path) => {
|
||||||
|
if !path.is_dir() {
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"Directory not found: `{}`",
|
||||||
|
path.user_display()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Self {
|
||||||
|
project: None,
|
||||||
|
requirements: vec![UnresolvedRequirementSpecification {
|
||||||
|
requirement: UnresolvedRequirement::Unnamed(UnnamedRequirement {
|
||||||
|
url: VerbatimParsedUrl::parse_absolute_path(path)?,
|
||||||
|
extras: vec![],
|
||||||
|
marker: None,
|
||||||
|
origin: None,
|
||||||
|
}),
|
||||||
|
hashes: vec![],
|
||||||
|
}],
|
||||||
|
..Self::default()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
RequirementsSource::SetupPy(path) | RequirementsSource::SetupCfg(path) => Self {
|
|
||||||
source_trees: vec![path.clone()],
|
|
||||||
..Self::default()
|
|
||||||
},
|
|
||||||
RequirementsSource::SourceTree(path) => Self {
|
|
||||||
project: None,
|
|
||||||
requirements: vec![UnresolvedRequirementSpecification {
|
|
||||||
requirement: UnresolvedRequirement::Unnamed(UnnamedRequirement {
|
|
||||||
url: VerbatimParsedUrl::parse_absolute_path(path)?,
|
|
||||||
extras: vec![],
|
|
||||||
marker: None,
|
|
||||||
origin: None,
|
|
||||||
}),
|
|
||||||
hashes: vec![],
|
|
||||||
}],
|
|
||||||
..Self::default()
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn from_editable_source(
|
|
||||||
name: &str,
|
|
||||||
extras: &ExtrasSpecification,
|
|
||||||
workspace: Option<&Workspace>,
|
|
||||||
preview: PreviewMode,
|
|
||||||
) -> Result<RequirementsSpecification> {
|
|
||||||
let requirement = EditableRequirement::parse(name, None, std::env::current_dir()?)
|
|
||||||
.with_context(|| format!("Failed to parse: `{name}`"))?;
|
|
||||||
|
|
||||||
// If we're not in preview mode, return the editable without searching for a workspace.
|
|
||||||
if preview.is_disabled() {
|
|
||||||
return Ok(Self {
|
|
||||||
requirements: vec![UnresolvedRequirementSpecification::from(requirement)],
|
|
||||||
..Self::default()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// First try to find the project in the existing workspace (if any), then try workspace
|
|
||||||
// discovery.
|
|
||||||
let project_in_exiting_workspace = workspace.and_then(|workspace| {
|
|
||||||
// We use `is_same_file` instead of indexing by path to support different versions of
|
|
||||||
// the same path (e.g. symlinks).
|
|
||||||
workspace
|
|
||||||
.packages()
|
|
||||||
.values()
|
|
||||||
.find(|member| is_same_file(member.root(), &requirement.path).unwrap_or(false))
|
|
||||||
.map(|member| (member.pyproject_toml(), workspace))
|
|
||||||
});
|
|
||||||
|
|
||||||
let editable_spec = if let Some((pyproject_toml, workspace)) = project_in_exiting_workspace
|
|
||||||
{
|
|
||||||
debug!(
|
|
||||||
"Found project in workspace at: `{}`",
|
|
||||||
requirement.path.user_display()
|
|
||||||
);
|
|
||||||
|
|
||||||
Self::parse_direct_pyproject_toml(
|
|
||||||
pyproject_toml,
|
|
||||||
workspace,
|
|
||||||
extras,
|
|
||||||
requirement.path.as_ref(),
|
|
||||||
preview,
|
|
||||||
)
|
|
||||||
.with_context(|| format!("Failed to parse: `{}`", requirement.path.user_display()))?
|
|
||||||
} else if let Some(project_workspace) =
|
|
||||||
ProjectWorkspace::from_maybe_project_root(&requirement.path).await?
|
|
||||||
{
|
|
||||||
debug!(
|
|
||||||
"Found project at workspace root: `{}`",
|
|
||||||
requirement.path.user_display()
|
|
||||||
);
|
|
||||||
|
|
||||||
let pyproject_toml = project_workspace.current_project().pyproject_toml();
|
|
||||||
let workspace = project_workspace.workspace();
|
|
||||||
Self::parse_direct_pyproject_toml(
|
|
||||||
pyproject_toml,
|
|
||||||
workspace,
|
|
||||||
extras,
|
|
||||||
requirement.path.as_ref(),
|
|
||||||
preview,
|
|
||||||
)
|
|
||||||
.with_context(|| format!("Failed to parse: `{}`", requirement.path.user_display()))?
|
|
||||||
} else {
|
|
||||||
// No `pyproject.toml` or no static metadata also means no workspace support (at the
|
|
||||||
// moment).
|
|
||||||
debug!(
|
|
||||||
"pyproject.toml has dynamic metadata at: `{}`",
|
|
||||||
requirement.path.user_display()
|
|
||||||
);
|
|
||||||
|
|
||||||
return Ok(Self {
|
|
||||||
requirements: vec![UnresolvedRequirementSpecification::from(requirement)],
|
|
||||||
..Self::default()
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(editable_spec) = editable_spec {
|
|
||||||
// We only collect the editables here to keep the count of root packages correct.
|
|
||||||
// TODO(konsti): Collect all workspace packages, even the non-editable ones.
|
|
||||||
Ok(Self {
|
|
||||||
requirements: editable_spec
|
|
||||||
.requirements
|
|
||||||
.into_iter()
|
|
||||||
.chain(std::iter::once(UnresolvedRequirementSpecification::from(
|
|
||||||
requirement,
|
|
||||||
)))
|
|
||||||
.filter(|entry| entry.requirement.is_editable())
|
|
||||||
.collect(),
|
|
||||||
..Self::default()
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
debug!(
|
|
||||||
"pyproject.toml has dynamic metadata at: `{}`",
|
|
||||||
requirement.path.user_display()
|
|
||||||
);
|
|
||||||
Ok(Self {
|
|
||||||
requirements: vec![UnresolvedRequirementSpecification::from(requirement)],
|
|
||||||
..Self::default()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn from_pyproject_toml_source(
|
|
||||||
path: &Path,
|
|
||||||
extras: &ExtrasSpecification,
|
|
||||||
preview: PreviewMode,
|
|
||||||
) -> Result<RequirementsSpecification> {
|
|
||||||
let dir = path.parent().context("pyproject.toml must have a parent")?;
|
|
||||||
// We have to handle three cases:
|
|
||||||
// * There is a workspace (possibly implicit) with static dependencies.
|
|
||||||
// * There are dynamic dependencies, we have to build and don't use workspace information if
|
|
||||||
// present.
|
|
||||||
// * There was an error during workspace discovery, such as an IO error or a
|
|
||||||
// `pyproject.toml` in the workspace not matching the (lenient) schema.
|
|
||||||
match ProjectWorkspace::from_project_root(dir).await {
|
|
||||||
Ok(project_workspace) => {
|
|
||||||
let static_pyproject_toml = Self::parse_direct_pyproject_toml(
|
|
||||||
project_workspace.current_project().pyproject_toml(),
|
|
||||||
project_workspace.workspace(),
|
|
||||||
extras,
|
|
||||||
path,
|
|
||||||
preview,
|
|
||||||
)
|
|
||||||
.with_context(|| format!("Failed to parse: `{}`", path.user_display()))?;
|
|
||||||
|
|
||||||
if let Some(static_pyproject_toml) = static_pyproject_toml {
|
|
||||||
Ok(static_pyproject_toml)
|
|
||||||
} else {
|
|
||||||
debug!("Dynamic pyproject.toml at: `{}`", path.user_display());
|
|
||||||
Ok(Self {
|
|
||||||
source_trees: vec![path.to_path_buf()],
|
|
||||||
..Self::default()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(WorkspaceError::MissingProject(_)) => {
|
|
||||||
debug!(
|
|
||||||
"Missing `project` table from pyproject.toml at: `{}`",
|
|
||||||
path.user_display()
|
|
||||||
);
|
|
||||||
Ok(Self {
|
|
||||||
source_trees: vec![path.to_path_buf()],
|
|
||||||
..Self::default()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
Err(err) => Err(anyhow::Error::new(err)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parse and lower a `pyproject.toml`, including all editable workspace dependencies.
|
|
||||||
///
|
|
||||||
/// When dependency information is dynamic or invalid `project.dependencies` (e.g., Hatch's
|
|
||||||
/// relative path support), we return `None` and query the metadata with PEP 517 later.
|
|
||||||
pub(crate) fn parse_direct_pyproject_toml(
|
|
||||||
pyproject: &PyProjectToml,
|
|
||||||
workspace: &Workspace,
|
|
||||||
extras: &ExtrasSpecification,
|
|
||||||
pyproject_path: &Path,
|
|
||||||
preview: PreviewMode,
|
|
||||||
) -> Result<Option<Self>> {
|
|
||||||
// We need use this path as base for the relative paths inside pyproject.toml, so
|
|
||||||
// we need the absolute path instead of a potentially relative path. E.g. with
|
|
||||||
// `foo = { path = "../foo" }`, we will join `../foo` onto this path.
|
|
||||||
let absolute_path = uv_fs::absolutize_path(pyproject_path)?;
|
|
||||||
let project_dir = absolute_path
|
|
||||||
.parent()
|
|
||||||
.context("`pyproject.toml` has no parent directory")?;
|
|
||||||
|
|
||||||
let Some(project) = Pep621Metadata::try_from(
|
|
||||||
pyproject,
|
|
||||||
extras,
|
|
||||||
pyproject_path,
|
|
||||||
project_dir,
|
|
||||||
workspace,
|
|
||||||
preview,
|
|
||||||
)?
|
|
||||||
else {
|
|
||||||
debug!(
|
|
||||||
"Dynamic pyproject.toml at: `{}`",
|
|
||||||
pyproject_path.user_display()
|
|
||||||
);
|
|
||||||
return Ok(None);
|
|
||||||
};
|
|
||||||
|
|
||||||
if preview.is_disabled() {
|
|
||||||
Ok(Some(Self {
|
|
||||||
project: Some(project.name),
|
|
||||||
requirements: project
|
|
||||||
.requirements
|
|
||||||
.into_iter()
|
|
||||||
.map(UnresolvedRequirementSpecification::from)
|
|
||||||
.collect(),
|
|
||||||
extras: project.used_extras,
|
|
||||||
..Self::default()
|
|
||||||
}))
|
|
||||||
} else {
|
|
||||||
Ok(Some(Self::collect_transitive_editables(
|
|
||||||
workspace, extras, preview, project,
|
|
||||||
)?))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Perform a workspace dependency DAG traversal (breadth-first search) to collect all editables
|
|
||||||
/// eagerly.
|
|
||||||
///
|
|
||||||
/// Consider a requirement on A in a workspace with workspace packages A, B, C where
|
|
||||||
/// A -> B and B -> C.
|
|
||||||
fn collect_transitive_editables(
|
|
||||||
workspace: &Workspace,
|
|
||||||
extras: &ExtrasSpecification,
|
|
||||||
preview: PreviewMode,
|
|
||||||
project: Pep621Metadata,
|
|
||||||
) -> Result<RequirementsSpecification> {
|
|
||||||
let mut seen = FxHashSet::from_iter([project.name.clone()]);
|
|
||||||
let mut queue = VecDeque::from([project.name.clone()]);
|
|
||||||
let mut requirements = Vec::new();
|
|
||||||
let mut used_extras = FxHashSet::default();
|
|
||||||
|
|
||||||
while let Some(project_name) = queue.pop_front() {
|
|
||||||
let Some(current) = workspace.packages().get(&project_name) else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
trace!("Processing metadata for workspace package {project_name}");
|
|
||||||
|
|
||||||
let project_root_absolute = current.root().absolutize_from(workspace.root())?;
|
|
||||||
let pyproject = current.pyproject_toml().clone();
|
|
||||||
let project = Pep621Metadata::try_from(
|
|
||||||
&pyproject,
|
|
||||||
extras,
|
|
||||||
&project_root_absolute.join("pyproject.toml"),
|
|
||||||
project_root_absolute.as_ref(),
|
|
||||||
workspace,
|
|
||||||
preview,
|
|
||||||
)
|
|
||||||
.with_context(|| {
|
|
||||||
format!(
|
|
||||||
"Invalid requirements in: `{}`",
|
|
||||||
current.root().join("pyproject.toml").user_display()
|
|
||||||
)
|
|
||||||
})?
|
|
||||||
// TODO(konsti): We should support this by building and using the built PEP 517 metadata
|
|
||||||
.with_context(|| {
|
|
||||||
format!(
|
|
||||||
"Workspace member doesn't declare static metadata: `{}`",
|
|
||||||
current.root().user_display()
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
// Recurse into any editables.
|
|
||||||
for requirement in &project.requirements {
|
|
||||||
if matches!(
|
|
||||||
requirement.source,
|
|
||||||
RequirementSource::Path { editable: true, .. }
|
|
||||||
) {
|
|
||||||
if seen.insert(requirement.name.clone()) {
|
|
||||||
queue.push_back(requirement.name.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Collect the requirements and extras.
|
|
||||||
used_extras.extend(project.used_extras);
|
|
||||||
requirements.extend(project.requirements);
|
|
||||||
}
|
|
||||||
|
|
||||||
let spec = Self {
|
|
||||||
project: Some(project.name),
|
|
||||||
requirements: requirements
|
|
||||||
.into_iter()
|
|
||||||
.map(UnresolvedRequirementSpecification::from)
|
|
||||||
.collect(),
|
|
||||||
extras: used_extras,
|
|
||||||
..Self::default()
|
|
||||||
};
|
|
||||||
Ok(spec)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read the combined requirements and constraints from a set of sources.
|
/// Read the combined requirements and constraints from a set of sources.
|
||||||
///
|
|
||||||
/// If a [`Workspace`] is provided, it will be used as-is without re-discovering a workspace
|
|
||||||
/// from the filesystem.
|
|
||||||
pub async fn from_sources(
|
pub async fn from_sources(
|
||||||
requirements: &[RequirementsSource],
|
requirements: &[RequirementsSource],
|
||||||
constraints: &[RequirementsSource],
|
constraints: &[RequirementsSource],
|
||||||
overrides: &[RequirementsSource],
|
overrides: &[RequirementsSource],
|
||||||
workspace: Option<&Workspace>,
|
|
||||||
extras: &ExtrasSpecification,
|
|
||||||
client_builder: &BaseClientBuilder<'_>,
|
client_builder: &BaseClientBuilder<'_>,
|
||||||
preview: PreviewMode,
|
|
||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
let mut spec = Self::default();
|
let mut spec = Self::default();
|
||||||
|
|
||||||
|
@ -469,8 +226,7 @@ impl RequirementsSpecification {
|
||||||
// A `requirements.txt` can contain a `-c constraints.txt` directive within it, so reading
|
// A `requirements.txt` can contain a `-c constraints.txt` directive within it, so reading
|
||||||
// a requirements file can also add constraints.
|
// a requirements file can also add constraints.
|
||||||
for source in requirements {
|
for source in requirements {
|
||||||
let source =
|
let source = Self::from_source(source, client_builder).await?;
|
||||||
Self::from_source(source, extras, client_builder, workspace, preview).await?;
|
|
||||||
spec.requirements.extend(source.requirements);
|
spec.requirements.extend(source.requirements);
|
||||||
spec.constraints.extend(source.constraints);
|
spec.constraints.extend(source.constraints);
|
||||||
spec.overrides.extend(source.overrides);
|
spec.overrides.extend(source.overrides);
|
||||||
|
@ -502,8 +258,7 @@ impl RequirementsSpecification {
|
||||||
// Read all constraints, treating both requirements _and_ constraints as constraints.
|
// Read all constraints, treating both requirements _and_ constraints as constraints.
|
||||||
// Overrides are ignored, as are the hashes, as they are not relevant for constraints.
|
// Overrides are ignored, as are the hashes, as they are not relevant for constraints.
|
||||||
for source in constraints {
|
for source in constraints {
|
||||||
let source =
|
let source = Self::from_source(source, client_builder).await?;
|
||||||
Self::from_source(source, extras, client_builder, workspace, preview).await?;
|
|
||||||
for entry in source.requirements {
|
for entry in source.requirements {
|
||||||
match entry.requirement {
|
match entry.requirement {
|
||||||
UnresolvedRequirement::Named(requirement) => {
|
UnresolvedRequirement::Named(requirement) => {
|
||||||
|
@ -538,7 +293,7 @@ impl RequirementsSpecification {
|
||||||
// Read all overrides, treating both requirements _and_ overrides as overrides.
|
// Read all overrides, treating both requirements _and_ overrides as overrides.
|
||||||
// Constraints are ignored.
|
// Constraints are ignored.
|
||||||
for source in overrides {
|
for source in overrides {
|
||||||
let source = Self::from_source(source, extras, client_builder, None, preview).await?;
|
let source = Self::from_source(source, client_builder).await?;
|
||||||
spec.overrides.extend(source.requirements);
|
spec.overrides.extend(source.requirements);
|
||||||
spec.overrides.extend(source.overrides);
|
spec.overrides.extend(source.overrides);
|
||||||
|
|
||||||
|
@ -566,17 +321,7 @@ impl RequirementsSpecification {
|
||||||
pub async fn from_simple_sources(
|
pub async fn from_simple_sources(
|
||||||
requirements: &[RequirementsSource],
|
requirements: &[RequirementsSource],
|
||||||
client_builder: &BaseClientBuilder<'_>,
|
client_builder: &BaseClientBuilder<'_>,
|
||||||
preview: PreviewMode,
|
|
||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
Self::from_sources(
|
Self::from_sources(requirements, &[], &[], client_builder).await
|
||||||
requirements,
|
|
||||||
&[],
|
|
||||||
&[],
|
|
||||||
None,
|
|
||||||
&ExtrasSpecification::None,
|
|
||||||
client_builder,
|
|
||||||
preview,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,10 +12,10 @@ use tracing::debug;
|
||||||
use distribution_filename::{SourceDistFilename, WheelFilename};
|
use distribution_filename::{SourceDistFilename, WheelFilename};
|
||||||
use distribution_types::{
|
use distribution_types::{
|
||||||
BuildableSource, DirectSourceUrl, DirectorySourceUrl, GitSourceUrl, PathSourceUrl,
|
BuildableSource, DirectSourceUrl, DirectorySourceUrl, GitSourceUrl, PathSourceUrl,
|
||||||
RemoteSource, Requirement, SourceUrl, UnresolvedRequirement,
|
RemoteSource, SourceUrl, UnresolvedRequirement, UnresolvedRequirementSpecification, VersionId,
|
||||||
UnresolvedRequirementSpecification, VersionId,
|
|
||||||
};
|
};
|
||||||
use pep508_rs::{UnnamedRequirement, VersionOrUrl};
|
use pep508_rs::{UnnamedRequirement, VersionOrUrl};
|
||||||
|
use pypi_types::Requirement;
|
||||||
use pypi_types::{Metadata10, ParsedUrl, VerbatimParsedUrl};
|
use pypi_types::{Metadata10, ParsedUrl, VerbatimParsedUrl};
|
||||||
use uv_distribution::{DistributionDatabase, Reporter};
|
use uv_distribution::{DistributionDatabase, Reporter};
|
||||||
use uv_normalize::PackageName;
|
use uv_normalize::PackageName;
|
||||||
|
|
|
@ -1,15 +1,14 @@
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
|
use anstream::eprint;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
|
||||||
use anstream::eprint;
|
|
||||||
use requirements_txt::RequirementsTxt;
|
use requirements_txt::RequirementsTxt;
|
||||||
use uv_client::{BaseClientBuilder, Connectivity};
|
use uv_client::{BaseClientBuilder, Connectivity};
|
||||||
use uv_configuration::Upgrade;
|
use uv_configuration::Upgrade;
|
||||||
|
use uv_distribution::ProjectWorkspace;
|
||||||
use uv_resolver::{Lock, Preference, PreferenceError};
|
use uv_resolver::{Lock, Preference, PreferenceError};
|
||||||
|
|
||||||
use crate::ProjectWorkspace;
|
|
||||||
|
|
||||||
/// Load the preferred requirements from an existing `requirements.txt`, applying the upgrade strategy.
|
/// Load the preferred requirements from an existing `requirements.txt`, applying the upgrade strategy.
|
||||||
pub async fn read_requirements_txt(
|
pub async fn read_requirements_txt(
|
||||||
output_file: Option<&Path>,
|
output_file: Option<&Path>,
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use either::Either;
|
use either::Either;
|
||||||
|
|
||||||
use distribution_types::Requirement;
|
|
||||||
use pep508_rs::MarkerEnvironment;
|
use pep508_rs::MarkerEnvironment;
|
||||||
|
use pypi_types::Requirement;
|
||||||
use uv_configuration::{Constraints, Overrides};
|
use uv_configuration::{Constraints, Overrides};
|
||||||
use uv_normalize::PackageName;
|
use uv_normalize::PackageName;
|
||||||
use uv_types::RequestedRequirements;
|
use uv_types::RequestedRequirements;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use distribution_types::RequirementSource;
|
use pypi_types::RequirementSource;
|
||||||
use rustc_hash::FxHashSet;
|
use rustc_hash::FxHashSet;
|
||||||
|
|
||||||
use pep508_rs::MarkerEnvironment;
|
use pep508_rs::MarkerEnvironment;
|
||||||
|
|
|
@ -3,9 +3,10 @@ use pubgrub::range::Range;
|
||||||
use rustc_hash::FxHashSet;
|
use rustc_hash::FxHashSet;
|
||||||
use tracing::warn;
|
use tracing::warn;
|
||||||
|
|
||||||
use distribution_types::{Requirement, RequirementSource, Verbatim};
|
use distribution_types::Verbatim;
|
||||||
use pep440_rs::Version;
|
use pep440_rs::Version;
|
||||||
use pep508_rs::MarkerEnvironment;
|
use pep508_rs::MarkerEnvironment;
|
||||||
|
use pypi_types::{Requirement, RequirementSource};
|
||||||
use uv_configuration::{Constraints, Overrides};
|
use uv_configuration::{Constraints, Overrides};
|
||||||
use uv_normalize::{ExtraName, PackageName};
|
use uv_normalize::{ExtraName, PackageName};
|
||||||
|
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
use std::hash::BuildHasherDefault;
|
use std::hash::BuildHasherDefault;
|
||||||
|
|
||||||
use rustc_hash::{FxHashMap, FxHashSet};
|
|
||||||
|
|
||||||
use distribution_types::{
|
|
||||||
Dist, DistributionMetadata, Name, Requirement, ResolutionDiagnostic, VersionId, VersionOrUrlRef,
|
|
||||||
};
|
|
||||||
use pep440_rs::{Version, VersionSpecifier};
|
|
||||||
use pep508_rs::{MarkerEnvironment, MarkerTree};
|
|
||||||
use petgraph::{
|
use petgraph::{
|
||||||
graph::{Graph, NodeIndex},
|
graph::{Graph, NodeIndex},
|
||||||
Directed,
|
Directed,
|
||||||
};
|
};
|
||||||
use pypi_types::{ParsedUrlError, Yanked};
|
use rustc_hash::{FxHashMap, FxHashSet};
|
||||||
|
|
||||||
|
use distribution_types::{
|
||||||
|
Dist, DistributionMetadata, Name, ResolutionDiagnostic, VersionId, VersionOrUrlRef,
|
||||||
|
};
|
||||||
|
use pep440_rs::{Version, VersionSpecifier};
|
||||||
|
use pep508_rs::{MarkerEnvironment, MarkerTree};
|
||||||
|
use pypi_types::{ParsedUrlError, Requirement, Yanked};
|
||||||
use uv_normalize::{ExtraName, PackageName};
|
use uv_normalize::{ExtraName, PackageName};
|
||||||
|
|
||||||
use crate::preferences::Preferences;
|
use crate::preferences::Preferences;
|
||||||
|
|
|
@ -6,7 +6,8 @@ use itertools::Itertools;
|
||||||
|
|
||||||
use distribution_types::{DistributionMetadata, Name, ResolvedDist, Verbatim, VersionOrUrlRef};
|
use distribution_types::{DistributionMetadata, Name, ResolvedDist, Verbatim, VersionOrUrlRef};
|
||||||
use pep508_rs::{split_scheme, MarkerTree, Scheme};
|
use pep508_rs::{split_scheme, MarkerTree, Scheme};
|
||||||
use pypi_types::{HashDigest, Metadata23};
|
use pypi_types::HashDigest;
|
||||||
|
use uv_distribution::Metadata;
|
||||||
use uv_normalize::{ExtraName, PackageName};
|
use uv_normalize::{ExtraName, PackageName};
|
||||||
|
|
||||||
pub use crate::resolution::display::{AnnotationStyle, DisplayResolutionGraph};
|
pub use crate::resolution::display::{AnnotationStyle, DisplayResolutionGraph};
|
||||||
|
@ -24,7 +25,7 @@ pub(crate) struct AnnotatedDist {
|
||||||
pub(crate) extra: Option<ExtraName>,
|
pub(crate) extra: Option<ExtraName>,
|
||||||
pub(crate) marker: Option<MarkerTree>,
|
pub(crate) marker: Option<MarkerTree>,
|
||||||
pub(crate) hashes: Vec<HashDigest>,
|
pub(crate) hashes: Vec<HashDigest>,
|
||||||
pub(crate) metadata: Metadata23,
|
pub(crate) metadata: Metadata,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Name for AnnotatedDist {
|
impl Name for AnnotatedDist {
|
||||||
|
|
|
@ -4,9 +4,10 @@ use std::str::FromStr;
|
||||||
use rustc_hash::FxHashMap;
|
use rustc_hash::FxHashMap;
|
||||||
|
|
||||||
use distribution_filename::{SourceDistFilename, WheelFilename};
|
use distribution_filename::{SourceDistFilename, WheelFilename};
|
||||||
use distribution_types::{RemoteSource, RequirementSource};
|
use distribution_types::RemoteSource;
|
||||||
use pep440_rs::{Operator, Version, VersionSpecifier, VersionSpecifierBuildError};
|
use pep440_rs::{Operator, Version, VersionSpecifier, VersionSpecifierBuildError};
|
||||||
use pep508_rs::MarkerEnvironment;
|
use pep508_rs::MarkerEnvironment;
|
||||||
|
use pypi_types::RequirementSource;
|
||||||
use uv_normalize::PackageName;
|
use uv_normalize::PackageName;
|
||||||
|
|
||||||
use crate::{DependencyMode, Manifest};
|
use crate::{DependencyMode, Manifest};
|
||||||
|
@ -203,10 +204,10 @@ mod tests {
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use distribution_types::RequirementSource;
|
|
||||||
use pep440_rs::{Operator, Version, VersionSpecifier, VersionSpecifiers};
|
use pep440_rs::{Operator, Version, VersionSpecifier, VersionSpecifiers};
|
||||||
use pep508_rs::VerbatimUrl;
|
use pep508_rs::VerbatimUrl;
|
||||||
use pypi_types::ParsedUrl;
|
use pypi_types::ParsedUrl;
|
||||||
|
use pypi_types::RequirementSource;
|
||||||
|
|
||||||
use crate::resolver::locals::{iter_locals, Locals};
|
use crate::resolver::locals::{iter_locals, Locals};
|
||||||
|
|
||||||
|
|
|
@ -21,14 +21,13 @@ use tracing::{debug, enabled, instrument, trace, warn, Level};
|
||||||
|
|
||||||
use distribution_types::{
|
use distribution_types::{
|
||||||
BuiltDist, Dist, DistributionMetadata, IncompatibleDist, IncompatibleSource, IncompatibleWheel,
|
BuiltDist, Dist, DistributionMetadata, IncompatibleDist, IncompatibleSource, IncompatibleWheel,
|
||||||
InstalledDist, RemoteSource, Requirement, ResolvedDist, ResolvedDistRef, SourceDist,
|
InstalledDist, RemoteSource, ResolvedDist, ResolvedDistRef, SourceDist, VersionOrUrlRef,
|
||||||
VersionOrUrlRef,
|
|
||||||
};
|
};
|
||||||
pub(crate) use locals::Locals;
|
pub(crate) use locals::Locals;
|
||||||
use pep440_rs::{Version, MIN_VERSION};
|
use pep440_rs::{Version, MIN_VERSION};
|
||||||
use pep508_rs::MarkerEnvironment;
|
use pep508_rs::MarkerEnvironment;
|
||||||
use platform_tags::Tags;
|
use platform_tags::Tags;
|
||||||
use pypi_types::Metadata23;
|
use pypi_types::{Metadata23, Requirement};
|
||||||
pub(crate) use urls::Urls;
|
pub(crate) use urls::Urls;
|
||||||
use uv_configuration::{Constraints, Overrides};
|
use uv_configuration::{Constraints, Overrides};
|
||||||
use uv_distribution::{ArchiveMetadata, DistributionDatabase};
|
use uv_distribution::{ArchiveMetadata, DistributionDatabase};
|
||||||
|
@ -1144,7 +1143,9 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
||||||
trace!("Received installed distribution metadata for: {dist}");
|
trace!("Received installed distribution metadata for: {dist}");
|
||||||
self.index.distributions().done(
|
self.index.distributions().done(
|
||||||
dist.version_id(),
|
dist.version_id(),
|
||||||
Arc::new(MetadataResponse::Found(ArchiveMetadata::from(metadata))),
|
Arc::new(MetadataResponse::Found(ArchiveMetadata::from_metadata23(
|
||||||
|
metadata,
|
||||||
|
))),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Some(Response::Dist {
|
Some(Response::Dist {
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
use distribution_types::{RequirementSource, Verbatim};
|
use distribution_types::Verbatim;
|
||||||
use rustc_hash::FxHashMap;
|
use rustc_hash::FxHashMap;
|
||||||
use tracing::debug;
|
use tracing::debug;
|
||||||
|
|
||||||
use pep508_rs::{MarkerEnvironment, VerbatimUrl};
|
use pep508_rs::{MarkerEnvironment, VerbatimUrl};
|
||||||
use pypi_types::{ParsedArchiveUrl, ParsedGitUrl, ParsedPathUrl, ParsedUrl, VerbatimParsedUrl};
|
use pypi_types::{
|
||||||
|
ParsedArchiveUrl, ParsedGitUrl, ParsedPathUrl, ParsedUrl, RequirementSource, VerbatimParsedUrl,
|
||||||
|
};
|
||||||
use uv_distribution::is_same_reference;
|
use uv_distribution::is_same_reference;
|
||||||
use uv_git::GitUrl;
|
use uv_git::GitUrl;
|
||||||
use uv_normalize::PackageName;
|
use uv_normalize::PackageName;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use distribution_types::RequirementSource;
|
use pypi_types::RequirementSource;
|
||||||
use rustc_hash::{FxHashMap, FxHashSet};
|
use rustc_hash::{FxHashMap, FxHashSet};
|
||||||
|
|
||||||
use pep440_rs::Version;
|
use pep440_rs::Version;
|
||||||
|
|
|
@ -10,10 +10,11 @@ use anyhow::Result;
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
|
|
||||||
use distribution_types::{CachedDist, IndexLocations, Requirement, Resolution, SourceDist};
|
use distribution_types::{CachedDist, IndexLocations, Resolution, SourceDist};
|
||||||
use pep440_rs::Version;
|
use pep440_rs::Version;
|
||||||
use pep508_rs::{MarkerEnvironment, MarkerEnvironmentBuilder};
|
use pep508_rs::{MarkerEnvironment, MarkerEnvironmentBuilder};
|
||||||
use platform_tags::{Arch, Os, Platform, Tags};
|
use platform_tags::{Arch, Os, Platform, Tags};
|
||||||
|
use pypi_types::Requirement;
|
||||||
use uv_cache::Cache;
|
use uv_cache::Cache;
|
||||||
use uv_client::RegistryClientBuilder;
|
use uv_client::RegistryClientBuilder;
|
||||||
use uv_configuration::{
|
use uv_configuration::{
|
||||||
|
@ -155,7 +156,12 @@ async fn resolve(
|
||||||
&hashes,
|
&hashes,
|
||||||
&build_context,
|
&build_context,
|
||||||
installed_packages,
|
installed_packages,
|
||||||
DistributionDatabase::new(&client, &build_context, concurrency.downloads),
|
DistributionDatabase::new(
|
||||||
|
&client,
|
||||||
|
&build_context,
|
||||||
|
concurrency.downloads,
|
||||||
|
PreviewMode::Disabled,
|
||||||
|
),
|
||||||
)?;
|
)?;
|
||||||
Ok(resolver.resolve().await?)
|
Ok(resolver.resolve().await?)
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,12 +3,9 @@ use std::str::FromStr;
|
||||||
use rustc_hash::FxHashMap;
|
use rustc_hash::FxHashMap;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use distribution_types::{
|
use distribution_types::{DistributionMetadata, HashPolicy, PackageId, UnresolvedRequirement};
|
||||||
DistributionMetadata, HashPolicy, PackageId, Requirement, RequirementSource,
|
|
||||||
UnresolvedRequirement,
|
|
||||||
};
|
|
||||||
use pep508_rs::MarkerEnvironment;
|
use pep508_rs::MarkerEnvironment;
|
||||||
use pypi_types::{HashDigest, HashError};
|
use pypi_types::{HashDigest, HashError, Requirement, RequirementSource};
|
||||||
use uv_normalize::PackageName;
|
use uv_normalize::PackageName;
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone)]
|
#[derive(Debug, Default, Clone)]
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use distribution_types::Requirement;
|
use pypi_types::Requirement;
|
||||||
use uv_normalize::ExtraName;
|
use uv_normalize::ExtraName;
|
||||||
|
|
||||||
/// A set of requirements as requested by a parent requirement.
|
/// A set of requirements as requested by a parent requirement.
|
||||||
|
|
|
@ -3,10 +3,9 @@ use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
|
||||||
use distribution_types::{
|
use distribution_types::{CachedDist, IndexLocations, InstalledDist, Resolution, SourceDist};
|
||||||
CachedDist, IndexLocations, InstalledDist, Requirement, Resolution, SourceDist,
|
|
||||||
};
|
|
||||||
use pep508_rs::PackageName;
|
use pep508_rs::PackageName;
|
||||||
|
use pypi_types::Requirement;
|
||||||
use uv_cache::Cache;
|
use uv_cache::Cache;
|
||||||
use uv_configuration::{BuildKind, NoBinary, NoBuild, SetupPyStrategy};
|
use uv_configuration::{BuildKind, NoBinary, NoBuild, SetupPyStrategy};
|
||||||
use uv_interpreter::{Interpreter, PythonEnvironment};
|
use uv_interpreter::{Interpreter, PythonEnvironment};
|
||||||
|
|
|
@ -112,7 +112,7 @@ pub(crate) async fn pip_compile(
|
||||||
|
|
||||||
// Read all requirements from the provided sources.
|
// Read all requirements from the provided sources.
|
||||||
let RequirementsSpecification {
|
let RequirementsSpecification {
|
||||||
project,
|
mut project,
|
||||||
requirements,
|
requirements,
|
||||||
constraints,
|
constraints,
|
||||||
overrides,
|
overrides,
|
||||||
|
@ -128,10 +128,7 @@ pub(crate) async fn pip_compile(
|
||||||
requirements,
|
requirements,
|
||||||
constraints,
|
constraints,
|
||||||
overrides,
|
overrides,
|
||||||
None,
|
|
||||||
&extras,
|
|
||||||
&client_builder,
|
&client_builder,
|
||||||
preview,
|
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
@ -327,6 +324,7 @@ pub(crate) async fn pip_compile(
|
||||||
&no_build,
|
&no_build,
|
||||||
&NoBinary::None,
|
&NoBinary::None,
|
||||||
concurrency,
|
concurrency,
|
||||||
|
preview,
|
||||||
)
|
)
|
||||||
.with_options(OptionsBuilder::new().exclude_newer(exclude_newer).build());
|
.with_options(OptionsBuilder::new().exclude_newer(exclude_newer).build());
|
||||||
|
|
||||||
|
@ -337,7 +335,7 @@ pub(crate) async fn pip_compile(
|
||||||
requirements,
|
requirements,
|
||||||
&hasher,
|
&hasher,
|
||||||
&top_level_index,
|
&top_level_index,
|
||||||
DistributionDatabase::new(&client, &build_dispatch, concurrency.downloads),
|
DistributionDatabase::new(&client, &build_dispatch, concurrency.downloads, preview),
|
||||||
)
|
)
|
||||||
.with_reporter(ResolverReporter::from(printer))
|
.with_reporter(ResolverReporter::from(printer))
|
||||||
.resolve()
|
.resolve()
|
||||||
|
@ -345,17 +343,52 @@ pub(crate) async fn pip_compile(
|
||||||
|
|
||||||
// Resolve any source trees into requirements.
|
// Resolve any source trees into requirements.
|
||||||
if !source_trees.is_empty() {
|
if !source_trees.is_empty() {
|
||||||
|
let resolutions = SourceTreeResolver::new(
|
||||||
|
source_trees,
|
||||||
|
&extras,
|
||||||
|
&hasher,
|
||||||
|
&top_level_index,
|
||||||
|
DistributionDatabase::new(&client, &build_dispatch, concurrency.downloads, preview),
|
||||||
|
)
|
||||||
|
.with_reporter(ResolverReporter::from(printer))
|
||||||
|
.resolve()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// If we resolved a single project, use it for the project name.
|
||||||
|
project = project.or_else(|| {
|
||||||
|
if let [resolution] = &resolutions[..] {
|
||||||
|
Some(resolution.project.clone())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// If any of the extras were unused, surface a warning.
|
||||||
|
if let ExtrasSpecification::Some(extras) = extras {
|
||||||
|
let mut unused_extras = extras
|
||||||
|
.iter()
|
||||||
|
.filter(|extra| {
|
||||||
|
!resolutions
|
||||||
|
.iter()
|
||||||
|
.any(|resolution| resolution.extras.contains(extra))
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
if !unused_extras.is_empty() {
|
||||||
|
unused_extras.sort_unstable();
|
||||||
|
unused_extras.dedup();
|
||||||
|
let s = if unused_extras.len() == 1 { "" } else { "s" };
|
||||||
|
return Err(anyhow!(
|
||||||
|
"Requested extra{s} not found: {}",
|
||||||
|
unused_extras.iter().join(", ")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extend the requirements with the resolved source trees.
|
||||||
requirements.extend(
|
requirements.extend(
|
||||||
SourceTreeResolver::new(
|
resolutions
|
||||||
source_trees,
|
.into_iter()
|
||||||
&extras,
|
.flat_map(|resolution| resolution.requirements),
|
||||||
&hasher,
|
|
||||||
&top_level_index,
|
|
||||||
DistributionDatabase::new(&client, &build_dispatch, concurrency.downloads),
|
|
||||||
)
|
|
||||||
.with_reporter(ResolverReporter::from(printer))
|
|
||||||
.resolve()
|
|
||||||
.await?,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -367,7 +400,7 @@ pub(crate) async fn pip_compile(
|
||||||
overrides,
|
overrides,
|
||||||
&hasher,
|
&hasher,
|
||||||
&top_level_index,
|
&top_level_index,
|
||||||
DistributionDatabase::new(&client, &build_dispatch, concurrency.downloads),
|
DistributionDatabase::new(&client, &build_dispatch, concurrency.downloads, preview),
|
||||||
)
|
)
|
||||||
.with_reporter(ResolverReporter::from(printer))
|
.with_reporter(ResolverReporter::from(printer))
|
||||||
.resolve()
|
.resolve()
|
||||||
|
@ -425,7 +458,7 @@ pub(crate) async fn pip_compile(
|
||||||
&overrides,
|
&overrides,
|
||||||
&hasher,
|
&hasher,
|
||||||
&top_level_index,
|
&top_level_index,
|
||||||
DistributionDatabase::new(&client, &build_dispatch, concurrency.downloads),
|
DistributionDatabase::new(&client, &build_dispatch, concurrency.downloads, preview),
|
||||||
)
|
)
|
||||||
.with_reporter(ResolverReporter::from(printer))
|
.with_reporter(ResolverReporter::from(printer))
|
||||||
.resolve(marker_filter)
|
.resolve(marker_filter)
|
||||||
|
@ -466,7 +499,7 @@ pub(crate) async fn pip_compile(
|
||||||
&hasher,
|
&hasher,
|
||||||
&build_dispatch,
|
&build_dispatch,
|
||||||
EmptyInstalledPackages,
|
EmptyInstalledPackages,
|
||||||
DistributionDatabase::new(&client, &build_dispatch, concurrency.downloads),
|
DistributionDatabase::new(&client, &build_dispatch, concurrency.downloads, preview),
|
||||||
)?
|
)?
|
||||||
.with_reporter(ResolverReporter::from(printer));
|
.with_reporter(ResolverReporter::from(printer));
|
||||||
|
|
||||||
|
|
|
@ -100,10 +100,8 @@ pub(crate) async fn pip_install(
|
||||||
requirements,
|
requirements,
|
||||||
constraints,
|
constraints,
|
||||||
overrides,
|
overrides,
|
||||||
None,
|
|
||||||
extras,
|
extras,
|
||||||
&client_builder,
|
&client_builder,
|
||||||
preview,
|
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
@ -318,6 +316,7 @@ pub(crate) async fn pip_install(
|
||||||
&no_build,
|
&no_build,
|
||||||
&no_binary,
|
&no_binary,
|
||||||
concurrency,
|
concurrency,
|
||||||
|
preview,
|
||||||
)
|
)
|
||||||
.with_options(OptionsBuilder::new().exclude_newer(exclude_newer).build());
|
.with_options(OptionsBuilder::new().exclude_newer(exclude_newer).build());
|
||||||
|
|
||||||
|
@ -358,6 +357,7 @@ pub(crate) async fn pip_install(
|
||||||
concurrency,
|
concurrency,
|
||||||
options,
|
options,
|
||||||
printer,
|
printer,
|
||||||
|
preview,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
|
@ -395,6 +395,7 @@ pub(crate) async fn pip_install(
|
||||||
&no_build,
|
&no_build,
|
||||||
&no_binary,
|
&no_binary,
|
||||||
concurrency,
|
concurrency,
|
||||||
|
preview,
|
||||||
)
|
)
|
||||||
.with_options(OptionsBuilder::new().exclude_newer(exclude_newer).build())
|
.with_options(OptionsBuilder::new().exclude_newer(exclude_newer).build())
|
||||||
};
|
};
|
||||||
|
@ -419,6 +420,7 @@ pub(crate) async fn pip_install(
|
||||||
&venv,
|
&venv,
|
||||||
dry_run,
|
dry_run,
|
||||||
printer,
|
printer,
|
||||||
|
preview,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
|
|
@ -9,8 +9,7 @@ use owo_colors::OwoColorize;
|
||||||
use tracing::debug;
|
use tracing::debug;
|
||||||
|
|
||||||
use distribution_types::{
|
use distribution_types::{
|
||||||
CachedDist, Diagnostic, InstalledDist, Requirement, ResolutionDiagnostic,
|
CachedDist, Diagnostic, InstalledDist, ResolutionDiagnostic, UnresolvedRequirementSpecification,
|
||||||
UnresolvedRequirementSpecification,
|
|
||||||
};
|
};
|
||||||
use distribution_types::{
|
use distribution_types::{
|
||||||
DistributionMetadata, IndexLocations, InstalledMetadata, LocalDist, Name, Resolution,
|
DistributionMetadata, IndexLocations, InstalledMetadata, LocalDist, Name, Resolution,
|
||||||
|
@ -18,6 +17,7 @@ use distribution_types::{
|
||||||
use install_wheel_rs::linker::LinkMode;
|
use install_wheel_rs::linker::LinkMode;
|
||||||
use pep508_rs::MarkerEnvironment;
|
use pep508_rs::MarkerEnvironment;
|
||||||
use platform_tags::Tags;
|
use platform_tags::Tags;
|
||||||
|
use pypi_types::Requirement;
|
||||||
use uv_cache::Cache;
|
use uv_cache::Cache;
|
||||||
use uv_client::{BaseClientBuilder, RegistryClient};
|
use uv_client::{BaseClientBuilder, RegistryClient};
|
||||||
use uv_configuration::{
|
use uv_configuration::{
|
||||||
|
@ -32,7 +32,7 @@ use uv_interpreter::{Interpreter, PythonEnvironment};
|
||||||
use uv_normalize::PackageName;
|
use uv_normalize::PackageName;
|
||||||
use uv_requirements::{
|
use uv_requirements::{
|
||||||
LookaheadResolver, NamedRequirementsResolver, RequirementsSource, RequirementsSpecification,
|
LookaheadResolver, NamedRequirementsResolver, RequirementsSource, RequirementsSpecification,
|
||||||
SourceTreeResolver, Workspace,
|
SourceTreeResolver,
|
||||||
};
|
};
|
||||||
use uv_resolver::{
|
use uv_resolver::{
|
||||||
DependencyMode, Exclusions, FlatIndex, InMemoryIndex, Manifest, Options, Preference,
|
DependencyMode, Exclusions, FlatIndex, InMemoryIndex, Manifest, Options, Preference,
|
||||||
|
@ -42,8 +42,7 @@ use uv_types::{HashStrategy, InFlight, InstalledPackagesProvider};
|
||||||
use uv_warnings::warn_user;
|
use uv_warnings::warn_user;
|
||||||
|
|
||||||
use crate::commands::reporters::{DownloadReporter, InstallReporter, ResolverReporter};
|
use crate::commands::reporters::{DownloadReporter, InstallReporter, ResolverReporter};
|
||||||
use crate::commands::DryRunEvent;
|
use crate::commands::{compile_bytecode, elapsed, ChangeEvent, ChangeEventKind, DryRunEvent};
|
||||||
use crate::commands::{compile_bytecode, elapsed, ChangeEvent, ChangeEventKind};
|
|
||||||
use crate::printer::Printer;
|
use crate::printer::Printer;
|
||||||
|
|
||||||
/// Consolidate the requirements for an installation.
|
/// Consolidate the requirements for an installation.
|
||||||
|
@ -51,10 +50,8 @@ pub(crate) async fn read_requirements(
|
||||||
requirements: &[RequirementsSource],
|
requirements: &[RequirementsSource],
|
||||||
constraints: &[RequirementsSource],
|
constraints: &[RequirementsSource],
|
||||||
overrides: &[RequirementsSource],
|
overrides: &[RequirementsSource],
|
||||||
workspace: Option<&Workspace>,
|
|
||||||
extras: &ExtrasSpecification,
|
extras: &ExtrasSpecification,
|
||||||
client_builder: &BaseClientBuilder<'_>,
|
client_builder: &BaseClientBuilder<'_>,
|
||||||
preview: PreviewMode,
|
|
||||||
) -> Result<RequirementsSpecification, Error> {
|
) -> Result<RequirementsSpecification, Error> {
|
||||||
// If the user requests `extras` but does not provide a valid source (e.g., a `pyproject.toml`),
|
// If the user requests `extras` but does not provide a valid source (e.g., a `pyproject.toml`),
|
||||||
// return an error.
|
// return an error.
|
||||||
|
@ -66,39 +63,13 @@ pub(crate) async fn read_requirements(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read all requirements from the provided sources.
|
// Read all requirements from the provided sources.
|
||||||
let spec = RequirementsSpecification::from_sources(
|
Ok(RequirementsSpecification::from_sources(
|
||||||
requirements,
|
requirements,
|
||||||
constraints,
|
constraints,
|
||||||
overrides,
|
overrides,
|
||||||
workspace,
|
|
||||||
extras,
|
|
||||||
client_builder,
|
client_builder,
|
||||||
preview,
|
|
||||||
)
|
)
|
||||||
.await?;
|
.await?)
|
||||||
|
|
||||||
// If all the metadata could be statically resolved, validate that every extra was used. If we
|
|
||||||
// need to resolve metadata via PEP 517, we don't know which extras are used until much later.
|
|
||||||
if spec.source_trees.is_empty() {
|
|
||||||
if let ExtrasSpecification::Some(extras) = extras {
|
|
||||||
let mut unused_extras = extras
|
|
||||||
.iter()
|
|
||||||
.filter(|extra| !spec.extras.contains(extra))
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
if !unused_extras.is_empty() {
|
|
||||||
unused_extras.sort_unstable();
|
|
||||||
unused_extras.dedup();
|
|
||||||
let s = if unused_extras.len() == 1 { "" } else { "s" };
|
|
||||||
return Err(anyhow!(
|
|
||||||
"Requested extra{s} not found: {}",
|
|
||||||
unused_extras.iter().join(", ")
|
|
||||||
)
|
|
||||||
.into());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(spec)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resolve a set of requirements, similar to running `pip compile`.
|
/// Resolve a set of requirements, similar to running `pip compile`.
|
||||||
|
@ -108,7 +79,7 @@ pub(crate) async fn resolve<InstalledPackages: InstalledPackagesProvider>(
|
||||||
constraints: Vec<Requirement>,
|
constraints: Vec<Requirement>,
|
||||||
overrides: Vec<UnresolvedRequirementSpecification>,
|
overrides: Vec<UnresolvedRequirementSpecification>,
|
||||||
source_trees: Vec<PathBuf>,
|
source_trees: Vec<PathBuf>,
|
||||||
project: Option<PackageName>,
|
mut project: Option<PackageName>,
|
||||||
extras: &ExtrasSpecification,
|
extras: &ExtrasSpecification,
|
||||||
preferences: Vec<Preference>,
|
preferences: Vec<Preference>,
|
||||||
installed_packages: InstalledPackages,
|
installed_packages: InstalledPackages,
|
||||||
|
@ -125,6 +96,7 @@ pub(crate) async fn resolve<InstalledPackages: InstalledPackagesProvider>(
|
||||||
concurrency: Concurrency,
|
concurrency: Concurrency,
|
||||||
options: Options,
|
options: Options,
|
||||||
printer: Printer,
|
printer: Printer,
|
||||||
|
preview: PreviewMode,
|
||||||
) -> Result<ResolutionGraph, Error> {
|
) -> Result<ResolutionGraph, Error> {
|
||||||
let start = std::time::Instant::now();
|
let start = std::time::Instant::now();
|
||||||
|
|
||||||
|
@ -135,7 +107,7 @@ pub(crate) async fn resolve<InstalledPackages: InstalledPackagesProvider>(
|
||||||
requirements,
|
requirements,
|
||||||
hasher,
|
hasher,
|
||||||
index,
|
index,
|
||||||
DistributionDatabase::new(client, build_dispatch, concurrency.downloads),
|
DistributionDatabase::new(client, build_dispatch, concurrency.downloads, preview),
|
||||||
)
|
)
|
||||||
.with_reporter(ResolverReporter::from(printer))
|
.with_reporter(ResolverReporter::from(printer))
|
||||||
.resolve()
|
.resolve()
|
||||||
|
@ -143,17 +115,53 @@ pub(crate) async fn resolve<InstalledPackages: InstalledPackagesProvider>(
|
||||||
|
|
||||||
// Resolve any source trees into requirements.
|
// Resolve any source trees into requirements.
|
||||||
if !source_trees.is_empty() {
|
if !source_trees.is_empty() {
|
||||||
|
let resolutions = SourceTreeResolver::new(
|
||||||
|
source_trees,
|
||||||
|
extras,
|
||||||
|
hasher,
|
||||||
|
index,
|
||||||
|
DistributionDatabase::new(client, build_dispatch, concurrency.downloads, preview),
|
||||||
|
)
|
||||||
|
.with_reporter(ResolverReporter::from(printer))
|
||||||
|
.resolve()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// If we resolved a single project, use it for the project name.
|
||||||
|
project = project.or_else(|| {
|
||||||
|
if let [resolution] = &resolutions[..] {
|
||||||
|
Some(resolution.project.clone())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// If any of the extras were unused, surface a warning.
|
||||||
|
if let ExtrasSpecification::Some(extras) = extras {
|
||||||
|
let mut unused_extras = extras
|
||||||
|
.iter()
|
||||||
|
.filter(|extra| {
|
||||||
|
!resolutions
|
||||||
|
.iter()
|
||||||
|
.any(|resolution| resolution.extras.contains(extra))
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
if !unused_extras.is_empty() {
|
||||||
|
unused_extras.sort_unstable();
|
||||||
|
unused_extras.dedup();
|
||||||
|
let s = if unused_extras.len() == 1 { "" } else { "s" };
|
||||||
|
return Err(anyhow!(
|
||||||
|
"Requested extra{s} not found: {}",
|
||||||
|
unused_extras.iter().join(", ")
|
||||||
|
)
|
||||||
|
.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extend the requirements with the resolved source trees.
|
||||||
requirements.extend(
|
requirements.extend(
|
||||||
SourceTreeResolver::new(
|
resolutions
|
||||||
source_trees,
|
.into_iter()
|
||||||
extras,
|
.flat_map(|resolution| resolution.requirements),
|
||||||
hasher,
|
|
||||||
index,
|
|
||||||
DistributionDatabase::new(client, build_dispatch, concurrency.downloads),
|
|
||||||
)
|
|
||||||
.with_reporter(ResolverReporter::from(printer))
|
|
||||||
.resolve()
|
|
||||||
.await?,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -165,7 +173,7 @@ pub(crate) async fn resolve<InstalledPackages: InstalledPackagesProvider>(
|
||||||
overrides,
|
overrides,
|
||||||
hasher,
|
hasher,
|
||||||
index,
|
index,
|
||||||
DistributionDatabase::new(client, build_dispatch, concurrency.downloads),
|
DistributionDatabase::new(client, build_dispatch, concurrency.downloads, preview),
|
||||||
)
|
)
|
||||||
.with_reporter(ResolverReporter::from(printer))
|
.with_reporter(ResolverReporter::from(printer))
|
||||||
.resolve()
|
.resolve()
|
||||||
|
@ -185,7 +193,7 @@ pub(crate) async fn resolve<InstalledPackages: InstalledPackagesProvider>(
|
||||||
&overrides,
|
&overrides,
|
||||||
hasher,
|
hasher,
|
||||||
index,
|
index,
|
||||||
DistributionDatabase::new(client, build_dispatch, concurrency.downloads),
|
DistributionDatabase::new(client, build_dispatch, concurrency.downloads, preview),
|
||||||
)
|
)
|
||||||
.with_reporter(ResolverReporter::from(printer))
|
.with_reporter(ResolverReporter::from(printer))
|
||||||
.resolve(Some(markers))
|
.resolve(Some(markers))
|
||||||
|
@ -229,7 +237,7 @@ pub(crate) async fn resolve<InstalledPackages: InstalledPackagesProvider>(
|
||||||
hasher,
|
hasher,
|
||||||
build_dispatch,
|
build_dispatch,
|
||||||
installed_packages,
|
installed_packages,
|
||||||
DistributionDatabase::new(client, build_dispatch, concurrency.downloads),
|
DistributionDatabase::new(client, build_dispatch, concurrency.downloads, preview),
|
||||||
)?
|
)?
|
||||||
.with_reporter(reporter);
|
.with_reporter(reporter);
|
||||||
|
|
||||||
|
@ -287,6 +295,7 @@ pub(crate) async fn install(
|
||||||
venv: &PythonEnvironment,
|
venv: &PythonEnvironment,
|
||||||
dry_run: bool,
|
dry_run: bool,
|
||||||
printer: Printer,
|
printer: Printer,
|
||||||
|
preview: PreviewMode,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let start = std::time::Instant::now();
|
let start = std::time::Instant::now();
|
||||||
|
|
||||||
|
@ -362,7 +371,7 @@ pub(crate) async fn install(
|
||||||
cache,
|
cache,
|
||||||
tags,
|
tags,
|
||||||
hasher,
|
hasher,
|
||||||
DistributionDatabase::new(client, build_dispatch, concurrency.downloads),
|
DistributionDatabase::new(client, build_dispatch, concurrency.downloads, preview),
|
||||||
)
|
)
|
||||||
.with_reporter(DownloadReporter::from(printer).with_length(remote.len() as u64));
|
.with_reporter(DownloadReporter::from(printer).with_length(remote.len() as u64));
|
||||||
|
|
||||||
|
|
|
@ -97,10 +97,8 @@ pub(crate) async fn pip_sync(
|
||||||
requirements,
|
requirements,
|
||||||
constraints,
|
constraints,
|
||||||
overrides,
|
overrides,
|
||||||
None,
|
|
||||||
&extras,
|
&extras,
|
||||||
&client_builder,
|
&client_builder,
|
||||||
preview,
|
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
@ -269,6 +267,7 @@ pub(crate) async fn pip_sync(
|
||||||
&no_build,
|
&no_build,
|
||||||
&no_binary,
|
&no_binary,
|
||||||
concurrency,
|
concurrency,
|
||||||
|
preview,
|
||||||
)
|
)
|
||||||
.with_options(OptionsBuilder::new().exclude_newer(exclude_newer).build());
|
.with_options(OptionsBuilder::new().exclude_newer(exclude_newer).build());
|
||||||
|
|
||||||
|
@ -308,6 +307,7 @@ pub(crate) async fn pip_sync(
|
||||||
concurrency,
|
concurrency,
|
||||||
options,
|
options,
|
||||||
printer,
|
printer,
|
||||||
|
preview,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
|
@ -344,6 +344,7 @@ pub(crate) async fn pip_sync(
|
||||||
&no_build,
|
&no_build,
|
||||||
&no_binary,
|
&no_binary,
|
||||||
concurrency,
|
concurrency,
|
||||||
|
preview,
|
||||||
)
|
)
|
||||||
.with_options(OptionsBuilder::new().exclude_newer(exclude_newer).build())
|
.with_options(OptionsBuilder::new().exclude_newer(exclude_newer).build())
|
||||||
};
|
};
|
||||||
|
@ -368,6 +369,7 @@ pub(crate) async fn pip_sync(
|
||||||
&venv,
|
&venv,
|
||||||
dry_run,
|
dry_run,
|
||||||
printer,
|
printer,
|
||||||
|
preview,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
|
|
@ -5,8 +5,9 @@ use itertools::{Either, Itertools};
|
||||||
use owo_colors::OwoColorize;
|
use owo_colors::OwoColorize;
|
||||||
use tracing::debug;
|
use tracing::debug;
|
||||||
|
|
||||||
use distribution_types::{InstalledMetadata, Name, Requirement, UnresolvedRequirement};
|
use distribution_types::{InstalledMetadata, Name, UnresolvedRequirement};
|
||||||
use pep508_rs::UnnamedRequirement;
|
use pep508_rs::UnnamedRequirement;
|
||||||
|
use pypi_types::Requirement;
|
||||||
use pypi_types::VerbatimParsedUrl;
|
use pypi_types::VerbatimParsedUrl;
|
||||||
use uv_cache::Cache;
|
use uv_cache::Cache;
|
||||||
use uv_client::{BaseClientBuilder, Connectivity};
|
use uv_client::{BaseClientBuilder, Connectivity};
|
||||||
|
@ -40,8 +41,7 @@ pub(crate) async fn pip_uninstall(
|
||||||
.keyring(keyring_provider);
|
.keyring(keyring_provider);
|
||||||
|
|
||||||
// Read all requirements from the provided sources.
|
// Read all requirements from the provided sources.
|
||||||
let spec =
|
let spec = RequirementsSpecification::from_simple_sources(sources, &client_builder).await?;
|
||||||
RequirementsSpecification::from_simple_sources(sources, &client_builder, preview).await?;
|
|
||||||
|
|
||||||
// Detect the current Python interpreter.
|
// Detect the current Python interpreter.
|
||||||
let system = if system {
|
let system = if system {
|
||||||
|
|
|
@ -10,9 +10,9 @@ use uv_configuration::{
|
||||||
SetupPyStrategy, Upgrade,
|
SetupPyStrategy, Upgrade,
|
||||||
};
|
};
|
||||||
use uv_dispatch::BuildDispatch;
|
use uv_dispatch::BuildDispatch;
|
||||||
|
use uv_distribution::ProjectWorkspace;
|
||||||
use uv_interpreter::PythonEnvironment;
|
use uv_interpreter::PythonEnvironment;
|
||||||
use uv_requirements::upgrade::read_lockfile;
|
use uv_requirements::upgrade::read_lockfile;
|
||||||
use uv_requirements::ProjectWorkspace;
|
|
||||||
use uv_resolver::{ExcludeNewer, FlatIndex, InMemoryIndex, Lock, OptionsBuilder};
|
use uv_resolver::{ExcludeNewer, FlatIndex, InMemoryIndex, Lock, OptionsBuilder};
|
||||||
use uv_types::{BuildIsolation, EmptyInstalledPackages, HashStrategy, InFlight};
|
use uv_types::{BuildIsolation, EmptyInstalledPackages, HashStrategy, InFlight};
|
||||||
use uv_warnings::warn_user;
|
use uv_warnings::warn_user;
|
||||||
|
@ -41,7 +41,17 @@ pub(crate) async fn lock(
|
||||||
let venv = project::init_environment(&project, preview, cache, printer)?;
|
let venv = project::init_environment(&project, preview, cache, printer)?;
|
||||||
|
|
||||||
// Perform the lock operation.
|
// Perform the lock operation.
|
||||||
match do_lock(&project, &venv, upgrade, exclude_newer, cache, printer).await {
|
match do_lock(
|
||||||
|
&project,
|
||||||
|
&venv,
|
||||||
|
upgrade,
|
||||||
|
exclude_newer,
|
||||||
|
preview,
|
||||||
|
cache,
|
||||||
|
printer,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
Ok(_) => Ok(ExitStatus::Success),
|
Ok(_) => Ok(ExitStatus::Success),
|
||||||
Err(ProjectError::Operation(pip::operations::Error::Resolve(
|
Err(ProjectError::Operation(pip::operations::Error::Resolve(
|
||||||
uv_resolver::ResolveError::NoSolution(err),
|
uv_resolver::ResolveError::NoSolution(err),
|
||||||
|
@ -61,6 +71,7 @@ pub(super) async fn do_lock(
|
||||||
venv: &PythonEnvironment,
|
venv: &PythonEnvironment,
|
||||||
upgrade: Upgrade,
|
upgrade: Upgrade,
|
||||||
exclude_newer: Option<ExcludeNewer>,
|
exclude_newer: Option<ExcludeNewer>,
|
||||||
|
preview: PreviewMode,
|
||||||
cache: &Cache,
|
cache: &Cache,
|
||||||
printer: Printer,
|
printer: Printer,
|
||||||
) -> Result<Lock, ProjectError> {
|
) -> Result<Lock, ProjectError> {
|
||||||
|
@ -124,6 +135,7 @@ pub(super) async fn do_lock(
|
||||||
&no_build,
|
&no_build,
|
||||||
&no_binary,
|
&no_binary,
|
||||||
concurrency,
|
concurrency,
|
||||||
|
preview,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Resolve the requirements.
|
// Resolve the requirements.
|
||||||
|
@ -149,6 +161,7 @@ pub(super) async fn do_lock(
|
||||||
concurrency,
|
concurrency,
|
||||||
options,
|
options,
|
||||||
printer,
|
printer,
|
||||||
|
preview,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
|
|
@ -14,10 +14,11 @@ use uv_configuration::{
|
||||||
SetupPyStrategy, Upgrade,
|
SetupPyStrategy, Upgrade,
|
||||||
};
|
};
|
||||||
use uv_dispatch::BuildDispatch;
|
use uv_dispatch::BuildDispatch;
|
||||||
|
use uv_distribution::ProjectWorkspace;
|
||||||
use uv_fs::Simplified;
|
use uv_fs::Simplified;
|
||||||
use uv_installer::{SatisfiesResult, SitePackages};
|
use uv_installer::{SatisfiesResult, SitePackages};
|
||||||
use uv_interpreter::{find_default_interpreter, PythonEnvironment};
|
use uv_interpreter::{find_default_interpreter, PythonEnvironment};
|
||||||
use uv_requirements::{ProjectWorkspace, RequirementsSource, RequirementsSpecification, Workspace};
|
use uv_requirements::{RequirementsSource, RequirementsSpecification};
|
||||||
use uv_resolver::{FlatIndex, InMemoryIndex, Options};
|
use uv_resolver::{FlatIndex, InMemoryIndex, Options};
|
||||||
use uv_types::{BuildIsolation, HashStrategy, InFlight};
|
use uv_types::{BuildIsolation, HashStrategy, InFlight};
|
||||||
|
|
||||||
|
@ -107,11 +108,10 @@ pub(crate) fn init_environment(
|
||||||
pub(crate) async fn update_environment(
|
pub(crate) async fn update_environment(
|
||||||
venv: PythonEnvironment,
|
venv: PythonEnvironment,
|
||||||
requirements: &[RequirementsSource],
|
requirements: &[RequirementsSource],
|
||||||
workspace: Option<&Workspace>,
|
|
||||||
preview: PreviewMode,
|
|
||||||
connectivity: Connectivity,
|
connectivity: Connectivity,
|
||||||
cache: &Cache,
|
cache: &Cache,
|
||||||
printer: Printer,
|
printer: Printer,
|
||||||
|
preview: PreviewMode,
|
||||||
) -> Result<PythonEnvironment> {
|
) -> Result<PythonEnvironment> {
|
||||||
// TODO(zanieb): Support client configuration
|
// TODO(zanieb): Support client configuration
|
||||||
let client_builder = BaseClientBuilder::default().connectivity(connectivity);
|
let client_builder = BaseClientBuilder::default().connectivity(connectivity);
|
||||||
|
@ -119,16 +119,8 @@ pub(crate) async fn update_environment(
|
||||||
// Read all requirements from the provided sources.
|
// Read all requirements from the provided sources.
|
||||||
// TODO(zanieb): Consider allowing constraints and extras
|
// TODO(zanieb): Consider allowing constraints and extras
|
||||||
// TODO(zanieb): Allow specifying extras somehow
|
// TODO(zanieb): Allow specifying extras somehow
|
||||||
let spec = RequirementsSpecification::from_sources(
|
let spec =
|
||||||
requirements,
|
RequirementsSpecification::from_sources(requirements, &[], &[], &client_builder).await?;
|
||||||
&[],
|
|
||||||
&[],
|
|
||||||
workspace,
|
|
||||||
&ExtrasSpecification::None,
|
|
||||||
&client_builder,
|
|
||||||
preview,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// Check if the current environment satisfies the requirements
|
// Check if the current environment satisfies the requirements
|
||||||
let site_packages = SitePackages::from_executable(&venv)?;
|
let site_packages = SitePackages::from_executable(&venv)?;
|
||||||
|
@ -204,6 +196,7 @@ pub(crate) async fn update_environment(
|
||||||
&no_build,
|
&no_build,
|
||||||
&no_binary,
|
&no_binary,
|
||||||
concurrency,
|
concurrency,
|
||||||
|
preview,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Resolve the requirements.
|
// Resolve the requirements.
|
||||||
|
@ -229,6 +222,7 @@ pub(crate) async fn update_environment(
|
||||||
concurrency,
|
concurrency,
|
||||||
options,
|
options,
|
||||||
printer,
|
printer,
|
||||||
|
preview,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
|
@ -259,6 +253,7 @@ pub(crate) async fn update_environment(
|
||||||
&no_build,
|
&no_build,
|
||||||
&no_binary,
|
&no_binary,
|
||||||
concurrency,
|
concurrency,
|
||||||
|
preview,
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -282,6 +277,7 @@ pub(crate) async fn update_environment(
|
||||||
&venv,
|
&venv,
|
||||||
dry_run,
|
dry_run,
|
||||||
printer,
|
printer,
|
||||||
|
preview,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
|
|
@ -10,8 +10,9 @@ use tracing::debug;
|
||||||
use uv_cache::Cache;
|
use uv_cache::Cache;
|
||||||
use uv_client::Connectivity;
|
use uv_client::Connectivity;
|
||||||
use uv_configuration::{ExtrasSpecification, PreviewMode, Upgrade};
|
use uv_configuration::{ExtrasSpecification, PreviewMode, Upgrade};
|
||||||
|
use uv_distribution::ProjectWorkspace;
|
||||||
use uv_interpreter::{PythonEnvironment, SystemPython};
|
use uv_interpreter::{PythonEnvironment, SystemPython};
|
||||||
use uv_requirements::{ProjectWorkspace, RequirementsSource};
|
use uv_requirements::RequirementsSource;
|
||||||
use uv_resolver::ExcludeNewer;
|
use uv_resolver::ExcludeNewer;
|
||||||
use uv_warnings::warn_user;
|
use uv_warnings::warn_user;
|
||||||
|
|
||||||
|
@ -48,9 +49,17 @@ pub(crate) async fn run(
|
||||||
let venv = project::init_environment(&project, preview, cache, printer)?;
|
let venv = project::init_environment(&project, preview, cache, printer)?;
|
||||||
|
|
||||||
// Lock and sync the environment.
|
// Lock and sync the environment.
|
||||||
let lock =
|
let lock = project::lock::do_lock(
|
||||||
project::lock::do_lock(&project, &venv, upgrade, exclude_newer, cache, printer).await?;
|
&project,
|
||||||
project::sync::do_sync(&project, &venv, &lock, extras, cache, printer).await?;
|
&venv,
|
||||||
|
upgrade,
|
||||||
|
exclude_newer,
|
||||||
|
preview,
|
||||||
|
cache,
|
||||||
|
printer,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
project::sync::do_sync(&project, &venv, &lock, extras, preview, cache, printer).await?;
|
||||||
|
|
||||||
Some(venv)
|
Some(venv)
|
||||||
};
|
};
|
||||||
|
@ -92,16 +101,8 @@ pub(crate) async fn run(
|
||||||
|
|
||||||
// Install the ephemeral requirements.
|
// Install the ephemeral requirements.
|
||||||
Some(
|
Some(
|
||||||
project::update_environment(
|
project::update_environment(venv, &requirements, connectivity, cache, printer, preview)
|
||||||
venv,
|
.await?,
|
||||||
&requirements,
|
|
||||||
None,
|
|
||||||
preview,
|
|
||||||
connectivity,
|
|
||||||
cache,
|
|
||||||
printer,
|
|
||||||
)
|
|
||||||
.await?,
|
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -9,9 +9,9 @@ use uv_configuration::{
|
||||||
SetupPyStrategy,
|
SetupPyStrategy,
|
||||||
};
|
};
|
||||||
use uv_dispatch::BuildDispatch;
|
use uv_dispatch::BuildDispatch;
|
||||||
|
use uv_distribution::ProjectWorkspace;
|
||||||
use uv_installer::SitePackages;
|
use uv_installer::SitePackages;
|
||||||
use uv_interpreter::PythonEnvironment;
|
use uv_interpreter::PythonEnvironment;
|
||||||
use uv_requirements::ProjectWorkspace;
|
|
||||||
use uv_resolver::{FlatIndex, InMemoryIndex, Lock};
|
use uv_resolver::{FlatIndex, InMemoryIndex, Lock};
|
||||||
use uv_types::{BuildIsolation, HashStrategy, InFlight};
|
use uv_types::{BuildIsolation, HashStrategy, InFlight};
|
||||||
use uv_warnings::warn_user;
|
use uv_warnings::warn_user;
|
||||||
|
@ -47,7 +47,7 @@ pub(crate) async fn sync(
|
||||||
};
|
};
|
||||||
|
|
||||||
// Perform the sync operation.
|
// Perform the sync operation.
|
||||||
do_sync(&project, &venv, &lock, extras, cache, printer).await?;
|
do_sync(&project, &venv, &lock, extras, preview, cache, printer).await?;
|
||||||
|
|
||||||
Ok(ExitStatus::Success)
|
Ok(ExitStatus::Success)
|
||||||
}
|
}
|
||||||
|
@ -58,6 +58,7 @@ pub(super) async fn do_sync(
|
||||||
venv: &PythonEnvironment,
|
venv: &PythonEnvironment,
|
||||||
lock: &Lock,
|
lock: &Lock,
|
||||||
extras: ExtrasSpecification,
|
extras: ExtrasSpecification,
|
||||||
|
preview: PreviewMode,
|
||||||
cache: &Cache,
|
cache: &Cache,
|
||||||
printer: Printer,
|
printer: Printer,
|
||||||
) -> Result<(), ProjectError> {
|
) -> Result<(), ProjectError> {
|
||||||
|
@ -112,6 +113,7 @@ pub(super) async fn do_sync(
|
||||||
&no_build,
|
&no_build,
|
||||||
&no_binary,
|
&no_binary,
|
||||||
concurrency,
|
concurrency,
|
||||||
|
preview,
|
||||||
);
|
);
|
||||||
|
|
||||||
let site_packages = SitePackages::from_executable(venv)?;
|
let site_packages = SitePackages::from_executable(venv)?;
|
||||||
|
@ -136,6 +138,7 @@ pub(super) async fn do_sync(
|
||||||
venv,
|
venv,
|
||||||
dry_run,
|
dry_run,
|
||||||
printer,
|
printer,
|
||||||
|
preview,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
|
|
@ -71,18 +71,8 @@ pub(crate) async fn run(
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
// Install the ephemeral requirements.
|
// Install the ephemeral requirements.
|
||||||
let ephemeral_env = Some(
|
let ephemeral_env =
|
||||||
update_environment(
|
Some(update_environment(venv, &requirements, connectivity, cache, printer, preview).await?);
|
||||||
venv,
|
|
||||||
&requirements,
|
|
||||||
None,
|
|
||||||
preview,
|
|
||||||
connectivity,
|
|
||||||
cache,
|
|
||||||
printer,
|
|
||||||
)
|
|
||||||
.await?,
|
|
||||||
);
|
|
||||||
|
|
||||||
// TODO(zanieb): Determine the command via the package entry points
|
// TODO(zanieb): Determine the command via the package entry points
|
||||||
let command = target;
|
let command = target;
|
||||||
|
|
|
@ -9,8 +9,9 @@ use miette::{Diagnostic, IntoDiagnostic};
|
||||||
use owo_colors::OwoColorize;
|
use owo_colors::OwoColorize;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
use distribution_types::{IndexLocations, Requirement};
|
use distribution_types::IndexLocations;
|
||||||
use install_wheel_rs::linker::LinkMode;
|
use install_wheel_rs::linker::LinkMode;
|
||||||
|
use pypi_types::Requirement;
|
||||||
use uv_auth::store_credentials_from_url;
|
use uv_auth::store_credentials_from_url;
|
||||||
use uv_cache::Cache;
|
use uv_cache::Cache;
|
||||||
use uv_client::{Connectivity, FlatIndexClient, RegistryClientBuilder};
|
use uv_client::{Connectivity, FlatIndexClient, RegistryClientBuilder};
|
||||||
|
@ -221,6 +222,7 @@ async fn venv_impl(
|
||||||
&NoBuild::All,
|
&NoBuild::All,
|
||||||
&NoBinary::None,
|
&NoBinary::None,
|
||||||
concurrency,
|
concurrency,
|
||||||
|
preview,
|
||||||
)
|
)
|
||||||
.with_options(OptionsBuilder::new().exclude_newer(exclude_newer).build());
|
.with_options(OptionsBuilder::new().exclude_newer(exclude_newer).build());
|
||||||
|
|
||||||
|
|
|
@ -116,8 +116,7 @@ fn missing_requirements_in() {
|
||||||
----- stdout -----
|
----- stdout -----
|
||||||
|
|
||||||
----- stderr -----
|
----- stderr -----
|
||||||
error: failed to read from file `requirements.in`
|
error: File not found: `requirements.in`
|
||||||
Caused by: No such file or directory (os error 2)
|
|
||||||
"###
|
"###
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -177,6 +176,7 @@ requires = ["setuptools", "wheel"]
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "project"
|
name = "project"
|
||||||
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyio==3.7.0",
|
"anyio==3.7.0",
|
||||||
]
|
]
|
||||||
|
@ -216,6 +216,7 @@ requires = ["setuptools", "wheel"]
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "project"
|
name = "project"
|
||||||
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyio==3.7.0",
|
"anyio==3.7.0",
|
||||||
]
|
]
|
||||||
|
@ -405,6 +406,7 @@ requires = ["setuptools", "wheel"]
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "project"
|
name = "project"
|
||||||
|
version = "0.1.0"
|
||||||
dependencies = []
|
dependencies = []
|
||||||
optional-dependencies.foo = [
|
optional-dependencies.foo = [
|
||||||
"anyio==3.7.0",
|
"anyio==3.7.0",
|
||||||
|
@ -447,6 +449,7 @@ requires = ["setuptools", "wheel"]
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "project"
|
name = "project"
|
||||||
|
version = "0.1.0"
|
||||||
dependencies = []
|
dependencies = []
|
||||||
optional-dependencies."FrIeNdLy-._.-bArD" = [
|
optional-dependencies."FrIeNdLy-._.-bArD" = [
|
||||||
"anyio==3.7.0",
|
"anyio==3.7.0",
|
||||||
|
@ -489,6 +492,7 @@ requires = ["setuptools", "wheel"]
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "project"
|
name = "project"
|
||||||
|
version = "0.1.0"
|
||||||
dependencies = []
|
dependencies = []
|
||||||
optional-dependencies.foo = [
|
optional-dependencies.foo = [
|
||||||
"anyio==3.7.0",
|
"anyio==3.7.0",
|
||||||
|
@ -789,6 +793,7 @@ requires = ["setuptools", "wheel"]
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "project"
|
name = "project"
|
||||||
|
version = "0.1.0"
|
||||||
dependencies = []
|
dependencies = []
|
||||||
optional-dependencies.foo = [
|
optional-dependencies.foo = [
|
||||||
"anyio==3.7.0",
|
"anyio==3.7.0",
|
||||||
|
@ -2165,6 +2170,7 @@ requires = ["setuptools", "wheel"]
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "project"
|
name = "project"
|
||||||
|
version = "0.1.0"
|
||||||
dependencies = ["anyio==3.7.0"]
|
dependencies = ["anyio==3.7.0"]
|
||||||
optional-dependencies.foo = [
|
optional-dependencies.foo = [
|
||||||
"iniconfig==1.1.1",
|
"iniconfig==1.1.1",
|
||||||
|
@ -2220,6 +2226,7 @@ requires = ["setuptools", "wheel"]
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "project"
|
name = "project"
|
||||||
|
version = "0.1.0"
|
||||||
dependencies = ["anyio==3.7.0"]
|
dependencies = ["anyio==3.7.0"]
|
||||||
optional-dependencies.foo = [
|
optional-dependencies.foo = [
|
||||||
"iniconfig==1.1.1",
|
"iniconfig==1.1.1",
|
||||||
|
@ -2309,6 +2316,7 @@ requires = ["setuptools", "wheel"]
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "my-project"
|
name = "my-project"
|
||||||
|
version = "0.1.0"
|
||||||
dependencies = ["anyio==3.7.0", "anyio==4.0.0"]
|
dependencies = ["anyio==3.7.0", "anyio==4.0.0"]
|
||||||
"#,
|
"#,
|
||||||
)?;
|
)?;
|
||||||
|
@ -2340,6 +2348,7 @@ requires = ["setuptools", "wheel"]
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "my-project"
|
name = "my-project"
|
||||||
|
version = "0.1.0"
|
||||||
dependencies = ["anyio==300.1.4"]
|
dependencies = ["anyio==300.1.4"]
|
||||||
"#,
|
"#,
|
||||||
)?;
|
)?;
|
||||||
|
@ -6228,27 +6237,27 @@ fn pre_release_constraint() -> Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resolve from a `pyproject.toml` file with a recursive extra.
|
/// Resolve from a `pyproject.toml` file with a mutually recursive extra.
|
||||||
#[test]
|
#[test]
|
||||||
fn compile_pyproject_toml_recursive_extra() -> Result<()> {
|
fn compile_pyproject_toml_mutually_recursive_extra() -> Result<()> {
|
||||||
let context = TestContext::new("3.12");
|
let context = TestContext::new("3.12");
|
||||||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||||||
pyproject_toml.write_str(
|
pyproject_toml.write_str(
|
||||||
r#"
|
r#"
|
||||||
[project]
|
[project]
|
||||||
name = "my-project"
|
name = "project"
|
||||||
version = "0.0.1"
|
version = "0.0.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"tomli>=2,<3",
|
"anyio"
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
test = [
|
test = [
|
||||||
"pep517>=0.13,<0.14",
|
"iniconfig",
|
||||||
"my-project[dev]"
|
"project[dev]"
|
||||||
]
|
]
|
||||||
dev = [
|
dev = [
|
||||||
"my-project[test]",
|
"project[test]",
|
||||||
]
|
]
|
||||||
"#,
|
"#,
|
||||||
)?;
|
)?;
|
||||||
|
@ -6262,13 +6271,67 @@ dev = [
|
||||||
----- stdout -----
|
----- stdout -----
|
||||||
# This file was autogenerated by uv via the following command:
|
# This file was autogenerated by uv via the following command:
|
||||||
# uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z pyproject.toml --extra dev
|
# uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z pyproject.toml --extra dev
|
||||||
pep517==0.13.1
|
anyio==4.3.0
|
||||||
# via my-project (pyproject.toml)
|
# via project (pyproject.toml)
|
||||||
tomli==2.0.1
|
idna==3.6
|
||||||
# via my-project (pyproject.toml)
|
# via anyio
|
||||||
|
iniconfig==2.0.0
|
||||||
|
# via project (pyproject.toml)
|
||||||
|
sniffio==1.3.1
|
||||||
|
# via anyio
|
||||||
|
|
||||||
----- stderr -----
|
----- stderr -----
|
||||||
Resolved 2 packages in [TIME]
|
Resolved 4 packages in [TIME]
|
||||||
|
"###
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolve from a `pyproject.toml` file with a recursive extra.
|
||||||
|
#[test]
|
||||||
|
fn compile_pyproject_toml_recursive_extra() -> Result<()> {
|
||||||
|
let context = TestContext::new("3.12");
|
||||||
|
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||||||
|
pyproject_toml.write_str(
|
||||||
|
r#"
|
||||||
|
[project]
|
||||||
|
name = "project"
|
||||||
|
version = "0.0.1"
|
||||||
|
dependencies = [
|
||||||
|
"anyio"
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
test = [
|
||||||
|
"iniconfig",
|
||||||
|
]
|
||||||
|
dev = [
|
||||||
|
"project[test]",
|
||||||
|
]
|
||||||
|
"#,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
uv_snapshot!(context.compile()
|
||||||
|
.arg("pyproject.toml")
|
||||||
|
.arg("--extra")
|
||||||
|
.arg("dev"), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
# This file was autogenerated by uv via the following command:
|
||||||
|
# uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z pyproject.toml --extra dev
|
||||||
|
anyio==4.3.0
|
||||||
|
# via project (pyproject.toml)
|
||||||
|
idna==3.6
|
||||||
|
# via anyio
|
||||||
|
iniconfig==2.0.0
|
||||||
|
# via project (pyproject.toml)
|
||||||
|
sniffio==1.3.1
|
||||||
|
# via anyio
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
Resolved 4 packages in [TIME]
|
||||||
"###
|
"###
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -81,8 +81,7 @@ fn missing_requirements_txt() {
|
||||||
----- stdout -----
|
----- stdout -----
|
||||||
|
|
||||||
----- stderr -----
|
----- stderr -----
|
||||||
error: failed to read from file `requirements.txt`
|
error: File not found: `requirements.txt`
|
||||||
Caused by: No such file or directory (os error 2)
|
|
||||||
"###
|
"###
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -124,8 +123,7 @@ fn missing_pyproject_toml() {
|
||||||
----- stdout -----
|
----- stdout -----
|
||||||
|
|
||||||
----- stderr -----
|
----- stderr -----
|
||||||
error: failed to read from file `pyproject.toml`
|
error: File not found: `pyproject.toml`
|
||||||
Caused by: No such file or directory (os error 2)
|
|
||||||
"###
|
"###
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -184,41 +182,6 @@ fn invalid_pyproject_toml_schema() -> Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// For user controlled pyproject.toml files, we enforce PEP 621.
|
|
||||||
#[test]
|
|
||||||
fn invalid_pyproject_toml_requirement_direct() -> Result<()> {
|
|
||||||
let context = TestContext::new("3.12");
|
|
||||||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
|
||||||
pyproject_toml.write_str(
|
|
||||||
r#"[project]
|
|
||||||
name = "project"
|
|
||||||
dependencies = ["flask==1.0.x"]
|
|
||||||
"#,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let filters = [("exit status", "exit code")]
|
|
||||||
.into_iter()
|
|
||||||
.chain(context.filters())
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
uv_snapshot!(filters, context.install()
|
|
||||||
.arg("-r")
|
|
||||||
.arg("pyproject.toml"), @r###"
|
|
||||||
success: false
|
|
||||||
exit_code: 2
|
|
||||||
----- stdout -----
|
|
||||||
|
|
||||||
----- stderr -----
|
|
||||||
error: Failed to parse: `pyproject.toml`
|
|
||||||
Caused by: after parsing '1.0', found '.x', which is not part of a valid version
|
|
||||||
flask==1.0.x
|
|
||||||
^^^^^^^
|
|
||||||
"###
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// For indirect, non-user controlled pyproject.toml, we don't enforce correctness.
|
/// For indirect, non-user controlled pyproject.toml, we don't enforce correctness.
|
||||||
///
|
///
|
||||||
/// If we fail to extract the PEP 621 metadata, we fall back to treating it as a source
|
/// If we fail to extract the PEP 621 metadata, we fall back to treating it as a source
|
||||||
|
@ -4980,7 +4943,8 @@ fn tool_uv_sources() -> Result<()> {
|
||||||
----- stdout -----
|
----- stdout -----
|
||||||
|
|
||||||
----- stderr -----
|
----- stderr -----
|
||||||
Audited 6 packages in [TIME]
|
Resolved 9 packages in [TIME]
|
||||||
|
Audited 9 packages in [TIME]
|
||||||
"###
|
"###
|
||||||
);
|
);
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -5013,9 +4977,7 @@ fn tool_uv_sources_is_in_preview() -> Result<()> {
|
||||||
----- stdout -----
|
----- stdout -----
|
||||||
|
|
||||||
----- stderr -----
|
----- stderr -----
|
||||||
error: Failed to parse: `pyproject.toml`
|
error: `tool.uv.sources` is a preview feature; use `--preview` or set `UV_PREVIEW=1` to enable it
|
||||||
Caused by: Failed to parse entry for: `tqdm`
|
|
||||||
Caused by: `tool.uv.sources` is a preview feature; use `--preview` or set `UV_PREVIEW=1` to enable it
|
|
||||||
"###
|
"###
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -95,8 +95,7 @@ fn missing_requirements_txt() {
|
||||||
----- stdout -----
|
----- stdout -----
|
||||||
|
|
||||||
----- stderr -----
|
----- stderr -----
|
||||||
error: failed to read from file `requirements.txt`
|
error: File not found: `requirements.txt`
|
||||||
Caused by: No such file or directory (os error 2)
|
|
||||||
"###);
|
"###);
|
||||||
|
|
||||||
requirements_txt.assert(predicates::path::missing());
|
requirements_txt.assert(predicates::path::missing());
|
||||||
|
|
|
@ -116,8 +116,7 @@ fn missing_requirements_txt() -> Result<()> {
|
||||||
----- stdout -----
|
----- stdout -----
|
||||||
|
|
||||||
----- stderr -----
|
----- stderr -----
|
||||||
error: failed to read from file `requirements.txt`
|
error: File not found: `requirements.txt`
|
||||||
Caused by: No such file or directory (os error 2)
|
|
||||||
"###
|
"###
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -218,7 +218,7 @@ fn test_albatross_root_workspace() {
|
||||||
----- stdout -----
|
----- stdout -----
|
||||||
|
|
||||||
----- stderr -----
|
----- stderr -----
|
||||||
Audited 3 packages in [TIME]
|
Audited 1 package in [TIME]
|
||||||
"###
|
"###
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -257,7 +257,7 @@ fn test_albatross_root_workspace_bird_feeder() {
|
||||||
----- stdout -----
|
----- stdout -----
|
||||||
|
|
||||||
----- stderr -----
|
----- stderr -----
|
||||||
Audited 2 packages in [TIME]
|
Audited 1 package in [TIME]
|
||||||
"###
|
"###
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -296,7 +296,7 @@ fn test_albatross_root_workspace_albatross() {
|
||||||
----- stdout -----
|
----- stdout -----
|
||||||
|
|
||||||
----- stderr -----
|
----- stderr -----
|
||||||
Audited 2 packages in [TIME]
|
Audited 1 package in [TIME]
|
||||||
"###
|
"###
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -335,7 +335,7 @@ fn test_albatross_virtual_workspace() {
|
||||||
----- stdout -----
|
----- stdout -----
|
||||||
|
|
||||||
----- stderr -----
|
----- stderr -----
|
||||||
Audited 2 packages in [TIME]
|
Audited 1 package in [TIME]
|
||||||
"###
|
"###
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue