mirror of https://github.com/astral-sh/uv
Allow multiple source entries for each package in `tool.uv.sources` (#7745)
## Summary
This PR enables users to provide multiple source entries in
`tool.uv.sources`, e.g.:
```toml
[tool.uv.sources]
httpx = [
{ git = "https://github.com/encode/httpx", tag = "0.27.2", marker = "sys_platform == 'darwin'" },
{ git = "https://github.com/encode/httpx", tag = "0.24.1", marker = "sys_platform == 'linux'" },
]
```
The implementation is relatively straightforward: when we lower the
requirement, we now return an iterator rather than a single requirement.
In other words, the above is transformed into two requirements:
```txt
httpx @ git+https://github.com/encode/httpx@0.27.2 ; sys_platform == 'darwin'
httpx @ git+https://github.com/encode/httpx@0.24.1 ; sys_platform == 'linux'
```
We verify (at deserialization time) that the markers are
non-overlapping.
Closes https://github.com/astral-sh/uv/issues/3397.
This commit is contained in:
parent
71d5661bd8
commit
f67347e72c
|
|
@ -4789,6 +4789,7 @@ dependencies = [
|
||||||
"cache-key",
|
"cache-key",
|
||||||
"distribution-filename",
|
"distribution-filename",
|
||||||
"distribution-types",
|
"distribution-types",
|
||||||
|
"either",
|
||||||
"fs-err",
|
"fs-err",
|
||||||
"futures",
|
"futures",
|
||||||
"indoc",
|
"indoc",
|
||||||
|
|
@ -5319,6 +5320,7 @@ dependencies = [
|
||||||
"glob",
|
"glob",
|
||||||
"insta",
|
"insta",
|
||||||
"itertools 0.13.0",
|
"itertools 0.13.0",
|
||||||
|
"owo-colors",
|
||||||
"pep440_rs",
|
"pep440_rs",
|
||||||
"pep508_rs",
|
"pep508_rs",
|
||||||
"pypi-types",
|
"pypi-types",
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,11 @@ use std::ops::{Bound, Deref};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
|
use pep440_rs::{Version, VersionParseError, VersionSpecifier};
|
||||||
use pubgrub::Range;
|
use pubgrub::Range;
|
||||||
#[cfg(feature = "pyo3")]
|
#[cfg(feature = "pyo3")]
|
||||||
use pyo3::{basic::CompareOp, pyclass, pymethods};
|
use pyo3::{basic::CompareOp, pyclass, pymethods};
|
||||||
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
|
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
|
||||||
|
|
||||||
use pep440_rs::{Version, VersionParseError, VersionSpecifier};
|
|
||||||
use uv_normalize::ExtraName;
|
use uv_normalize::ExtraName;
|
||||||
|
|
||||||
use crate::cursor::Cursor;
|
use crate::cursor::Cursor;
|
||||||
|
|
@ -1667,6 +1666,28 @@ impl Display for MarkerTreeContents {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "schemars")]
|
||||||
|
impl schemars::JsonSchema for MarkerTree {
|
||||||
|
fn schema_name() -> String {
|
||||||
|
"MarkerTree".to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn json_schema(_gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
|
||||||
|
schemars::schema::SchemaObject {
|
||||||
|
instance_type: Some(schemars::schema::InstanceType::String.into()),
|
||||||
|
metadata: Some(Box::new(schemars::schema::Metadata {
|
||||||
|
description: Some(
|
||||||
|
"A PEP 508-compliant marker expression, e.g., `sys_platform == 'Darwin'`"
|
||||||
|
.to_string(),
|
||||||
|
),
|
||||||
|
..schemars::schema::Metadata::default()
|
||||||
|
})),
|
||||||
|
..schemars::schema::SchemaObject::default()
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use std::ops::Bound;
|
use std::ops::Bound;
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ uv-warnings = { workspace = true }
|
||||||
uv-workspace = { workspace = true }
|
uv-workspace = { workspace = true }
|
||||||
|
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
|
either = { workspace = true }
|
||||||
fs-err = { workspace = true }
|
fs-err = { workspace = true }
|
||||||
futures = { workspace = true }
|
futures = { workspace = true }
|
||||||
nanoid = { workspace = true }
|
nanoid = { workspace = true }
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,18 @@
|
||||||
|
use either::Either;
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use distribution_filename::DistExtension;
|
use distribution_filename::DistExtension;
|
||||||
use pep440_rs::VersionSpecifiers;
|
use pep440_rs::VersionSpecifiers;
|
||||||
use pep508_rs::{VerbatimUrl, VersionOrUrl};
|
use pep508_rs::{MarkerTree, VerbatimUrl, VersionOrUrl};
|
||||||
use pypi_types::{ParsedUrlError, Requirement, RequirementSource, VerbatimParsedUrl};
|
use pypi_types::{ParsedUrlError, Requirement, RequirementSource, VerbatimParsedUrl};
|
||||||
use uv_git::GitReference;
|
use uv_git::GitReference;
|
||||||
use uv_normalize::PackageName;
|
use uv_normalize::PackageName;
|
||||||
use uv_warnings::warn_user_once;
|
use uv_warnings::warn_user_once;
|
||||||
use uv_workspace::pyproject::{PyProjectToml, Source};
|
use uv_workspace::pyproject::{PyProjectToml, Source, Sources};
|
||||||
use uv_workspace::Workspace;
|
use uv_workspace::Workspace;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
|
@ -28,13 +28,13 @@ enum Origin {
|
||||||
|
|
||||||
impl LoweredRequirement {
|
impl LoweredRequirement {
|
||||||
/// Combine `project.dependencies` or `project.optional-dependencies` with `tool.uv.sources`.
|
/// Combine `project.dependencies` or `project.optional-dependencies` with `tool.uv.sources`.
|
||||||
pub(crate) fn from_requirement(
|
pub(crate) fn from_requirement<'data>(
|
||||||
requirement: pep508_rs::Requirement<VerbatimParsedUrl>,
|
requirement: pep508_rs::Requirement<VerbatimParsedUrl>,
|
||||||
project_name: &PackageName,
|
project_name: &'data PackageName,
|
||||||
project_dir: &Path,
|
project_dir: &'data Path,
|
||||||
project_sources: &BTreeMap<PackageName, Source>,
|
project_sources: &'data BTreeMap<PackageName, Sources>,
|
||||||
workspace: &Workspace,
|
workspace: &'data Workspace,
|
||||||
) -> Result<Self, LoweringError> {
|
) -> impl Iterator<Item = Result<LoweredRequirement, LoweringError>> + 'data {
|
||||||
let (source, origin) = if let Some(source) = project_sources.get(&requirement.name) {
|
let (source, origin) = if let Some(source) = project_sources.get(&requirement.name) {
|
||||||
(Some(source), Origin::Project)
|
(Some(source), Origin::Project)
|
||||||
} else if let Some(source) = workspace.sources().get(&requirement.name) {
|
} else if let Some(source) = workspace.sources().get(&requirement.name) {
|
||||||
|
|
@ -48,18 +48,16 @@ impl LoweredRequirement {
|
||||||
// We require that when you use a package that's part of the workspace, ...
|
// We require that when you use a package that's part of the workspace, ...
|
||||||
!workspace.packages().contains_key(&requirement.name)
|
!workspace.packages().contains_key(&requirement.name)
|
||||||
// ... it must be declared as a workspace dependency (`workspace = true`), ...
|
// ... it must be declared as a workspace dependency (`workspace = true`), ...
|
||||||
|| matches!(
|
|| source.as_ref().filter(|sources| !sources.is_empty()).is_some_and(|source| source.iter().all(|source| {
|
||||||
source,
|
matches!(source, Source::Workspace { workspace: true, .. })
|
||||||
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.
|
// ... except for recursive self-inclusion (extras that activate other extras), e.g.
|
||||||
// `framework[machine_learning]` depends on `framework[cuda]`.
|
// `framework[machine_learning]` depends on `framework[cuda]`.
|
||||||
|| &requirement.name == project_name;
|
|| &requirement.name == project_name;
|
||||||
if !workspace_package_declared {
|
if !workspace_package_declared {
|
||||||
return Err(LoweringError::UndeclaredWorkspacePackage);
|
return Either::Left(std::iter::once(Err(
|
||||||
|
LoweringError::UndeclaredWorkspacePackage,
|
||||||
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
let Some(source) = source else {
|
let Some(source) = source else {
|
||||||
|
|
@ -74,172 +72,282 @@ impl LoweredRequirement {
|
||||||
requirement.name
|
requirement.name
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return Ok(Self(Requirement::from(requirement)));
|
return Either::Left(std::iter::once(Ok(Self(Requirement::from(requirement)))));
|
||||||
};
|
};
|
||||||
|
|
||||||
let source = match source {
|
// Determine whether the markers cover the full space for the requirement. If not, fill the
|
||||||
Source::Git {
|
// remaining space with the negation of the sources.
|
||||||
git,
|
let remaining = {
|
||||||
subdirectory,
|
// Determine the space covered by the sources.
|
||||||
rev,
|
let mut total = MarkerTree::FALSE;
|
||||||
tag,
|
for source in source.iter() {
|
||||||
branch,
|
total.or(source.marker());
|
||||||
} => {
|
|
||||||
if matches!(requirement.version_or_url, Some(VersionOrUrl::Url(_))) {
|
|
||||||
return Err(LoweringError::ConflictingUrls);
|
|
||||||
}
|
|
||||||
git_source(&git, subdirectory.map(PathBuf::from), rev, tag, branch)?
|
|
||||||
}
|
}
|
||||||
Source::Url { url, subdirectory } => {
|
|
||||||
if matches!(requirement.version_or_url, Some(VersionOrUrl::Url(_))) {
|
|
||||||
return Err(LoweringError::ConflictingUrls);
|
|
||||||
}
|
|
||||||
url_source(url, subdirectory.map(PathBuf::from))?
|
|
||||||
}
|
|
||||||
Source::Path { path, editable } => {
|
|
||||||
if matches!(requirement.version_or_url, Some(VersionOrUrl::Url(_))) {
|
|
||||||
return Err(LoweringError::ConflictingUrls);
|
|
||||||
}
|
|
||||||
path_source(
|
|
||||||
PathBuf::from(path),
|
|
||||||
origin,
|
|
||||||
project_dir,
|
|
||||||
workspace.install_path(),
|
|
||||||
editable.unwrap_or(false),
|
|
||||||
)?
|
|
||||||
}
|
|
||||||
Source::Registry { index } => registry_source(&requirement, index)?,
|
|
||||||
Source::Workspace {
|
|
||||||
workspace: is_workspace,
|
|
||||||
} => {
|
|
||||||
if !is_workspace {
|
|
||||||
return Err(LoweringError::WorkspaceFalse);
|
|
||||||
}
|
|
||||||
if matches!(requirement.version_or_url, Some(VersionOrUrl::Url(_))) {
|
|
||||||
return Err(LoweringError::ConflictingUrls);
|
|
||||||
}
|
|
||||||
let member = workspace
|
|
||||||
.packages()
|
|
||||||
.get(&requirement.name)
|
|
||||||
.ok_or(LoweringError::UndeclaredWorkspacePackage)?
|
|
||||||
.clone();
|
|
||||||
|
|
||||||
// Say we have:
|
// Determine the space covered by the requirement.
|
||||||
// ```
|
let mut remaining = total.negate();
|
||||||
// root
|
remaining.and(requirement.marker.clone());
|
||||||
// ├── main_workspace <- We want to the path from here ...
|
|
||||||
// │ ├── pyproject.toml
|
|
||||||
// │ └── uv.lock
|
|
||||||
// └──current_workspace
|
|
||||||
// └── packages
|
|
||||||
// └── current_package <- ... to here.
|
|
||||||
// └── pyproject.toml
|
|
||||||
// ```
|
|
||||||
// The path we need in the lockfile: `../current_workspace/packages/current_project`
|
|
||||||
// member root: `/root/current_workspace/packages/current_project`
|
|
||||||
// workspace install root: `/root/current_workspace`
|
|
||||||
// relative to workspace: `packages/current_project`
|
|
||||||
// workspace lock root: `../current_workspace`
|
|
||||||
// relative to main workspace: `../current_workspace/packages/current_project`
|
|
||||||
let url = VerbatimUrl::from_absolute_path(member.root())?;
|
|
||||||
let install_path = url.to_file_path().map_err(|()| {
|
|
||||||
LoweringError::RelativeTo(io::Error::new(
|
|
||||||
io::ErrorKind::Other,
|
|
||||||
"Invalid path in file URL",
|
|
||||||
))
|
|
||||||
})?;
|
|
||||||
|
|
||||||
if member.pyproject_toml().is_package() {
|
LoweredRequirement(Requirement {
|
||||||
RequirementSource::Directory {
|
marker: remaining,
|
||||||
install_path,
|
..Requirement::from(requirement.clone())
|
||||||
url,
|
})
|
||||||
editable: true,
|
|
||||||
r#virtual: false,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
RequirementSource::Directory {
|
|
||||||
install_path,
|
|
||||||
url,
|
|
||||||
editable: false,
|
|
||||||
r#virtual: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Source::CatchAll { .. } => {
|
|
||||||
// Emit a dedicated error message, which is an improvement over Serde's default error.
|
|
||||||
return Err(LoweringError::InvalidEntry);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
Ok(Self(Requirement {
|
|
||||||
name: requirement.name,
|
Either::Right(
|
||||||
extras: requirement.extras,
|
source
|
||||||
marker: requirement.marker,
|
.into_iter()
|
||||||
source,
|
.map(move |source| {
|
||||||
origin: requirement.origin,
|
let (source, mut marker) = match source {
|
||||||
}))
|
Source::Git {
|
||||||
|
git,
|
||||||
|
subdirectory,
|
||||||
|
rev,
|
||||||
|
tag,
|
||||||
|
branch,
|
||||||
|
marker,
|
||||||
|
} => {
|
||||||
|
if matches!(requirement.version_or_url, Some(VersionOrUrl::Url(_))) {
|
||||||
|
return Err(LoweringError::ConflictingUrls);
|
||||||
|
}
|
||||||
|
let source = git_source(
|
||||||
|
&git,
|
||||||
|
subdirectory.map(PathBuf::from),
|
||||||
|
rev,
|
||||||
|
tag,
|
||||||
|
branch,
|
||||||
|
)?;
|
||||||
|
(source, marker)
|
||||||
|
}
|
||||||
|
Source::Url {
|
||||||
|
url,
|
||||||
|
subdirectory,
|
||||||
|
marker,
|
||||||
|
} => {
|
||||||
|
if matches!(requirement.version_or_url, Some(VersionOrUrl::Url(_))) {
|
||||||
|
return Err(LoweringError::ConflictingUrls);
|
||||||
|
}
|
||||||
|
let source = url_source(url, subdirectory.map(PathBuf::from))?;
|
||||||
|
(source, marker)
|
||||||
|
}
|
||||||
|
Source::Path {
|
||||||
|
path,
|
||||||
|
editable,
|
||||||
|
marker,
|
||||||
|
} => {
|
||||||
|
if matches!(requirement.version_or_url, Some(VersionOrUrl::Url(_))) {
|
||||||
|
return Err(LoweringError::ConflictingUrls);
|
||||||
|
}
|
||||||
|
let source = path_source(
|
||||||
|
PathBuf::from(path),
|
||||||
|
origin,
|
||||||
|
project_dir,
|
||||||
|
workspace.install_path(),
|
||||||
|
editable.unwrap_or(false),
|
||||||
|
)?;
|
||||||
|
(source, marker)
|
||||||
|
}
|
||||||
|
Source::Registry { index, marker } => {
|
||||||
|
let source = registry_source(&requirement, index)?;
|
||||||
|
(source, marker)
|
||||||
|
}
|
||||||
|
Source::Workspace {
|
||||||
|
workspace: is_workspace,
|
||||||
|
marker,
|
||||||
|
} => {
|
||||||
|
if !is_workspace {
|
||||||
|
return Err(LoweringError::WorkspaceFalse);
|
||||||
|
}
|
||||||
|
if matches!(requirement.version_or_url, Some(VersionOrUrl::Url(_))) {
|
||||||
|
return Err(LoweringError::ConflictingUrls);
|
||||||
|
}
|
||||||
|
let member = workspace
|
||||||
|
.packages()
|
||||||
|
.get(&requirement.name)
|
||||||
|
.ok_or(LoweringError::UndeclaredWorkspacePackage)?
|
||||||
|
.clone();
|
||||||
|
|
||||||
|
// Say we have:
|
||||||
|
// ```
|
||||||
|
// root
|
||||||
|
// ├── main_workspace <- We want to the path from here ...
|
||||||
|
// │ ├── pyproject.toml
|
||||||
|
// │ └── uv.lock
|
||||||
|
// └──current_workspace
|
||||||
|
// └── packages
|
||||||
|
// └── current_package <- ... to here.
|
||||||
|
// └── pyproject.toml
|
||||||
|
// ```
|
||||||
|
// The path we need in the lockfile: `../current_workspace/packages/current_project`
|
||||||
|
// member root: `/root/current_workspace/packages/current_project`
|
||||||
|
// workspace install root: `/root/current_workspace`
|
||||||
|
// relative to workspace: `packages/current_project`
|
||||||
|
// workspace lock root: `../current_workspace`
|
||||||
|
// relative to main workspace: `../current_workspace/packages/current_project`
|
||||||
|
let url = VerbatimUrl::from_absolute_path(member.root())?;
|
||||||
|
let install_path = url.to_file_path().map_err(|()| {
|
||||||
|
LoweringError::RelativeTo(io::Error::new(
|
||||||
|
io::ErrorKind::Other,
|
||||||
|
"Invalid path in file URL",
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let source = if member.pyproject_toml().is_package() {
|
||||||
|
RequirementSource::Directory {
|
||||||
|
install_path,
|
||||||
|
url,
|
||||||
|
editable: true,
|
||||||
|
r#virtual: false,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
RequirementSource::Directory {
|
||||||
|
install_path,
|
||||||
|
url,
|
||||||
|
editable: false,
|
||||||
|
r#virtual: true,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
(source, marker)
|
||||||
|
}
|
||||||
|
Source::CatchAll { .. } => {
|
||||||
|
// Emit a dedicated error message, which is an improvement over Serde's default error.
|
||||||
|
return Err(LoweringError::InvalidEntry);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
marker.and(requirement.marker.clone());
|
||||||
|
|
||||||
|
Ok(Self(Requirement {
|
||||||
|
name: requirement.name.clone(),
|
||||||
|
extras: requirement.extras.clone(),
|
||||||
|
marker,
|
||||||
|
source,
|
||||||
|
origin: requirement.origin.clone(),
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
.chain(std::iter::once(Ok(remaining)))
|
||||||
|
.filter(|requirement| match requirement {
|
||||||
|
Ok(requirement) => !requirement.0.marker.is_false(),
|
||||||
|
Err(_) => true,
|
||||||
|
}),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Lower a [`pep508_rs::Requirement`] in a non-workspace setting (for example, in a PEP 723
|
/// Lower a [`pep508_rs::Requirement`] in a non-workspace setting (for example, in a PEP 723
|
||||||
/// script, which runs in an isolated context).
|
/// script, which runs in an isolated context).
|
||||||
pub fn from_non_workspace_requirement(
|
pub fn from_non_workspace_requirement<'data>(
|
||||||
requirement: pep508_rs::Requirement<VerbatimParsedUrl>,
|
requirement: pep508_rs::Requirement<VerbatimParsedUrl>,
|
||||||
dir: &Path,
|
dir: &'data Path,
|
||||||
sources: &BTreeMap<PackageName, Source>,
|
sources: &'data BTreeMap<PackageName, Sources>,
|
||||||
) -> Result<Self, LoweringError> {
|
) -> impl Iterator<Item = Result<LoweredRequirement, LoweringError>> + 'data {
|
||||||
let source = sources.get(&requirement.name).cloned();
|
let source = sources.get(&requirement.name).cloned();
|
||||||
|
|
||||||
let Some(source) = source else {
|
let Some(source) = source else {
|
||||||
return Ok(Self(Requirement::from(requirement)));
|
return Either::Left(std::iter::once(Ok(Self(Requirement::from(requirement)))));
|
||||||
};
|
};
|
||||||
|
|
||||||
let source = match source {
|
// Determine whether the markers cover the full space for the requirement. If not, fill the
|
||||||
Source::Git {
|
// remaining space with the negation of the sources.
|
||||||
git,
|
let remaining = {
|
||||||
subdirectory,
|
// Determine the space covered by the sources.
|
||||||
rev,
|
let mut total = MarkerTree::FALSE;
|
||||||
tag,
|
for source in source.iter() {
|
||||||
branch,
|
total.or(source.marker());
|
||||||
} => {
|
|
||||||
if matches!(requirement.version_or_url, Some(VersionOrUrl::Url(_))) {
|
|
||||||
return Err(LoweringError::ConflictingUrls);
|
|
||||||
}
|
|
||||||
git_source(&git, subdirectory.map(PathBuf::from), rev, tag, branch)?
|
|
||||||
}
|
|
||||||
Source::Url { url, subdirectory } => {
|
|
||||||
if matches!(requirement.version_or_url, Some(VersionOrUrl::Url(_))) {
|
|
||||||
return Err(LoweringError::ConflictingUrls);
|
|
||||||
}
|
|
||||||
url_source(url, subdirectory.map(PathBuf::from))?
|
|
||||||
}
|
|
||||||
Source::Path { path, editable } => {
|
|
||||||
if matches!(requirement.version_or_url, Some(VersionOrUrl::Url(_))) {
|
|
||||||
return Err(LoweringError::ConflictingUrls);
|
|
||||||
}
|
|
||||||
path_source(
|
|
||||||
PathBuf::from(path),
|
|
||||||
Origin::Project,
|
|
||||||
dir,
|
|
||||||
dir,
|
|
||||||
editable.unwrap_or(false),
|
|
||||||
)?
|
|
||||||
}
|
|
||||||
Source::Registry { index } => registry_source(&requirement, index)?,
|
|
||||||
Source::Workspace { .. } => {
|
|
||||||
return Err(LoweringError::WorkspaceMember);
|
|
||||||
}
|
|
||||||
Source::CatchAll { .. } => {
|
|
||||||
// Emit a dedicated error message, which is an improvement over Serde's default
|
|
||||||
// error.
|
|
||||||
return Err(LoweringError::InvalidEntry);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Determine the space covered by the requirement.
|
||||||
|
let mut remaining = total.negate();
|
||||||
|
remaining.and(requirement.marker.clone());
|
||||||
|
|
||||||
|
LoweredRequirement(Requirement {
|
||||||
|
marker: remaining,
|
||||||
|
..Requirement::from(requirement.clone())
|
||||||
|
})
|
||||||
};
|
};
|
||||||
Ok(Self(Requirement {
|
|
||||||
name: requirement.name,
|
Either::Right(
|
||||||
extras: requirement.extras,
|
source
|
||||||
marker: requirement.marker,
|
.into_iter()
|
||||||
source,
|
.map(move |source| {
|
||||||
origin: requirement.origin,
|
let (source, mut marker) = match source {
|
||||||
}))
|
Source::Git {
|
||||||
|
git,
|
||||||
|
subdirectory,
|
||||||
|
rev,
|
||||||
|
tag,
|
||||||
|
branch,
|
||||||
|
marker,
|
||||||
|
} => {
|
||||||
|
if matches!(requirement.version_or_url, Some(VersionOrUrl::Url(_))) {
|
||||||
|
return Err(LoweringError::ConflictingUrls);
|
||||||
|
}
|
||||||
|
let source = git_source(
|
||||||
|
&git,
|
||||||
|
subdirectory.map(PathBuf::from),
|
||||||
|
rev,
|
||||||
|
tag,
|
||||||
|
branch,
|
||||||
|
)?;
|
||||||
|
(source, marker)
|
||||||
|
}
|
||||||
|
Source::Url {
|
||||||
|
url,
|
||||||
|
subdirectory,
|
||||||
|
marker,
|
||||||
|
} => {
|
||||||
|
if matches!(requirement.version_or_url, Some(VersionOrUrl::Url(_))) {
|
||||||
|
return Err(LoweringError::ConflictingUrls);
|
||||||
|
}
|
||||||
|
let source = url_source(url, subdirectory.map(PathBuf::from))?;
|
||||||
|
(source, marker)
|
||||||
|
}
|
||||||
|
Source::Path {
|
||||||
|
path,
|
||||||
|
editable,
|
||||||
|
marker,
|
||||||
|
} => {
|
||||||
|
if matches!(requirement.version_or_url, Some(VersionOrUrl::Url(_))) {
|
||||||
|
return Err(LoweringError::ConflictingUrls);
|
||||||
|
}
|
||||||
|
let source = path_source(
|
||||||
|
PathBuf::from(path),
|
||||||
|
Origin::Project,
|
||||||
|
dir,
|
||||||
|
dir,
|
||||||
|
editable.unwrap_or(false),
|
||||||
|
)?;
|
||||||
|
(source, marker)
|
||||||
|
}
|
||||||
|
Source::Registry { index, marker } => {
|
||||||
|
let source = registry_source(&requirement, index)?;
|
||||||
|
(source, marker)
|
||||||
|
}
|
||||||
|
Source::Workspace { .. } => {
|
||||||
|
return Err(LoweringError::WorkspaceMember);
|
||||||
|
}
|
||||||
|
Source::CatchAll { .. } => {
|
||||||
|
// Emit a dedicated error message, which is an improvement over Serde's default
|
||||||
|
// error.
|
||||||
|
return Err(LoweringError::InvalidEntry);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
marker.and(requirement.marker.clone());
|
||||||
|
|
||||||
|
Ok(Self(Requirement {
|
||||||
|
name: requirement.name.clone(),
|
||||||
|
extras: requirement.extras.clone(),
|
||||||
|
marker,
|
||||||
|
source,
|
||||||
|
origin: requirement.origin.clone(),
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
.chain(std::iter::once(Ok(remaining)))
|
||||||
|
.filter(|requirement| match requirement {
|
||||||
|
Ok(requirement) => !requirement.0.marker.is_false(),
|
||||||
|
Err(_) => true,
|
||||||
|
}),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert back into a [`Requirement`].
|
/// Convert back into a [`Requirement`].
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,8 @@ pub enum MetadataError {
|
||||||
Workspace(#[from] WorkspaceError),
|
Workspace(#[from] WorkspaceError),
|
||||||
#[error("Failed to parse entry for: `{0}`")]
|
#[error("Failed to parse entry for: `{0}`")]
|
||||||
LoweringError(PackageName, #[source] LoweringError),
|
LoweringError(PackageName, #[source] LoweringError),
|
||||||
|
#[error(transparent)]
|
||||||
|
Lower(#[from] LoweringError),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use crate::metadata::{LoweredRequirement, MetadataError};
|
use crate::metadata::{LoweredRequirement, MetadataError};
|
||||||
use crate::Metadata;
|
use crate::Metadata;
|
||||||
use pypi_types::Requirement;
|
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use uv_configuration::SourceStrategy;
|
use uv_configuration::SourceStrategy;
|
||||||
|
|
@ -84,7 +84,7 @@ impl RequiresDist {
|
||||||
.cloned();
|
.cloned();
|
||||||
let dev_dependencies = match source_strategy {
|
let dev_dependencies = match source_strategy {
|
||||||
SourceStrategy::Enabled => dev_dependencies
|
SourceStrategy::Enabled => dev_dependencies
|
||||||
.map(|requirement| {
|
.flat_map(|requirement| {
|
||||||
let requirement_name = requirement.name.clone();
|
let requirement_name = requirement.name.clone();
|
||||||
LoweredRequirement::from_requirement(
|
LoweredRequirement::from_requirement(
|
||||||
requirement,
|
requirement,
|
||||||
|
|
@ -93,13 +93,17 @@ impl RequiresDist {
|
||||||
sources,
|
sources,
|
||||||
project_workspace.workspace(),
|
project_workspace.workspace(),
|
||||||
)
|
)
|
||||||
.map(LoweredRequirement::into_inner)
|
.map(move |requirement| match requirement {
|
||||||
.map_err(|err| MetadataError::LoweringError(requirement_name.clone(), err))
|
Ok(requirement) => Ok(requirement.into_inner()),
|
||||||
|
Err(err) => {
|
||||||
|
Err(MetadataError::LoweringError(requirement_name.clone(), err))
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
.collect::<Result<Vec<_>, _>>()?,
|
.collect::<Result<Vec<_>, _>>()?,
|
||||||
SourceStrategy::Disabled => dev_dependencies
|
SourceStrategy::Disabled => dev_dependencies
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(Requirement::from)
|
.map(pypi_types::Requirement::from)
|
||||||
.collect(),
|
.collect(),
|
||||||
};
|
};
|
||||||
if dev_dependencies.is_empty() {
|
if dev_dependencies.is_empty() {
|
||||||
|
|
@ -112,7 +116,7 @@ impl RequiresDist {
|
||||||
let requires_dist = metadata.requires_dist.into_iter();
|
let requires_dist = metadata.requires_dist.into_iter();
|
||||||
let requires_dist = match source_strategy {
|
let requires_dist = match source_strategy {
|
||||||
SourceStrategy::Enabled => requires_dist
|
SourceStrategy::Enabled => requires_dist
|
||||||
.map(|requirement| {
|
.flat_map(|requirement| {
|
||||||
let requirement_name = requirement.name.clone();
|
let requirement_name = requirement.name.clone();
|
||||||
LoweredRequirement::from_requirement(
|
LoweredRequirement::from_requirement(
|
||||||
requirement,
|
requirement,
|
||||||
|
|
@ -121,11 +125,18 @@ impl RequiresDist {
|
||||||
sources,
|
sources,
|
||||||
project_workspace.workspace(),
|
project_workspace.workspace(),
|
||||||
)
|
)
|
||||||
.map(LoweredRequirement::into_inner)
|
.map(move |requirement| match requirement {
|
||||||
.map_err(|err| MetadataError::LoweringError(requirement_name.clone(), err))
|
Ok(requirement) => Ok(requirement.into_inner()),
|
||||||
|
Err(err) => {
|
||||||
|
Err(MetadataError::LoweringError(requirement_name.clone(), err))
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
.collect::<Result<_, _>>()?,
|
.collect::<Result<Vec<_>, _>>()?,
|
||||||
SourceStrategy::Disabled => requires_dist.into_iter().map(Requirement::from).collect(),
|
SourceStrategy::Disabled => requires_dist
|
||||||
|
.into_iter()
|
||||||
|
.map(pypi_types::Requirement::from)
|
||||||
|
.collect(),
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
|
|
@ -253,7 +264,7 @@ mod test {
|
||||||
|
|
|
|
||||||
8 | tqdm = { git = "https://github.com/tqdm/tqdm", ref = "baaaaaab" }
|
8 | tqdm = { git = "https://github.com/tqdm/tqdm", ref = "baaaaaab" }
|
||||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
data did not match any variant of untagged enum Source
|
data did not match any variant of untagged enum SourcesWire
|
||||||
|
|
||||||
"###);
|
"###);
|
||||||
}
|
}
|
||||||
|
|
@ -277,7 +288,7 @@ mod test {
|
||||||
|
|
|
|
||||||
8 | tqdm = { path = "tqdm", index = "torch" }
|
8 | tqdm = { path = "tqdm", index = "torch" }
|
||||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
data did not match any variant of untagged enum Source
|
data did not match any variant of untagged enum SourcesWire
|
||||||
|
|
||||||
"###);
|
"###);
|
||||||
}
|
}
|
||||||
|
|
@ -337,7 +348,7 @@ mod test {
|
||||||
|
|
|
|
||||||
8 | tqdm = { url = "§invalid#+#*Ä" }
|
8 | tqdm = { url = "§invalid#+#*Ä" }
|
||||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
data did not match any variant of untagged enum Source
|
data did not match any variant of untagged enum SourcesWire
|
||||||
|
|
||||||
"###);
|
"###);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ use pep440_rs::VersionSpecifiers;
|
||||||
use pep508_rs::PackageName;
|
use pep508_rs::PackageName;
|
||||||
use pypi_types::VerbatimParsedUrl;
|
use pypi_types::VerbatimParsedUrl;
|
||||||
use uv_settings::{GlobalOptions, ResolverInstallerOptions};
|
use uv_settings::{GlobalOptions, ResolverInstallerOptions};
|
||||||
use uv_workspace::pyproject::Source;
|
use uv_workspace::pyproject::Sources;
|
||||||
|
|
||||||
static FINDER: LazyLock<Finder> = LazyLock::new(|| Finder::new(b"# /// script"));
|
static FINDER: LazyLock<Finder> = LazyLock::new(|| Finder::new(b"# /// script"));
|
||||||
|
|
||||||
|
|
@ -193,7 +193,7 @@ pub struct ToolUv {
|
||||||
pub globals: GlobalOptions,
|
pub globals: GlobalOptions,
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
pub top_level: ResolverInstallerOptions,
|
pub top_level: ResolverInstallerOptions,
|
||||||
pub sources: Option<BTreeMap<PackageName, Source>>,
|
pub sources: Option<BTreeMap<PackageName, Sources>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,8 @@ uv-options-metadata = { workspace = true }
|
||||||
either = { workspace = true }
|
either = { workspace = true }
|
||||||
fs-err = { workspace = true }
|
fs-err = { workspace = true }
|
||||||
glob = { workspace = true }
|
glob = { workspace = true }
|
||||||
|
itertools = { workspace = true }
|
||||||
|
owo-colors = { workspace = true }
|
||||||
rustc-hash = { workspace = true }
|
rustc-hash = { workspace = true }
|
||||||
same-file = { workspace = true }
|
same-file = { workspace = true }
|
||||||
schemars = { workspace = true, optional = true }
|
schemars = { workspace = true, optional = true }
|
||||||
|
|
@ -36,7 +38,6 @@ toml = { workspace = true }
|
||||||
toml_edit = { workspace = true }
|
toml_edit = { workspace = true }
|
||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
url = { workspace = true }
|
url = { workspace = true }
|
||||||
itertools = { workspace = true }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
//! Then lowers them into a dependency specification.
|
//! Then lowers them into a dependency specification.
|
||||||
|
|
||||||
use glob::Pattern;
|
use glob::Pattern;
|
||||||
|
use owo_colors::OwoColorize;
|
||||||
use serde::{de::IntoDeserializer, Deserialize, Serialize};
|
use serde::{de::IntoDeserializer, Deserialize, Serialize};
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
@ -16,6 +17,7 @@ use thiserror::Error;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use pep440_rs::{Version, VersionSpecifiers};
|
use pep440_rs::{Version, VersionSpecifiers};
|
||||||
|
use pep508_rs::MarkerTree;
|
||||||
use pypi_types::{RequirementSource, SupportedEnvironments, VerbatimParsedUrl};
|
use pypi_types::{RequirementSource, SupportedEnvironments, VerbatimParsedUrl};
|
||||||
use uv_fs::{relative_to, PortablePathBuf};
|
use uv_fs::{relative_to, PortablePathBuf};
|
||||||
use uv_git::GitReference;
|
use uv_git::GitReference;
|
||||||
|
|
@ -294,17 +296,17 @@ pub struct ToolUv {
|
||||||
#[derive(Default, Debug, Clone, PartialEq, Eq)]
|
#[derive(Default, Debug, Clone, PartialEq, Eq)]
|
||||||
#[cfg_attr(test, derive(Serialize))]
|
#[cfg_attr(test, derive(Serialize))]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
pub struct ToolUvSources(BTreeMap<PackageName, Source>);
|
pub struct ToolUvSources(BTreeMap<PackageName, Sources>);
|
||||||
|
|
||||||
impl ToolUvSources {
|
impl ToolUvSources {
|
||||||
/// Returns the underlying `BTreeMap` of package names to sources.
|
/// Returns the underlying `BTreeMap` of package names to sources.
|
||||||
pub fn inner(&self) -> &BTreeMap<PackageName, Source> {
|
pub fn inner(&self) -> &BTreeMap<PackageName, Sources> {
|
||||||
&self.0
|
&self.0
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert the [`ToolUvSources`] into its inner `BTreeMap`.
|
/// Convert the [`ToolUvSources`] into its inner `BTreeMap`.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn into_inner(self) -> BTreeMap<PackageName, Source> {
|
pub fn into_inner(self) -> BTreeMap<PackageName, Sources> {
|
||||||
self.0
|
self.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -329,7 +331,7 @@ impl<'de> serde::de::Deserialize<'de> for ToolUvSources {
|
||||||
M: serde::de::MapAccess<'de>,
|
M: serde::de::MapAccess<'de>,
|
||||||
{
|
{
|
||||||
let mut sources = BTreeMap::new();
|
let mut sources = BTreeMap::new();
|
||||||
while let Some((key, value)) = access.next_entry::<PackageName, Source>()? {
|
while let Some((key, value)) = access.next_entry::<PackageName, Sources>()? {
|
||||||
match sources.entry(key) {
|
match sources.entry(key) {
|
||||||
std::collections::btree_map::Entry::Occupied(entry) => {
|
std::collections::btree_map::Entry::Occupied(entry) => {
|
||||||
return Err(serde::de::Error::custom(format!(
|
return Err(serde::de::Error::custom(format!(
|
||||||
|
|
@ -407,6 +409,105 @@ impl Deref for SerdePattern {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||||
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
|
#[serde(rename_all = "kebab-case", try_from = "SourcesWire")]
|
||||||
|
pub struct Sources(#[cfg_attr(feature = "schemars", schemars(with = "SourcesWire"))] Vec<Source>);
|
||||||
|
|
||||||
|
impl Sources {
|
||||||
|
/// Return an [`Iterator`] over the sources.
|
||||||
|
///
|
||||||
|
/// If the iterator contains multiple entries, they will always use disjoint markers.
|
||||||
|
///
|
||||||
|
/// The iterator will contain at most one registry source.
|
||||||
|
pub fn iter(&self) -> impl Iterator<Item = &Source> {
|
||||||
|
self.0.iter()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `true` if the sources list is empty.
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.0.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the number of sources in the list.
|
||||||
|
pub fn len(&self) -> usize {
|
||||||
|
self.0.len()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoIterator for Sources {
|
||||||
|
type Item = Source;
|
||||||
|
type IntoIter = std::vec::IntoIter<Source>;
|
||||||
|
|
||||||
|
fn into_iter(self) -> Self::IntoIter {
|
||||||
|
self.0.into_iter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "kebab-case", untagged)]
|
||||||
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
|
#[allow(clippy::large_enum_variant)]
|
||||||
|
enum SourcesWire {
|
||||||
|
One(Source),
|
||||||
|
Many(Vec<Source>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<SourcesWire> for Sources {
|
||||||
|
type Error = SourceError;
|
||||||
|
|
||||||
|
fn try_from(wire: SourcesWire) -> Result<Self, Self::Error> {
|
||||||
|
match wire {
|
||||||
|
SourcesWire::One(source) => Ok(Self(vec![source])),
|
||||||
|
SourcesWire::Many(sources) => {
|
||||||
|
// Ensure that the markers are disjoint.
|
||||||
|
for (lhs, rhs) in sources
|
||||||
|
.iter()
|
||||||
|
.map(Source::marker)
|
||||||
|
.zip(sources.iter().skip(1).map(Source::marker))
|
||||||
|
{
|
||||||
|
if !lhs.is_disjoint(&rhs) {
|
||||||
|
let mut hint = lhs.negate();
|
||||||
|
hint.and(rhs.clone());
|
||||||
|
|
||||||
|
let lhs = lhs
|
||||||
|
.contents()
|
||||||
|
.map(|contents| contents.to_string())
|
||||||
|
.unwrap_or_else(|| "true".to_string());
|
||||||
|
let rhs = rhs
|
||||||
|
.contents()
|
||||||
|
.map(|contents| contents.to_string())
|
||||||
|
.unwrap_or_else(|| "true".to_string());
|
||||||
|
let hint = hint
|
||||||
|
.contents()
|
||||||
|
.map(|contents| contents.to_string())
|
||||||
|
.unwrap_or_else(|| "true".to_string());
|
||||||
|
|
||||||
|
return Err(SourceError::OverlappingMarkers(lhs, rhs, hint));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure that there is at least one source.
|
||||||
|
if sources.is_empty() {
|
||||||
|
return Err(SourceError::EmptySources);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure that there is at most one registry source.
|
||||||
|
if sources
|
||||||
|
.iter()
|
||||||
|
.filter(|source| matches!(source, Source::Registry { .. }))
|
||||||
|
.nth(1)
|
||||||
|
.is_some()
|
||||||
|
{
|
||||||
|
return Err(SourceError::MultipleIndexes);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self(sources))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A `tool.uv.sources` value.
|
/// A `tool.uv.sources` value.
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
|
|
@ -427,6 +528,12 @@ pub enum Source {
|
||||||
rev: Option<String>,
|
rev: Option<String>,
|
||||||
tag: Option<String>,
|
tag: Option<String>,
|
||||||
branch: Option<String>,
|
branch: Option<String>,
|
||||||
|
#[serde(
|
||||||
|
skip_serializing_if = "pep508_rs::marker::ser::is_empty",
|
||||||
|
serialize_with = "pep508_rs::marker::ser::serialize",
|
||||||
|
default
|
||||||
|
)]
|
||||||
|
marker: MarkerTree,
|
||||||
},
|
},
|
||||||
/// A remote `http://` or `https://` URL, either a wheel (`.whl`) or a source distribution
|
/// A remote `http://` or `https://` URL, either a wheel (`.whl`) or a source distribution
|
||||||
/// (`.zip`, `.tar.gz`).
|
/// (`.zip`, `.tar.gz`).
|
||||||
|
|
@ -440,6 +547,12 @@ pub enum Source {
|
||||||
/// For source distributions, the path to the directory with the `pyproject.toml`, if it's
|
/// For source distributions, the path to the directory with the `pyproject.toml`, if it's
|
||||||
/// not in the archive root.
|
/// not in the archive root.
|
||||||
subdirectory: Option<PortablePathBuf>,
|
subdirectory: Option<PortablePathBuf>,
|
||||||
|
#[serde(
|
||||||
|
skip_serializing_if = "pep508_rs::marker::ser::is_empty",
|
||||||
|
serialize_with = "pep508_rs::marker::ser::serialize",
|
||||||
|
default
|
||||||
|
)]
|
||||||
|
marker: MarkerTree,
|
||||||
},
|
},
|
||||||
/// The path to a dependency, either a wheel (a `.whl` file), source distribution (a `.zip` or
|
/// The path to a dependency, either a wheel (a `.whl` file), source distribution (a `.zip` or
|
||||||
/// `.tar.gz` file), or source tree (i.e., a directory containing a `pyproject.toml` or
|
/// `.tar.gz` file), or source tree (i.e., a directory containing a `pyproject.toml` or
|
||||||
|
|
@ -448,17 +561,35 @@ pub enum Source {
|
||||||
path: PortablePathBuf,
|
path: PortablePathBuf,
|
||||||
/// `false` by default.
|
/// `false` by default.
|
||||||
editable: Option<bool>,
|
editable: Option<bool>,
|
||||||
|
#[serde(
|
||||||
|
skip_serializing_if = "pep508_rs::marker::ser::is_empty",
|
||||||
|
serialize_with = "pep508_rs::marker::ser::serialize",
|
||||||
|
default
|
||||||
|
)]
|
||||||
|
marker: MarkerTree,
|
||||||
},
|
},
|
||||||
/// A dependency pinned to a specific index, e.g., `torch` after setting `torch` to `https://download.pytorch.org/whl/cu118`.
|
/// A dependency pinned to a specific index, e.g., `torch` after setting `torch` to `https://download.pytorch.org/whl/cu118`.
|
||||||
Registry {
|
Registry {
|
||||||
// TODO(konstin): The string is more-or-less a placeholder
|
// TODO(konstin): The string is more-or-less a placeholder
|
||||||
index: String,
|
index: String,
|
||||||
|
#[serde(
|
||||||
|
skip_serializing_if = "pep508_rs::marker::ser::is_empty",
|
||||||
|
serialize_with = "pep508_rs::marker::ser::serialize",
|
||||||
|
default
|
||||||
|
)]
|
||||||
|
marker: MarkerTree,
|
||||||
},
|
},
|
||||||
/// A dependency on another package in the workspace.
|
/// A dependency on another package in the workspace.
|
||||||
Workspace {
|
Workspace {
|
||||||
/// When set to `false`, the package will be fetched from the remote index, rather than
|
/// When set to `false`, the package will be fetched from the remote index, rather than
|
||||||
/// included as a workspace package.
|
/// included as a workspace package.
|
||||||
workspace: bool,
|
workspace: bool,
|
||||||
|
#[serde(
|
||||||
|
skip_serializing_if = "pep508_rs::marker::ser::is_empty",
|
||||||
|
serialize_with = "pep508_rs::marker::ser::serialize",
|
||||||
|
default
|
||||||
|
)]
|
||||||
|
marker: MarkerTree,
|
||||||
},
|
},
|
||||||
/// A catch-all variant used to emit precise error messages when deserializing.
|
/// A catch-all variant used to emit precise error messages when deserializing.
|
||||||
CatchAll {
|
CatchAll {
|
||||||
|
|
@ -471,6 +602,12 @@ pub enum Source {
|
||||||
path: PortablePathBuf,
|
path: PortablePathBuf,
|
||||||
index: String,
|
index: String,
|
||||||
workspace: bool,
|
workspace: bool,
|
||||||
|
#[serde(
|
||||||
|
skip_serializing_if = "pep508_rs::marker::ser::is_empty",
|
||||||
|
serialize_with = "pep508_rs::marker::ser::serialize",
|
||||||
|
default
|
||||||
|
)]
|
||||||
|
marker: MarkerTree,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -494,6 +631,12 @@ pub enum SourceError {
|
||||||
Absolute(#[from] std::io::Error),
|
Absolute(#[from] std::io::Error),
|
||||||
#[error("Path contains invalid characters: `{}`", _0.display())]
|
#[error("Path contains invalid characters: `{}`", _0.display())]
|
||||||
NonUtf8Path(PathBuf),
|
NonUtf8Path(PathBuf),
|
||||||
|
#[error("Source markers must be disjoint, but the following markers overlap: `{0}` and `{1}`.\n\n{hint}{colon} replace `{1}` with `{2}`.", hint = "hint".bold().cyan(), colon = ":".bold())]
|
||||||
|
OverlappingMarkers(String, String, String),
|
||||||
|
#[error("Must provide at least one source")]
|
||||||
|
EmptySources,
|
||||||
|
#[error("Sources can only include a single index source")]
|
||||||
|
MultipleIndexes,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Source {
|
impl Source {
|
||||||
|
|
@ -524,7 +667,10 @@ impl Source {
|
||||||
if workspace {
|
if workspace {
|
||||||
return match source {
|
return match source {
|
||||||
RequirementSource::Registry { .. } | RequirementSource::Directory { .. } => {
|
RequirementSource::Registry { .. } | RequirementSource::Directory { .. } => {
|
||||||
Ok(Some(Source::Workspace { workspace: true }))
|
Ok(Some(Source::Workspace {
|
||||||
|
workspace: true,
|
||||||
|
marker: MarkerTree::TRUE,
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
RequirementSource::Url { .. } => {
|
RequirementSource::Url { .. } => {
|
||||||
Err(SourceError::WorkspacePackageUrl(name.to_string()))
|
Err(SourceError::WorkspacePackageUrl(name.to_string()))
|
||||||
|
|
@ -548,12 +694,14 @@ impl Source {
|
||||||
.or_else(|_| std::path::absolute(&install_path))
|
.or_else(|_| std::path::absolute(&install_path))
|
||||||
.map_err(SourceError::Absolute)?,
|
.map_err(SourceError::Absolute)?,
|
||||||
),
|
),
|
||||||
|
marker: MarkerTree::TRUE,
|
||||||
},
|
},
|
||||||
RequirementSource::Url {
|
RequirementSource::Url {
|
||||||
subdirectory, url, ..
|
subdirectory, url, ..
|
||||||
} => Source::Url {
|
} => Source::Url {
|
||||||
url: url.to_url(),
|
url: url.to_url(),
|
||||||
subdirectory: subdirectory.map(PortablePathBuf::from),
|
subdirectory: subdirectory.map(PortablePathBuf::from),
|
||||||
|
marker: MarkerTree::TRUE,
|
||||||
},
|
},
|
||||||
RequirementSource::Git {
|
RequirementSource::Git {
|
||||||
repository,
|
repository,
|
||||||
|
|
@ -578,6 +726,7 @@ impl Source {
|
||||||
branch,
|
branch,
|
||||||
git: repository,
|
git: repository,
|
||||||
subdirectory: subdirectory.map(PortablePathBuf::from),
|
subdirectory: subdirectory.map(PortablePathBuf::from),
|
||||||
|
marker: MarkerTree::TRUE,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Source::Git {
|
Source::Git {
|
||||||
|
|
@ -586,6 +735,7 @@ impl Source {
|
||||||
branch,
|
branch,
|
||||||
git: repository,
|
git: repository,
|
||||||
subdirectory: subdirectory.map(PortablePathBuf::from),
|
subdirectory: subdirectory.map(PortablePathBuf::from),
|
||||||
|
marker: MarkerTree::TRUE,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -593,6 +743,18 @@ impl Source {
|
||||||
|
|
||||||
Ok(Some(source))
|
Ok(Some(source))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return the [`MarkerTree`] for the source.
|
||||||
|
pub fn marker(&self) -> MarkerTree {
|
||||||
|
match self {
|
||||||
|
Source::Git { marker, .. } => marker.clone(),
|
||||||
|
Source::Url { marker, .. } => marker.clone(),
|
||||||
|
Source::Path { marker, .. } => marker.clone(),
|
||||||
|
Source::Registry { marker, .. } => marker.clone(),
|
||||||
|
Source::Workspace { marker, .. } => marker.clone(),
|
||||||
|
Source::CatchAll { marker, .. } => marker.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The type of a dependency in a `pyproject.toml`.
|
/// The type of a dependency in a `pyproject.toml`.
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ use uv_normalize::{GroupName, PackageName, DEV_DEPENDENCIES};
|
||||||
use uv_warnings::{warn_user, warn_user_once};
|
use uv_warnings::{warn_user, warn_user_once};
|
||||||
|
|
||||||
use crate::pyproject::{
|
use crate::pyproject::{
|
||||||
Project, PyProjectToml, PyprojectTomlError, Source, ToolUvSources, ToolUvWorkspace,
|
Project, PyProjectToml, PyprojectTomlError, Source, Sources, ToolUvSources, ToolUvWorkspace,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(thiserror::Error, Debug)]
|
#[derive(thiserror::Error, Debug)]
|
||||||
|
|
@ -78,7 +78,7 @@ pub struct Workspace {
|
||||||
/// The sources table from the workspace `pyproject.toml`.
|
/// The sources table from the workspace `pyproject.toml`.
|
||||||
///
|
///
|
||||||
/// This table is overridden by the project sources.
|
/// This table is overridden by the project sources.
|
||||||
sources: BTreeMap<PackageName, Source>,
|
sources: BTreeMap<PackageName, Sources>,
|
||||||
/// The `pyproject.toml` of the workspace root.
|
/// The `pyproject.toml` of the workspace root.
|
||||||
pyproject_toml: PyProjectToml,
|
pyproject_toml: PyProjectToml,
|
||||||
}
|
}
|
||||||
|
|
@ -517,7 +517,7 @@ impl Workspace {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The sources table from the workspace `pyproject.toml`.
|
/// The sources table from the workspace `pyproject.toml`.
|
||||||
pub fn sources(&self) -> &BTreeMap<PackageName, Source> {
|
pub fn sources(&self) -> &BTreeMap<PackageName, Sources> {
|
||||||
&self.sources
|
&self.sources
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -531,7 +531,7 @@ impl Workspace {
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|uv| uv.sources.as_ref())
|
.and_then(|uv| uv.sources.as_ref())
|
||||||
.map(ToolUvSources::inner)
|
.map(ToolUvSources::inner)
|
||||||
.map(|sources| sources.values())
|
.map(|sources| sources.values().flat_map(Sources::iter))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.flatten()
|
.flatten()
|
||||||
|
|
@ -1755,9 +1755,11 @@ mod tests {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sources": {
|
"sources": {
|
||||||
"bird-feeder": {
|
"bird-feeder": [
|
||||||
"workspace": true
|
{
|
||||||
}
|
"workspace": true
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"pyproject_toml": {
|
"pyproject_toml": {
|
||||||
"project": {
|
"project": {
|
||||||
|
|
@ -1773,9 +1775,11 @@ mod tests {
|
||||||
"tool": {
|
"tool": {
|
||||||
"uv": {
|
"uv": {
|
||||||
"sources": {
|
"sources": {
|
||||||
"bird-feeder": {
|
"bird-feeder": [
|
||||||
"workspace": true
|
{
|
||||||
}
|
"workspace": true
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"workspace": {
|
"workspace": {
|
||||||
"members": [
|
"members": [
|
||||||
|
|
|
||||||
|
|
@ -453,6 +453,7 @@ pub(crate) async fn add(
|
||||||
rev,
|
rev,
|
||||||
tag,
|
tag,
|
||||||
branch,
|
branch,
|
||||||
|
marker,
|
||||||
}) => {
|
}) => {
|
||||||
let credentials = Credentials::from_url(&git);
|
let credentials = Credentials::from_url(&git);
|
||||||
if let Some(credentials) = credentials {
|
if let Some(credentials) = credentials {
|
||||||
|
|
@ -468,6 +469,7 @@ pub(crate) async fn add(
|
||||||
rev,
|
rev,
|
||||||
tag,
|
tag,
|
||||||
branch,
|
branch,
|
||||||
|
marker,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
_ => source,
|
_ => source,
|
||||||
|
|
|
||||||
|
|
@ -293,15 +293,15 @@ async fn do_lock(
|
||||||
let lhs = lhs
|
let lhs = lhs
|
||||||
.contents()
|
.contents()
|
||||||
.map(|contents| contents.to_string())
|
.map(|contents| contents.to_string())
|
||||||
.unwrap_or("true".to_string());
|
.unwrap_or_else(|| "true".to_string());
|
||||||
let rhs = rhs
|
let rhs = rhs
|
||||||
.contents()
|
.contents()
|
||||||
.map(|contents| contents.to_string())
|
.map(|contents| contents.to_string())
|
||||||
.unwrap_or("true".to_string());
|
.unwrap_or_else(|| "true".to_string());
|
||||||
let hint = hint
|
let hint = hint
|
||||||
.contents()
|
.contents()
|
||||||
.map(|contents| contents.to_string())
|
.map(|contents| contents.to_string())
|
||||||
.unwrap_or("true".to_string());
|
.unwrap_or_else(|| "true".to_string());
|
||||||
|
|
||||||
return Err(ProjectError::OverlappingMarkers(lhs, rhs, hint));
|
return Err(ProjectError::OverlappingMarkers(lhs, rhs, hint));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -196,13 +196,13 @@ pub(crate) async fn run(
|
||||||
|
|
||||||
let requirements = dependencies
|
let requirements = dependencies
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|requirement| {
|
.flat_map(|requirement| {
|
||||||
LoweredRequirement::from_non_workspace_requirement(
|
LoweredRequirement::from_non_workspace_requirement(
|
||||||
requirement,
|
requirement,
|
||||||
script_dir,
|
script_dir,
|
||||||
script_sources,
|
script_sources,
|
||||||
)
|
)
|
||||||
.map(LoweredRequirement::into_inner)
|
.map_ok(uv_distribution::LoweredRequirement::into_inner)
|
||||||
})
|
})
|
||||||
.collect::<Result<_, _>>()?;
|
.collect::<Result<_, _>>()?;
|
||||||
let spec = RequirementsSpecification::from_requirements(requirements);
|
let spec = RequirementsSpecification::from_requirements(requirements);
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ use uv_python::{PythonDownloads, PythonEnvironment, PythonPreference, PythonRequ
|
||||||
use uv_resolver::{FlatIndex, Lock};
|
use uv_resolver::{FlatIndex, Lock};
|
||||||
use uv_types::{BuildIsolation, HashStrategy};
|
use uv_types::{BuildIsolation, HashStrategy};
|
||||||
use uv_warnings::warn_user;
|
use uv_warnings::warn_user;
|
||||||
use uv_workspace::pyproject::{Source, ToolUvSources};
|
use uv_workspace::pyproject::{Source, Sources, ToolUvSources};
|
||||||
use uv_workspace::{DiscoveryOptions, InstallTarget, MemberDiscovery, VirtualProject, Workspace};
|
use uv_workspace::{DiscoveryOptions, InstallTarget, MemberDiscovery, VirtualProject, Workspace};
|
||||||
|
|
||||||
/// Sync the project environment.
|
/// Sync the project environment.
|
||||||
|
|
@ -425,7 +425,7 @@ fn store_credentials_from_workspace(workspace: &Workspace) {
|
||||||
.and_then(|uv| uv.sources.as_ref())
|
.and_then(|uv| uv.sources.as_ref())
|
||||||
.map(ToolUvSources::inner)
|
.map(ToolUvSources::inner)
|
||||||
.iter()
|
.iter()
|
||||||
.flat_map(|sources| sources.values())
|
.flat_map(|sources| sources.values().flat_map(Sources::iter))
|
||||||
{
|
{
|
||||||
match source {
|
match source {
|
||||||
Source::Git { git, .. } => {
|
Source::Git { git, .. } => {
|
||||||
|
|
|
||||||
|
|
@ -13323,3 +13323,468 @@ fn lock_change_requires_python() -> Result<()> {
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn lock_multiple_sources() -> 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.1.0"
|
||||||
|
requires-python = ">=3.12"
|
||||||
|
dependencies = ["iniconfig"]
|
||||||
|
|
||||||
|
[tool.uv.sources]
|
||||||
|
iniconfig = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", marker = "sys_platform != 'win32'" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", marker = "sys_platform == 'win32'" },
|
||||||
|
]
|
||||||
|
"#,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
uv_snapshot!(context.filters(), context.lock(), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
Resolved 3 packages in [TIME]
|
||||||
|
"###);
|
||||||
|
|
||||||
|
let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock")).unwrap();
|
||||||
|
|
||||||
|
insta::with_settings!({
|
||||||
|
filters => context.filters(),
|
||||||
|
}, {
|
||||||
|
assert_snapshot!(
|
||||||
|
lock, @r###"
|
||||||
|
version = 1
|
||||||
|
requires-python = ">=3.12"
|
||||||
|
resolution-markers = [
|
||||||
|
"sys_platform != 'win32'",
|
||||||
|
"sys_platform == 'win32'",
|
||||||
|
]
|
||||||
|
|
||||||
|
[options]
|
||||||
|
exclude-newer = "2024-03-25T00:00:00Z"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iniconfig"
|
||||||
|
version = "2.0.0"
|
||||||
|
source = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz" }
|
||||||
|
resolution-markers = [
|
||||||
|
"sys_platform == 'win32'",
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3" }
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iniconfig"
|
||||||
|
version = "2.0.0"
|
||||||
|
source = { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl" }
|
||||||
|
resolution-markers = [
|
||||||
|
"sys_platform != 'win32'",
|
||||||
|
]
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "project"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = { virtual = "." }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "iniconfig", version = "2.0.0", source = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz" }, marker = "sys_platform == 'win32'" },
|
||||||
|
{ name = "iniconfig", version = "2.0.0", source = { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl" }, marker = "sys_platform != 'win32'" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.metadata]
|
||||||
|
requires-dist = [
|
||||||
|
{ name = "iniconfig", marker = "sys_platform != 'win32'", url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl" },
|
||||||
|
{ name = "iniconfig", marker = "sys_platform == 'win32'", url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz" },
|
||||||
|
]
|
||||||
|
"###
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Re-run with `--locked`.
|
||||||
|
uv_snapshot!(context.filters(), context.lock().arg("--locked"), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
Resolved 3 packages in [TIME]
|
||||||
|
"###);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn lock_multiple_sources_conflict() -> 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.1.0"
|
||||||
|
requires-python = ">=3.12"
|
||||||
|
dependencies = ["iniconfig"]
|
||||||
|
|
||||||
|
[tool.uv.sources]
|
||||||
|
iniconfig = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", marker = "sys_platform == 'win32' and python_version == '3.12'" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", marker = "sys_platform == 'win32'" },
|
||||||
|
]
|
||||||
|
"#,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
uv_snapshot!(context.filters(), context.lock(), @r###"
|
||||||
|
success: false
|
||||||
|
exit_code: 2
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
error: Failed to parse: `pyproject.toml`
|
||||||
|
Caused by: TOML parse error at line 9, column 21
|
||||||
|
|
|
||||||
|
9 | iniconfig = [
|
||||||
|
| ^
|
||||||
|
Source markers must be disjoint, but the following markers overlap: `python_full_version == '3.12.*' and sys_platform == 'win32'` and `sys_platform == 'win32'`.
|
||||||
|
|
||||||
|
hint: replace `sys_platform == 'win32'` with `python_full_version != '3.12.*' and sys_platform == 'win32'`.
|
||||||
|
|
||||||
|
"###);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Multiple `index` entries is not yet supported.
|
||||||
|
#[test]
|
||||||
|
fn lock_multiple_sources_index() -> 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.1.0"
|
||||||
|
requires-python = ">=3.12"
|
||||||
|
dependencies = ["iniconfig"]
|
||||||
|
|
||||||
|
[tool.uv.sources]
|
||||||
|
iniconfig = [
|
||||||
|
{ index = "pytorch", marker = "sys_platform != 'win32'" },
|
||||||
|
{ index = "internal", marker = "sys_platform == 'win32'" },
|
||||||
|
]
|
||||||
|
"#,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
uv_snapshot!(context.filters(), context.lock(), @r###"
|
||||||
|
success: false
|
||||||
|
exit_code: 2
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
error: Failed to parse: `pyproject.toml`
|
||||||
|
Caused by: TOML parse error at line 9, column 21
|
||||||
|
|
|
||||||
|
9 | iniconfig = [
|
||||||
|
| ^
|
||||||
|
Sources can only include a single index source
|
||||||
|
|
||||||
|
"###);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn lock_multiple_sources_non_total() -> 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.1.0"
|
||||||
|
requires-python = ">=3.12"
|
||||||
|
dependencies = ["iniconfig"]
|
||||||
|
|
||||||
|
[tool.uv.sources]
|
||||||
|
iniconfig = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", marker = "sys_platform == 'darwin'" },
|
||||||
|
]
|
||||||
|
"#,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
uv_snapshot!(context.filters(), context.lock(), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
Resolved 3 packages in [TIME]
|
||||||
|
"###);
|
||||||
|
|
||||||
|
let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock")).unwrap();
|
||||||
|
|
||||||
|
insta::with_settings!({
|
||||||
|
filters => context.filters(),
|
||||||
|
}, {
|
||||||
|
assert_snapshot!(
|
||||||
|
lock, @r###"
|
||||||
|
version = 1
|
||||||
|
requires-python = ">=3.12"
|
||||||
|
resolution-markers = [
|
||||||
|
"sys_platform == 'darwin'",
|
||||||
|
"sys_platform != 'darwin'",
|
||||||
|
]
|
||||||
|
|
||||||
|
[options]
|
||||||
|
exclude-newer = "2024-03-25T00:00:00Z"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iniconfig"
|
||||||
|
version = "2.0.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
resolution-markers = [
|
||||||
|
"sys_platform != 'darwin'",
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iniconfig"
|
||||||
|
version = "2.0.0"
|
||||||
|
source = { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl" }
|
||||||
|
resolution-markers = [
|
||||||
|
"sys_platform == 'darwin'",
|
||||||
|
]
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "project"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = { virtual = "." }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "iniconfig", version = "2.0.0", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'darwin'" },
|
||||||
|
{ name = "iniconfig", version = "2.0.0", source = { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl" }, marker = "sys_platform == 'darwin'" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.metadata]
|
||||||
|
requires-dist = [
|
||||||
|
{ name = "iniconfig", marker = "sys_platform != 'darwin'" },
|
||||||
|
{ name = "iniconfig", marker = "sys_platform == 'darwin'", url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl" },
|
||||||
|
]
|
||||||
|
"###
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Re-run with `--locked`.
|
||||||
|
uv_snapshot!(context.filters(), context.lock().arg("--locked"), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
Resolved 3 packages in [TIME]
|
||||||
|
"###);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn lock_multiple_sources_respect_marker() -> 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.1.0"
|
||||||
|
requires-python = ">=3.12"
|
||||||
|
dependencies = ["iniconfig ; platform_system == 'Windows'"]
|
||||||
|
|
||||||
|
[tool.uv.sources]
|
||||||
|
iniconfig = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", marker = "sys_platform == 'darwin'" },
|
||||||
|
]
|
||||||
|
"#,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
uv_snapshot!(context.filters(), context.lock(), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
Resolved 3 packages in [TIME]
|
||||||
|
"###);
|
||||||
|
|
||||||
|
let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock")).unwrap();
|
||||||
|
|
||||||
|
insta::with_settings!({
|
||||||
|
filters => context.filters(),
|
||||||
|
}, {
|
||||||
|
assert_snapshot!(
|
||||||
|
lock, @r###"
|
||||||
|
version = 1
|
||||||
|
requires-python = ">=3.12"
|
||||||
|
resolution-markers = [
|
||||||
|
"platform_system == 'Windows' and sys_platform == 'darwin'",
|
||||||
|
"platform_system == 'Windows' and sys_platform != 'darwin'",
|
||||||
|
"platform_system != 'Windows'",
|
||||||
|
]
|
||||||
|
|
||||||
|
[options]
|
||||||
|
exclude-newer = "2024-03-25T00:00:00Z"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iniconfig"
|
||||||
|
version = "2.0.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
resolution-markers = [
|
||||||
|
"platform_system == 'Windows' and sys_platform != 'darwin'",
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iniconfig"
|
||||||
|
version = "2.0.0"
|
||||||
|
source = { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl" }
|
||||||
|
resolution-markers = [
|
||||||
|
"platform_system == 'Windows' and sys_platform == 'darwin'",
|
||||||
|
]
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "project"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = { virtual = "." }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "iniconfig", version = "2.0.0", source = { registry = "https://pypi.org/simple" }, marker = "platform_system == 'Windows' and sys_platform != 'darwin'" },
|
||||||
|
{ name = "iniconfig", version = "2.0.0", source = { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl" }, marker = "platform_system == 'Windows' and sys_platform == 'darwin'" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.metadata]
|
||||||
|
requires-dist = [
|
||||||
|
{ name = "iniconfig", marker = "platform_system == 'Windows' and sys_platform != 'darwin'" },
|
||||||
|
{ name = "iniconfig", marker = "platform_system == 'Windows' and sys_platform == 'darwin'", url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl" },
|
||||||
|
]
|
||||||
|
"###
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Re-run with `--locked`.
|
||||||
|
uv_snapshot!(context.filters(), context.lock().arg("--locked"), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
Resolved 3 packages in [TIME]
|
||||||
|
"###);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn lock_multiple_sources_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.1.0"
|
||||||
|
requires-python = ">=3.12"
|
||||||
|
dependencies = ["iniconfig"]
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
cpu = []
|
||||||
|
|
||||||
|
[tool.uv.sources]
|
||||||
|
iniconfig = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", marker = "extra == 'cpu'" },
|
||||||
|
]
|
||||||
|
"#,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
uv_snapshot!(context.filters(), context.lock(), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
Resolved 2 packages in [TIME]
|
||||||
|
"###);
|
||||||
|
|
||||||
|
let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock")).unwrap();
|
||||||
|
|
||||||
|
insta::with_settings!({
|
||||||
|
filters => context.filters(),
|
||||||
|
}, {
|
||||||
|
assert_snapshot!(
|
||||||
|
lock, @r###"
|
||||||
|
version = 1
|
||||||
|
requires-python = ">=3.12"
|
||||||
|
|
||||||
|
[options]
|
||||||
|
exclude-newer = "2024-03-25T00:00:00Z"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iniconfig"
|
||||||
|
version = "2.0.0"
|
||||||
|
source = { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "project"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = { virtual = "." }
|
||||||
|
|
||||||
|
[package.optional-dependencies]
|
||||||
|
cpu = [
|
||||||
|
{ name = "iniconfig" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.metadata]
|
||||||
|
requires-dist = [
|
||||||
|
{ name = "iniconfig", marker = "extra == 'cpu'", url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl" },
|
||||||
|
{ name = "iniconfig", marker = "extra != 'cpu'" },
|
||||||
|
]
|
||||||
|
"###
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Re-run with `--locked`.
|
||||||
|
uv_snapshot!(context.filters(), context.lock().arg("--locked"), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
Resolved 2 packages in [TIME]
|
||||||
|
"###);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -223,6 +223,46 @@ members = [
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Platform-specific sources
|
||||||
|
|
||||||
|
You can limit a source to a given platform or Python version by providing
|
||||||
|
[PEP 508](https://peps.python.org/pep-0508/#environment-markers)-compatible environment markers for
|
||||||
|
the source.
|
||||||
|
|
||||||
|
For example, to pull `httpx` from GitHub, but only on macOS, use the following:
|
||||||
|
|
||||||
|
```toml title="pyproject.toml"
|
||||||
|
[project]
|
||||||
|
dependencies = [
|
||||||
|
"httpx",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.uv.sources]
|
||||||
|
httpx = { git = "https://github.com/encode/httpx", tag = "0.27.2", marker = "sys_platform == 'darwin'" }
|
||||||
|
```
|
||||||
|
|
||||||
|
By specifying the marker on the source, uv will still include `httpx` on all platforms, but will
|
||||||
|
download the source from GitHub on macOS, and fall back to PyPI on all other platforms.
|
||||||
|
|
||||||
|
### Multiple sources
|
||||||
|
|
||||||
|
You can specify multiple sources for a single dependency by providing a list of sources,
|
||||||
|
disambiguated by [PEP 508](https://peps.python.org/pep-0508/#environment-markers)-compatible
|
||||||
|
environment markers. For example, to pull in different `httpx` commits on macOS vs. Linux:
|
||||||
|
|
||||||
|
```toml title="pyproject.toml"
|
||||||
|
[project]
|
||||||
|
dependencies = [
|
||||||
|
"httpx",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.uv.sources]
|
||||||
|
httpx = [
|
||||||
|
{ git = "https://github.com/encode/httpx", tag = "0.27.2", marker = "sys_platform == 'darwin'" },
|
||||||
|
{ git = "https://github.com/encode/httpx", tag = "0.24.1", marker = "sys_platform == 'linux'" },
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
## Optional dependencies
|
## Optional dependencies
|
||||||
|
|
||||||
It is common for projects that are published as libraries to make some features optional to reduce
|
It is common for projects that are published as libraries to make some features optional to reduce
|
||||||
|
|
|
||||||
|
|
@ -625,6 +625,10 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"MarkerTree": {
|
||||||
|
"description": "A PEP 508-compliant marker expression, e.g., `sys_platform == 'Darwin'`",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"PackageName": {
|
"PackageName": {
|
||||||
"description": "The normalized name of a package.\n\nConverts the name to lowercase and collapses runs of `-`, `_`, and `.` down to a single `-`. For example, `---`, `.`, and `__` are all converted to a single `-`.\n\nSee: <https://packaging.python.org/en/latest/specifications/name-normalization/>",
|
"description": "The normalized name of a package.\n\nConverts the name to lowercase and collapses runs of `-`, `_`, and `.` down to a single `-`. For example, `---`, `.`, and `__` are all converted to a single `-`.\n\nSee: <https://packaging.python.org/en/latest/specifications/name-normalization/>",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
|
@ -1240,6 +1244,9 @@
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"format": "uri"
|
"format": "uri"
|
||||||
},
|
},
|
||||||
|
"marker": {
|
||||||
|
"$ref": "#/definitions/MarkerTree"
|
||||||
|
},
|
||||||
"rev": {
|
"rev": {
|
||||||
"type": [
|
"type": [
|
||||||
"string",
|
"string",
|
||||||
|
|
@ -1273,6 +1280,9 @@
|
||||||
"url"
|
"url"
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"marker": {
|
||||||
|
"$ref": "#/definitions/MarkerTree"
|
||||||
|
},
|
||||||
"subdirectory": {
|
"subdirectory": {
|
||||||
"description": "For source distributions, the path to the directory with the `pyproject.toml`, if it's not in the archive root.",
|
"description": "For source distributions, the path to the directory with the `pyproject.toml`, if it's not in the archive root.",
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
|
|
@ -1305,6 +1315,9 @@
|
||||||
"null"
|
"null"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"marker": {
|
||||||
|
"$ref": "#/definitions/MarkerTree"
|
||||||
|
},
|
||||||
"path": {
|
"path": {
|
||||||
"$ref": "#/definitions/String"
|
"$ref": "#/definitions/String"
|
||||||
}
|
}
|
||||||
|
|
@ -1320,6 +1333,9 @@
|
||||||
"properties": {
|
"properties": {
|
||||||
"index": {
|
"index": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
"marker": {
|
||||||
|
"$ref": "#/definitions/MarkerTree"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
|
|
@ -1331,6 +1347,9 @@
|
||||||
"workspace"
|
"workspace"
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"marker": {
|
||||||
|
"$ref": "#/definitions/MarkerTree"
|
||||||
|
},
|
||||||
"workspace": {
|
"workspace": {
|
||||||
"description": "When set to `false`, the package will be fetched from the remote index, rather than included as a workspace package.",
|
"description": "When set to `false`, the package will be fetched from the remote index, rather than included as a workspace package.",
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
|
|
@ -1361,6 +1380,9 @@
|
||||||
"index": {
|
"index": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"marker": {
|
||||||
|
"$ref": "#/definitions/MarkerTree"
|
||||||
|
},
|
||||||
"path": {
|
"path": {
|
||||||
"$ref": "#/definitions/String"
|
"$ref": "#/definitions/String"
|
||||||
},
|
},
|
||||||
|
|
@ -1397,6 +1419,22 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"Sources": {
|
||||||
|
"$ref": "#/definitions/SourcesWire"
|
||||||
|
},
|
||||||
|
"SourcesWire": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/Source"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/Source"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"StaticMetadata": {
|
"StaticMetadata": {
|
||||||
"description": "A subset of the Python Package Metadata 2.3 standard as specified in <https://packaging.python.org/specifications/core-metadata/>.",
|
"description": "A subset of the Python Package Metadata 2.3 standard as specified in <https://packaging.python.org/specifications/core-metadata/>.",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
|
@ -1565,7 +1603,7 @@
|
||||||
"ToolUvSources": {
|
"ToolUvSources": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": {
|
"additionalProperties": {
|
||||||
"$ref": "#/definitions/Source"
|
"$ref": "#/definitions/Sources"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ToolUvWorkspace": {
|
"ToolUvWorkspace": {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue