mirror of
https://github.com/astral-sh/uv.git
synced 2025-12-03 16:46:18 +00:00
Enable extra build dependencies to 'match runtime' versions (#15036)
## Summary This is an alternative to https://github.com/astral-sh/uv/pull/14944 that functions a little differently. Rather than adding separate strategies, you can instead say: ```toml [tool.uv.extra-build-dependencies] child = [{ requirement = "anyio", match-runtime = true }] ``` Which will then enforce that `anyio` uses the same version as in the lockfile.
This commit is contained in:
parent
b2c382f7c1
commit
8ef3b2eb8e
11 changed files with 566 additions and 81 deletions
|
|
@ -328,6 +328,7 @@ impl SourceBuild {
|
||||||
.and_then(|name| extra_build_requires.get(name).cloned())
|
.and_then(|name| extra_build_requires.get(name).cloned())
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
.map(Requirement::from)
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
// Create a virtual environment, or install into the shared environment if requested.
|
// Create a virtual environment, or install into the shared environment if requested.
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,24 @@
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
|
use uv_cache_key::{CacheKey, CacheKeyHasher};
|
||||||
use uv_normalize::PackageName;
|
use uv_normalize::PackageName;
|
||||||
|
|
||||||
use crate::Requirement;
|
use crate::{Name, Requirement, RequirementSource, Resolution};
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum ExtraBuildRequiresError {
|
||||||
|
#[error(
|
||||||
|
"`{0}` was declared as an extra build dependency with `match-runtime = true`, but was not found in the resolution"
|
||||||
|
)]
|
||||||
|
NotFound(PackageName),
|
||||||
|
}
|
||||||
|
|
||||||
/// Lowered extra build dependencies with source resolution applied.
|
/// Lowered extra build dependencies with source resolution applied.
|
||||||
#[derive(Debug, Clone, Default)]
|
#[derive(Debug, Clone, Default)]
|
||||||
pub struct ExtraBuildRequires(BTreeMap<PackageName, Vec<Requirement>>);
|
pub struct ExtraBuildRequires(BTreeMap<PackageName, Vec<ExtraBuildRequirement>>);
|
||||||
|
|
||||||
impl std::ops::Deref for ExtraBuildRequires {
|
impl std::ops::Deref for ExtraBuildRequires {
|
||||||
type Target = BTreeMap<PackageName, Vec<Requirement>>;
|
type Target = BTreeMap<PackageName, Vec<ExtraBuildRequirement>>;
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
fn deref(&self) -> &Self::Target {
|
||||||
&self.0
|
&self.0
|
||||||
|
|
@ -23,16 +32,78 @@ impl std::ops::DerefMut for ExtraBuildRequires {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IntoIterator for ExtraBuildRequires {
|
impl IntoIterator for ExtraBuildRequires {
|
||||||
type Item = (PackageName, Vec<Requirement>);
|
type Item = (PackageName, Vec<ExtraBuildRequirement>);
|
||||||
type IntoIter = std::collections::btree_map::IntoIter<PackageName, Vec<Requirement>>;
|
type IntoIter = std::collections::btree_map::IntoIter<PackageName, Vec<ExtraBuildRequirement>>;
|
||||||
|
|
||||||
fn into_iter(self) -> Self::IntoIter {
|
fn into_iter(self) -> Self::IntoIter {
|
||||||
self.0.into_iter()
|
self.0.into_iter()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromIterator<(PackageName, Vec<Requirement>)> for ExtraBuildRequires {
|
impl FromIterator<(PackageName, Vec<ExtraBuildRequirement>)> for ExtraBuildRequires {
|
||||||
fn from_iter<T: IntoIterator<Item = (PackageName, Vec<Requirement>)>>(iter: T) -> Self {
|
fn from_iter<T: IntoIterator<Item = (PackageName, Vec<ExtraBuildRequirement>)>>(
|
||||||
|
iter: T,
|
||||||
|
) -> Self {
|
||||||
Self(iter.into_iter().collect())
|
Self(iter.into_iter().collect())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct ExtraBuildRequirement {
|
||||||
|
/// The underlying [`Requirement`] for the build requirement.
|
||||||
|
pub requirement: Requirement,
|
||||||
|
/// Whether this build requirement should match the runtime environment.
|
||||||
|
pub match_runtime: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ExtraBuildRequirement> for Requirement {
|
||||||
|
fn from(value: ExtraBuildRequirement) -> Self {
|
||||||
|
value.requirement
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CacheKey for ExtraBuildRequirement {
|
||||||
|
fn cache_key(&self, state: &mut CacheKeyHasher) {
|
||||||
|
self.requirement.cache_key(state);
|
||||||
|
self.match_runtime.cache_key(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ExtraBuildRequires {
|
||||||
|
/// Apply runtime constraints from a resolution to the extra build requirements.
|
||||||
|
pub fn match_runtime(
|
||||||
|
self,
|
||||||
|
resolution: &Resolution,
|
||||||
|
) -> Result<ExtraBuildRequires, ExtraBuildRequiresError> {
|
||||||
|
self.into_iter()
|
||||||
|
.map(|(name, requirements)| {
|
||||||
|
let requirements = requirements
|
||||||
|
.into_iter()
|
||||||
|
.map(|requirement| match requirement {
|
||||||
|
ExtraBuildRequirement {
|
||||||
|
requirement,
|
||||||
|
match_runtime: true,
|
||||||
|
} => {
|
||||||
|
let dist = resolution
|
||||||
|
.distributions()
|
||||||
|
.find(|dist| dist.name() == &requirement.name)
|
||||||
|
.ok_or_else(|| {
|
||||||
|
ExtraBuildRequiresError::NotFound(requirement.name.clone())
|
||||||
|
})?;
|
||||||
|
let requirement = Requirement {
|
||||||
|
source: RequirementSource::from(dist),
|
||||||
|
..requirement
|
||||||
|
};
|
||||||
|
Ok::<_, ExtraBuildRequiresError>(ExtraBuildRequirement {
|
||||||
|
requirement,
|
||||||
|
match_runtime: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
requirement => Ok(requirement),
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
|
Ok::<_, ExtraBuildRequiresError>((name, requirements))
|
||||||
|
})
|
||||||
|
.collect::<Result<ExtraBuildRequires, _>>()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,8 @@ use uv_cache_info::CacheInfo;
|
||||||
use uv_cache_key::cache_digest;
|
use uv_cache_key::cache_digest;
|
||||||
use uv_configuration::{ConfigSettings, PackageConfigSettings};
|
use uv_configuration::{ConfigSettings, PackageConfigSettings};
|
||||||
use uv_distribution_types::{
|
use uv_distribution_types::{
|
||||||
DirectUrlSourceDist, DirectorySourceDist, ExtraBuildRequires, GitSourceDist, Hashed,
|
DirectUrlSourceDist, DirectorySourceDist, ExtraBuildRequirement, ExtraBuildRequires,
|
||||||
PathSourceDist, Requirement,
|
GitSourceDist, Hashed, PathSourceDist,
|
||||||
};
|
};
|
||||||
use uv_normalize::PackageName;
|
use uv_normalize::PackageName;
|
||||||
use uv_platform_tags::Tags;
|
use uv_platform_tags::Tags;
|
||||||
|
|
@ -267,8 +267,8 @@ impl<'a> BuiltWheelIndex<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Determine the extra build dependencies for the given package name.
|
/// Determine the extra build requirements for the given package name.
|
||||||
fn extra_build_requires_for(&self, name: &PackageName) -> &[Requirement] {
|
fn extra_build_requires_for(&self, name: &PackageName) -> &[ExtraBuildRequirement] {
|
||||||
self.extra_build_requires
|
self.extra_build_requires
|
||||||
.get(name)
|
.get(name)
|
||||||
.map(Vec::as_slice)
|
.map(Vec::as_slice)
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,11 @@ use std::collections::BTreeMap;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use uv_configuration::SourceStrategy;
|
use uv_configuration::SourceStrategy;
|
||||||
use uv_distribution_types::{ExtraBuildRequires, IndexLocations, Requirement};
|
use uv_distribution_types::{
|
||||||
|
ExtraBuildRequirement, ExtraBuildRequires, IndexLocations, Requirement,
|
||||||
|
};
|
||||||
use uv_normalize::PackageName;
|
use uv_normalize::PackageName;
|
||||||
use uv_workspace::pyproject::{ExtraBuildDependencies, ToolUvSources};
|
use uv_workspace::pyproject::{ExtraBuildDependencies, ExtraBuildDependency, ToolUvSources};
|
||||||
use uv_workspace::{
|
use uv_workspace::{
|
||||||
DiscoveryOptions, MemberDiscovery, ProjectWorkspace, Workspace, WorkspaceCache,
|
DiscoveryOptions, MemberDiscovery, ProjectWorkspace, Workspace, WorkspaceCache,
|
||||||
};
|
};
|
||||||
|
|
@ -248,50 +250,48 @@ impl LoweredExtraBuildDependencies {
|
||||||
// Lower each package's extra build dependencies
|
// Lower each package's extra build dependencies
|
||||||
let mut build_requires = ExtraBuildRequires::default();
|
let mut build_requires = ExtraBuildRequires::default();
|
||||||
for (package_name, requirements) in extra_build_dependencies {
|
for (package_name, requirements) in extra_build_dependencies {
|
||||||
let lowered: Vec<Requirement> = requirements
|
let lowered: Vec<ExtraBuildRequirement> = requirements
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.flat_map(|requirement| {
|
.flat_map(
|
||||||
let requirement_name = requirement.name.clone();
|
|ExtraBuildDependency {
|
||||||
let extra = requirement.marker.top_level_extra_name();
|
requirement,
|
||||||
let group = None;
|
match_runtime,
|
||||||
LoweredRequirement::from_requirement(
|
}| {
|
||||||
requirement,
|
let requirement_name = requirement.name.clone();
|
||||||
None,
|
let extra = requirement.marker.top_level_extra_name();
|
||||||
workspace.install_path(),
|
let group = None;
|
||||||
project_sources,
|
LoweredRequirement::from_requirement(
|
||||||
project_indexes,
|
requirement,
|
||||||
extra.as_deref(),
|
None,
|
||||||
group,
|
workspace.install_path(),
|
||||||
index_locations,
|
project_sources,
|
||||||
workspace,
|
project_indexes,
|
||||||
None,
|
extra.as_deref(),
|
||||||
)
|
group,
|
||||||
.map(
|
index_locations,
|
||||||
move |requirement| match requirement {
|
workspace,
|
||||||
Ok(requirement) => Ok(requirement.into_inner()),
|
None,
|
||||||
Err(err) => Err(MetadataError::LoweringError(
|
)
|
||||||
requirement_name.clone(),
|
.map(move |requirement| {
|
||||||
Box::new(err),
|
match requirement {
|
||||||
)),
|
Ok(requirement) => Ok(ExtraBuildRequirement {
|
||||||
},
|
requirement: requirement.into_inner(),
|
||||||
)
|
match_runtime,
|
||||||
})
|
}),
|
||||||
|
Err(err) => Err(MetadataError::LoweringError(
|
||||||
|
requirement_name.clone(),
|
||||||
|
Box::new(err),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
)
|
||||||
.collect::<Result<Vec<_>, _>>()?;
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
build_requires.insert(package_name, lowered);
|
build_requires.insert(package_name, lowered);
|
||||||
}
|
}
|
||||||
Ok(Self(build_requires))
|
Ok(Self(build_requires))
|
||||||
}
|
}
|
||||||
SourceStrategy::Disabled => Ok(Self(
|
SourceStrategy::Disabled => Ok(Self::from_non_lowered(extra_build_dependencies)),
|
||||||
extra_build_dependencies
|
|
||||||
.into_iter()
|
|
||||||
.map(|(name, requirements)| {
|
|
||||||
(
|
|
||||||
name,
|
|
||||||
requirements.into_iter().map(Requirement::from).collect(),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect(),
|
|
||||||
)),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -308,7 +308,20 @@ impl LoweredExtraBuildDependencies {
|
||||||
.map(|(name, requirements)| {
|
.map(|(name, requirements)| {
|
||||||
(
|
(
|
||||||
name,
|
name,
|
||||||
requirements.into_iter().map(Requirement::from).collect(),
|
requirements
|
||||||
|
.into_iter()
|
||||||
|
.map(
|
||||||
|
|ExtraBuildDependency {
|
||||||
|
requirement,
|
||||||
|
match_runtime,
|
||||||
|
}| {
|
||||||
|
ExtraBuildRequirement {
|
||||||
|
requirement: requirement.into(),
|
||||||
|
match_runtime,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
|
|
|
||||||
|
|
@ -32,8 +32,8 @@ use uv_client::{
|
||||||
use uv_configuration::{BuildKind, BuildOutput, ConfigSettings, SourceStrategy};
|
use uv_configuration::{BuildKind, BuildOutput, ConfigSettings, SourceStrategy};
|
||||||
use uv_distribution_filename::{SourceDistExtension, WheelFilename};
|
use uv_distribution_filename::{SourceDistExtension, WheelFilename};
|
||||||
use uv_distribution_types::{
|
use uv_distribution_types::{
|
||||||
BuildableSource, DirectorySourceUrl, GitSourceUrl, HashPolicy, Hashed, IndexUrl, PathSourceUrl,
|
BuildableSource, DirectorySourceUrl, ExtraBuildRequirement, GitSourceUrl, HashPolicy, Hashed,
|
||||||
Requirement, SourceDist, SourceUrl,
|
IndexUrl, PathSourceUrl, SourceDist, SourceUrl,
|
||||||
};
|
};
|
||||||
use uv_extract::hash::Hasher;
|
use uv_extract::hash::Hasher;
|
||||||
use uv_fs::{rename_with_retry, write_atomic};
|
use uv_fs::{rename_with_retry, write_atomic};
|
||||||
|
|
@ -405,7 +405,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Determine the extra build dependencies for the given package name.
|
/// Determine the extra build dependencies for the given package name.
|
||||||
fn extra_build_dependencies_for(&self, name: Option<&PackageName>) -> &[Requirement] {
|
fn extra_build_dependencies_for(&self, name: Option<&PackageName>) -> &[ExtraBuildRequirement] {
|
||||||
name.and_then(|name| {
|
name.and_then(|name| {
|
||||||
self.build_context
|
self.build_context
|
||||||
.extra_build_requires()
|
.extra_build_requires()
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ use uv_pypi_types::VerbatimParsedUrl;
|
||||||
use uv_redacted::DisplaySafeUrl;
|
use uv_redacted::DisplaySafeUrl;
|
||||||
use uv_settings::{GlobalOptions, ResolverInstallerOptions};
|
use uv_settings::{GlobalOptions, ResolverInstallerOptions};
|
||||||
use uv_warnings::warn_user;
|
use uv_warnings::warn_user;
|
||||||
use uv_workspace::pyproject::Sources;
|
use uv_workspace::pyproject::{ExtraBuildDependency, Sources};
|
||||||
|
|
||||||
static FINDER: LazyLock<Finder> = LazyLock::new(|| Finder::new(b"# /// script"));
|
static FINDER: LazyLock<Finder> = LazyLock::new(|| Finder::new(b"# /// script"));
|
||||||
|
|
||||||
|
|
@ -428,8 +428,7 @@ pub struct ToolUv {
|
||||||
pub override_dependencies: Option<Vec<uv_pep508::Requirement<VerbatimParsedUrl>>>,
|
pub override_dependencies: Option<Vec<uv_pep508::Requirement<VerbatimParsedUrl>>>,
|
||||||
pub constraint_dependencies: Option<Vec<uv_pep508::Requirement<VerbatimParsedUrl>>>,
|
pub constraint_dependencies: Option<Vec<uv_pep508::Requirement<VerbatimParsedUrl>>>,
|
||||||
pub build_constraint_dependencies: Option<Vec<uv_pep508::Requirement<VerbatimParsedUrl>>>,
|
pub build_constraint_dependencies: Option<Vec<uv_pep508::Requirement<VerbatimParsedUrl>>>,
|
||||||
pub extra_build_dependencies:
|
pub extra_build_dependencies: Option<BTreeMap<PackageName, Vec<ExtraBuildDependency>>>,
|
||||||
Option<BTreeMap<PackageName, Vec<uv_pep508::Requirement<VerbatimParsedUrl>>>>,
|
|
||||||
pub sources: Option<BTreeMap<PackageName, Sources>>,
|
pub sources: Option<BTreeMap<PackageName, Sources>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ use uv_macros::OptionsMetadata;
|
||||||
use uv_normalize::{DefaultGroups, ExtraName, GroupName, PackageName};
|
use uv_normalize::{DefaultGroups, ExtraName, GroupName, PackageName};
|
||||||
use uv_options_metadata::{OptionSet, OptionsMetadata, Visit};
|
use uv_options_metadata::{OptionSet, OptionsMetadata, Visit};
|
||||||
use uv_pep440::{Version, VersionSpecifiers};
|
use uv_pep440::{Version, VersionSpecifiers};
|
||||||
use uv_pep508::MarkerTree;
|
use uv_pep508::{MarkerTree, VersionOrUrl};
|
||||||
use uv_pypi_types::{
|
use uv_pypi_types::{
|
||||||
Conflicts, DependencyGroups, SchemaConflicts, SupportedEnvironments, VerbatimParsedUrl,
|
Conflicts, DependencyGroups, SchemaConflicts, SupportedEnvironments, VerbatimParsedUrl,
|
||||||
};
|
};
|
||||||
|
|
@ -755,14 +755,86 @@ pub struct DependencyGroupSettings {
|
||||||
pub requires_python: Option<VersionSpecifiers>,
|
pub requires_python: Option<VersionSpecifiers>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(untagged, rename_all = "kebab-case")]
|
||||||
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
|
pub enum ExtraBuildDependencyWire {
|
||||||
|
Unannotated(uv_pep508::Requirement<VerbatimParsedUrl>),
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
Annotated {
|
||||||
|
requirement: uv_pep508::Requirement<VerbatimParsedUrl>,
|
||||||
|
match_runtime: bool,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(
|
||||||
|
deny_unknown_fields,
|
||||||
|
try_from = "ExtraBuildDependencyWire",
|
||||||
|
into = "ExtraBuildDependencyWire"
|
||||||
|
)]
|
||||||
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
|
pub struct ExtraBuildDependency {
|
||||||
|
pub requirement: uv_pep508::Requirement<VerbatimParsedUrl>,
|
||||||
|
pub match_runtime: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ExtraBuildDependency> for uv_pep508::Requirement<VerbatimParsedUrl> {
|
||||||
|
fn from(value: ExtraBuildDependency) -> Self {
|
||||||
|
value.requirement
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum ExtraBuildDependencyError {
|
||||||
|
#[error("Dependencies marked with `match-runtime = true` cannot include version specifiers")]
|
||||||
|
VersionSpecifiersNotAllowed,
|
||||||
|
#[error("Dependencies marked with `match-runtime = true` cannot include URL constraints")]
|
||||||
|
UrlNotAllowed,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<ExtraBuildDependencyWire> for ExtraBuildDependency {
|
||||||
|
type Error = ExtraBuildDependencyError;
|
||||||
|
|
||||||
|
fn try_from(wire: ExtraBuildDependencyWire) -> Result<Self, ExtraBuildDependencyError> {
|
||||||
|
match wire {
|
||||||
|
ExtraBuildDependencyWire::Unannotated(requirement) => Ok(Self {
|
||||||
|
requirement,
|
||||||
|
match_runtime: false,
|
||||||
|
}),
|
||||||
|
ExtraBuildDependencyWire::Annotated {
|
||||||
|
requirement,
|
||||||
|
match_runtime,
|
||||||
|
} => match requirement.version_or_url {
|
||||||
|
None => Ok(Self {
|
||||||
|
requirement,
|
||||||
|
match_runtime,
|
||||||
|
}),
|
||||||
|
// If `match-runtime = true`, reject additional constraints.
|
||||||
|
Some(VersionOrUrl::VersionSpecifier(..)) => {
|
||||||
|
Err(ExtraBuildDependencyError::VersionSpecifiersNotAllowed)
|
||||||
|
}
|
||||||
|
Some(VersionOrUrl::Url(..)) => Err(ExtraBuildDependencyError::UrlNotAllowed),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ExtraBuildDependency> for ExtraBuildDependencyWire {
|
||||||
|
fn from(item: ExtraBuildDependency) -> ExtraBuildDependencyWire {
|
||||||
|
ExtraBuildDependencyWire::Annotated {
|
||||||
|
requirement: item.requirement,
|
||||||
|
match_runtime: item.match_runtime,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize)]
|
#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize)]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
pub struct ExtraBuildDependencies(
|
pub struct ExtraBuildDependencies(BTreeMap<PackageName, Vec<ExtraBuildDependency>>);
|
||||||
BTreeMap<PackageName, Vec<uv_pep508::Requirement<VerbatimParsedUrl>>>,
|
|
||||||
);
|
|
||||||
|
|
||||||
impl std::ops::Deref for ExtraBuildDependencies {
|
impl std::ops::Deref for ExtraBuildDependencies {
|
||||||
type Target = BTreeMap<PackageName, Vec<uv_pep508::Requirement<VerbatimParsedUrl>>>;
|
type Target = BTreeMap<PackageName, Vec<ExtraBuildDependency>>;
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
fn deref(&self) -> &Self::Target {
|
||||||
&self.0
|
&self.0
|
||||||
|
|
@ -776,17 +848,22 @@ impl std::ops::DerefMut for ExtraBuildDependencies {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IntoIterator for ExtraBuildDependencies {
|
impl IntoIterator for ExtraBuildDependencies {
|
||||||
type Item = (PackageName, Vec<uv_pep508::Requirement<VerbatimParsedUrl>>);
|
type Item = (PackageName, Vec<ExtraBuildDependency>);
|
||||||
type IntoIter = std::collections::btree_map::IntoIter<
|
type IntoIter = std::collections::btree_map::IntoIter<PackageName, Vec<ExtraBuildDependency>>;
|
||||||
PackageName,
|
|
||||||
Vec<uv_pep508::Requirement<VerbatimParsedUrl>>,
|
|
||||||
>;
|
|
||||||
|
|
||||||
fn into_iter(self) -> Self::IntoIter {
|
fn into_iter(self) -> Self::IntoIter {
|
||||||
self.0.into_iter()
|
self.0.into_iter()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl FromIterator<(PackageName, Vec<ExtraBuildDependency>)> for ExtraBuildDependencies {
|
||||||
|
fn from_iter<T: IntoIterator<Item = (PackageName, Vec<ExtraBuildDependency>)>>(
|
||||||
|
iter: T,
|
||||||
|
) -> Self {
|
||||||
|
Self(iter.into_iter().collect())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Ensure that all keys in the TOML table are unique.
|
/// Ensure that all keys in the TOML table are unique.
|
||||||
impl<'de> serde::de::Deserialize<'de> for ExtraBuildDependencies {
|
impl<'de> serde::de::Deserialize<'de> for ExtraBuildDependencies {
|
||||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,8 @@ use uv_configuration::{
|
||||||
use uv_dispatch::{BuildDispatch, SharedState};
|
use uv_dispatch::{BuildDispatch, SharedState};
|
||||||
use uv_distribution::{DistributionDatabase, LoweredExtraBuildDependencies, LoweredRequirement};
|
use uv_distribution::{DistributionDatabase, LoweredExtraBuildDependencies, LoweredRequirement};
|
||||||
use uv_distribution_types::{
|
use uv_distribution_types::{
|
||||||
ExtraBuildRequires, Index, Requirement, RequiresPython, Resolution, UnresolvedRequirement,
|
ExtraBuildRequirement, ExtraBuildRequires, Index, Requirement, RequiresPython, Resolution,
|
||||||
UnresolvedRequirementSpecification,
|
UnresolvedRequirement, UnresolvedRequirementSpecification,
|
||||||
};
|
};
|
||||||
use uv_fs::{CWD, LockedFile, Simplified};
|
use uv_fs::{CWD, LockedFile, Simplified};
|
||||||
use uv_git::ResolvedRepositoryReference;
|
use uv_git::ResolvedRepositoryReference;
|
||||||
|
|
@ -46,6 +46,7 @@ use uv_types::{BuildIsolation, EmptyInstalledPackages, HashStrategy};
|
||||||
use uv_virtualenv::remove_virtualenv;
|
use uv_virtualenv::remove_virtualenv;
|
||||||
use uv_warnings::{warn_user, warn_user_once};
|
use uv_warnings::{warn_user, warn_user_once};
|
||||||
use uv_workspace::dependency_groups::DependencyGroupError;
|
use uv_workspace::dependency_groups::DependencyGroupError;
|
||||||
|
use uv_workspace::pyproject::ExtraBuildDependency;
|
||||||
use uv_workspace::pyproject::PyProjectToml;
|
use uv_workspace::pyproject::PyProjectToml;
|
||||||
use uv_workspace::{RequiresPythonSources, Workspace, WorkspaceCache};
|
use uv_workspace::{RequiresPythonSources, Workspace, WorkspaceCache};
|
||||||
|
|
||||||
|
|
@ -252,6 +253,9 @@ pub(crate) enum ProjectError {
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
PyprojectMut(#[from] uv_workspace::pyproject_mut::Error),
|
PyprojectMut(#[from] uv_workspace::pyproject_mut::Error),
|
||||||
|
|
||||||
|
#[error(transparent)]
|
||||||
|
ExtraBuildRequires(#[from] uv_distribution_types::ExtraBuildRequiresError),
|
||||||
|
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Fmt(#[from] std::fmt::Error),
|
Fmt(#[from] std::fmt::Error),
|
||||||
|
|
||||||
|
|
@ -2647,16 +2651,24 @@ pub(crate) fn script_extra_build_requires(
|
||||||
let lowered_requirements: Vec<_> = requirements
|
let lowered_requirements: Vec<_> = requirements
|
||||||
.iter()
|
.iter()
|
||||||
.cloned()
|
.cloned()
|
||||||
.flat_map(|requirement| {
|
.flat_map(
|
||||||
LoweredRequirement::from_non_workspace_requirement(
|
|ExtraBuildDependency {
|
||||||
requirement,
|
requirement,
|
||||||
script_dir.as_ref(),
|
match_runtime,
|
||||||
script_sources,
|
}| {
|
||||||
script_indexes,
|
LoweredRequirement::from_non_workspace_requirement(
|
||||||
&settings.index_locations,
|
requirement,
|
||||||
)
|
script_dir.as_ref(),
|
||||||
.map_ok(uv_distribution::LoweredRequirement::into_inner)
|
script_sources,
|
||||||
})
|
script_indexes,
|
||||||
|
&settings.index_locations,
|
||||||
|
)
|
||||||
|
.map_ok(move |requirement| ExtraBuildRequirement {
|
||||||
|
requirement: requirement.into_inner(),
|
||||||
|
match_runtime,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
)
|
||||||
.collect::<Result<Vec<_>, _>>()?;
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
extra_build_requires.insert(name.clone(), lowered_requirements);
|
extra_build_requires.insert(name.clone(), lowered_requirements);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -713,6 +713,9 @@ pub(super) async fn do_sync(
|
||||||
// If necessary, convert editable to non-editable distributions.
|
// If necessary, convert editable to non-editable distributions.
|
||||||
let resolution = apply_editable_mode(resolution, editable);
|
let resolution = apply_editable_mode(resolution, editable);
|
||||||
|
|
||||||
|
// Constrain any build requirements marked as `match-runtime = true`.
|
||||||
|
let extra_build_requires = extra_build_requires.match_runtime(&resolution)?;
|
||||||
|
|
||||||
index_locations.cache_index_credentials();
|
index_locations.cache_index_credentials();
|
||||||
|
|
||||||
// Populate credentials from the target.
|
// Populate credentials from the target.
|
||||||
|
|
|
||||||
|
|
@ -12479,3 +12479,290 @@ fn sync_does_not_remove_empty_virtual_environment_directory() -> Result<()> {
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Test that build dependencies respect locked versions from the lockfile.
|
||||||
|
#[test]
|
||||||
|
fn sync_build_dependencies_respect_locked_versions() -> Result<()> {
|
||||||
|
let context = TestContext::new("3.12").with_filtered_counts();
|
||||||
|
|
||||||
|
// Write a test package that arbitrarily requires `anyio` at build time
|
||||||
|
let child = context.temp_dir.child("child");
|
||||||
|
child.create_dir_all()?;
|
||||||
|
let child_pyproject_toml = child.child("pyproject.toml");
|
||||||
|
child_pyproject_toml.write_str(indoc! {r#"
|
||||||
|
[project]
|
||||||
|
name = "child"
|
||||||
|
version = "0.1.0"
|
||||||
|
requires-python = ">=3.9"
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["hatchling", "anyio"]
|
||||||
|
backend-path = ["."]
|
||||||
|
build-backend = "build_backend"
|
||||||
|
"#})?;
|
||||||
|
|
||||||
|
// Create a build backend that checks for a specific version of anyio
|
||||||
|
let build_backend = child.child("build_backend.py");
|
||||||
|
build_backend.write_str(indoc! {r#"
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from hatchling.build import *
|
||||||
|
|
||||||
|
expected_version = os.environ.get("EXPECTED_ANYIO_VERSION", "")
|
||||||
|
if not expected_version:
|
||||||
|
print("`EXPECTED_ANYIO_VERSION` not set", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
try:
|
||||||
|
import anyio
|
||||||
|
except ModuleNotFoundError:
|
||||||
|
print("Missing `anyio` module", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
from importlib.metadata import version
|
||||||
|
anyio_version = version("anyio")
|
||||||
|
|
||||||
|
if not anyio_version.startswith(expected_version):
|
||||||
|
print(f"Expected `anyio` version {expected_version} but got {anyio_version}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print(f"Found expected `anyio` version {anyio_version}", file=sys.stderr)
|
||||||
|
"#})?;
|
||||||
|
child.child("src/child/__init__.py").touch()?;
|
||||||
|
|
||||||
|
// Create a project that will resolve to a non-latest version of `anyio`
|
||||||
|
let parent = &context.temp_dir;
|
||||||
|
let pyproject_toml = parent.child("pyproject.toml");
|
||||||
|
pyproject_toml.write_str(indoc! {r#"
|
||||||
|
[project]
|
||||||
|
name = "parent"
|
||||||
|
version = "0.1.0"
|
||||||
|
requires-python = ">=3.9"
|
||||||
|
dependencies = ["anyio<4.1"]
|
||||||
|
"#})?;
|
||||||
|
|
||||||
|
uv_snapshot!(context.filters(), context.lock(), @r"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
Resolved [N] packages in [TIME]
|
||||||
|
");
|
||||||
|
|
||||||
|
// Now add the child dependency.
|
||||||
|
pyproject_toml.write_str(indoc! {r#"
|
||||||
|
[project]
|
||||||
|
name = "parent"
|
||||||
|
version = "0.1.0"
|
||||||
|
requires-python = ">=3.9"
|
||||||
|
dependencies = ["anyio<4.1", "child"]
|
||||||
|
|
||||||
|
[tool.uv.sources]
|
||||||
|
child = { path = "child" }
|
||||||
|
"#})?;
|
||||||
|
|
||||||
|
// Ensure our build backend is checking the version correctly
|
||||||
|
uv_snapshot!(context.filters(), context.sync().env("EXPECTED_ANYIO_VERSION", "3.0"), @r"
|
||||||
|
success: false
|
||||||
|
exit_code: 1
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
Resolved [N] packages in [TIME]
|
||||||
|
× Failed to build `child @ file://[TEMP_DIR]/child`
|
||||||
|
├─▶ The build backend returned an error
|
||||||
|
╰─▶ Call to `build_backend.build_wheel` failed (exit status: 1)
|
||||||
|
|
||||||
|
[stderr]
|
||||||
|
Expected `anyio` version 3.0 but got 4.3.0
|
||||||
|
|
||||||
|
hint: This usually indicates a problem with the package or the build environment.
|
||||||
|
help: `child` was included because `parent` (v0.1.0) depends on `child`
|
||||||
|
");
|
||||||
|
|
||||||
|
// Now constrain the `anyio` build dependency to match the runtime
|
||||||
|
pyproject_toml.write_str(indoc! {r#"
|
||||||
|
[project]
|
||||||
|
name = "parent"
|
||||||
|
version = "0.1.0"
|
||||||
|
requires-python = ">=3.9"
|
||||||
|
dependencies = ["anyio<4.1", "child"]
|
||||||
|
|
||||||
|
[tool.uv.sources]
|
||||||
|
child = { path = "child" }
|
||||||
|
|
||||||
|
[tool.uv.extra-build-dependencies]
|
||||||
|
child = [{ requirement = "anyio", match-runtime = true }]
|
||||||
|
"#})?;
|
||||||
|
|
||||||
|
// The child should be built with anyio 4.0
|
||||||
|
uv_snapshot!(context.filters(), context.sync().env("EXPECTED_ANYIO_VERSION", "4.0"), @r"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
warning: The `extra-build-dependencies` option is experimental and may change without warning. Pass `--preview-features extra-build-dependencies` to disable this warning.
|
||||||
|
Resolved [N] packages in [TIME]
|
||||||
|
Prepared [N] packages in [TIME]
|
||||||
|
Installed [N] packages in [TIME]
|
||||||
|
+ anyio==4.0.0
|
||||||
|
+ child==0.1.0 (from file://[TEMP_DIR]/child)
|
||||||
|
+ idna==3.6
|
||||||
|
+ sniffio==1.3.1
|
||||||
|
");
|
||||||
|
|
||||||
|
// Change the constraints on anyio
|
||||||
|
pyproject_toml.write_str(indoc! {r#"
|
||||||
|
[project]
|
||||||
|
name = "parent"
|
||||||
|
version = "0.1.0"
|
||||||
|
requires-python = ">=3.9"
|
||||||
|
dependencies = ["anyio<3.8", "child"]
|
||||||
|
|
||||||
|
[tool.uv.sources]
|
||||||
|
child = { path = "child" }
|
||||||
|
|
||||||
|
[tool.uv.extra-build-dependencies]
|
||||||
|
child = [{ requirement = "anyio", match-runtime = true }]
|
||||||
|
"#})?;
|
||||||
|
|
||||||
|
// The child should be rebuilt with anyio 3.7, without `--reinstall`
|
||||||
|
uv_snapshot!(context.filters(), context.sync()
|
||||||
|
.arg("--reinstall-package").arg("child").env("EXPECTED_ANYIO_VERSION", "4.0"), @r"
|
||||||
|
success: false
|
||||||
|
exit_code: 1
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
warning: The `extra-build-dependencies` option is experimental and may change without warning. Pass `--preview-features extra-build-dependencies` to disable this warning.
|
||||||
|
Resolved [N] packages in [TIME]
|
||||||
|
× Failed to build `child @ file://[TEMP_DIR]/child`
|
||||||
|
├─▶ The build backend returned an error
|
||||||
|
╰─▶ Call to `build_backend.build_wheel` failed (exit status: 1)
|
||||||
|
|
||||||
|
[stderr]
|
||||||
|
Expected `anyio` version 4.0 but got 3.7.1
|
||||||
|
|
||||||
|
hint: This usually indicates a problem with the package or the build environment.
|
||||||
|
help: `child` was included because `parent` (v0.1.0) depends on `child`
|
||||||
|
");
|
||||||
|
|
||||||
|
uv_snapshot!(context.filters(), context.sync()
|
||||||
|
.arg("--reinstall-package").arg("child").env("EXPECTED_ANYIO_VERSION", "3.7"), @r"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
warning: The `extra-build-dependencies` option is experimental and may change without warning. Pass `--preview-features extra-build-dependencies` to disable this warning.
|
||||||
|
Resolved [N] packages in [TIME]
|
||||||
|
Prepared [N] packages in [TIME]
|
||||||
|
Uninstalled [N] packages in [TIME]
|
||||||
|
Installed [N] packages in [TIME]
|
||||||
|
- anyio==4.0.0
|
||||||
|
+ anyio==3.7.1
|
||||||
|
~ child==0.1.0 (from file://[TEMP_DIR]/child)
|
||||||
|
");
|
||||||
|
|
||||||
|
// With preview enabled, there's no warning
|
||||||
|
uv_snapshot!(context.filters(), context.sync()
|
||||||
|
.arg("--preview-features").arg("extra-build-dependencies")
|
||||||
|
.arg("--reinstall-package").arg("child")
|
||||||
|
.env("EXPECTED_ANYIO_VERSION", "3.7"), @r"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
Resolved [N] packages in [TIME]
|
||||||
|
Prepared [N] packages in [TIME]
|
||||||
|
Uninstalled [N] packages in [TIME]
|
||||||
|
Installed [N] packages in [TIME]
|
||||||
|
~ child==0.1.0 (from file://[TEMP_DIR]/child)
|
||||||
|
");
|
||||||
|
|
||||||
|
// Now, we'll set a constraint in the parent project
|
||||||
|
pyproject_toml.write_str(indoc! {r#"
|
||||||
|
[project]
|
||||||
|
name = "parent"
|
||||||
|
version = "0.1.0"
|
||||||
|
requires-python = ">=3.9"
|
||||||
|
dependencies = ["anyio<3.8", "child"]
|
||||||
|
|
||||||
|
[tool.uv.sources]
|
||||||
|
child = { path = "child" }
|
||||||
|
|
||||||
|
[tool.uv.extra-build-dependencies]
|
||||||
|
child = [{ requirement = "anyio", match-runtime = true }]
|
||||||
|
"#})?;
|
||||||
|
|
||||||
|
// And an incompatible constraint in the child project
|
||||||
|
child_pyproject_toml.write_str(indoc! {r#"
|
||||||
|
[project]
|
||||||
|
name = "child"
|
||||||
|
version = "0.1.0"
|
||||||
|
requires-python = ">=3.9"
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["hatchling", "anyio>3.8,<4.2"]
|
||||||
|
backend-path = ["."]
|
||||||
|
build-backend = "build_backend"
|
||||||
|
"#})?;
|
||||||
|
|
||||||
|
// This should fail
|
||||||
|
uv_snapshot!(context.filters(), context.sync()
|
||||||
|
.arg("--reinstall-package").arg("child").env("EXPECTED_ANYIO_VERSION", "4.1"), @r"
|
||||||
|
success: false
|
||||||
|
exit_code: 1
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
warning: The `extra-build-dependencies` option is experimental and may change without warning. Pass `--preview-features extra-build-dependencies` to disable this warning.
|
||||||
|
Resolved [N] packages in [TIME]
|
||||||
|
× Failed to build `child @ file://[TEMP_DIR]/child`
|
||||||
|
├─▶ Failed to resolve requirements from `build-system.requires` and `extra-build-dependencies`
|
||||||
|
├─▶ No solution found when resolving: `hatchling`, `anyio>3.8, <4.2`, `anyio==3.7.1 (index: https://pypi.org/simple)`
|
||||||
|
╰─▶ Because you require anyio>3.8,<4.2 and anyio==3.7.1, we can conclude that your requirements are unsatisfiable.
|
||||||
|
help: `child` was included because `parent` (v0.1.0) depends on `child`
|
||||||
|
");
|
||||||
|
|
||||||
|
// Adding a version specifier should also fail
|
||||||
|
pyproject_toml.write_str(indoc! {r#"
|
||||||
|
[project]
|
||||||
|
name = "parent"
|
||||||
|
version = "0.1.0"
|
||||||
|
requires-python = ">=3.9"
|
||||||
|
dependencies = ["anyio<4.1", "child"]
|
||||||
|
|
||||||
|
[tool.uv.sources]
|
||||||
|
child = { path = "child" }
|
||||||
|
|
||||||
|
[tool.uv.extra-build-dependencies]
|
||||||
|
child = [{ requirement = "anyio>4", match-runtime = true }]
|
||||||
|
"#})?;
|
||||||
|
|
||||||
|
uv_snapshot!(context.filters(), context.sync(), @r#"
|
||||||
|
success: false
|
||||||
|
exit_code: 2
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
warning: Failed to parse `pyproject.toml` during settings discovery:
|
||||||
|
TOML parse error at line 11, column 9
|
||||||
|
|
|
||||||
|
11 | child = [{ requirement = "anyio>4", match-runtime = true }]
|
||||||
|
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
Dependencies marked with `match-runtime = true` cannot include version specifiers
|
||||||
|
|
||||||
|
error: Failed to parse: `pyproject.toml`
|
||||||
|
Caused by: TOML parse error at line 11, column 9
|
||||||
|
|
|
||||||
|
11 | child = [{ requirement = "anyio>4", match-runtime = true }]
|
||||||
|
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
Dependencies marked with `match-runtime = true` cannot include version specifiers
|
||||||
|
"#);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
|
||||||
24
uv.schema.json
generated
24
uv.schema.json
generated
|
|
@ -889,10 +889,32 @@
|
||||||
"additionalProperties": {
|
"additionalProperties": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/definitions/Requirement"
|
"$ref": "#/definitions/ExtraBuildDependency"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"ExtraBuildDependency": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/Requirement"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"match-runtime": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"requirement": {
|
||||||
|
"$ref": "#/definitions/Requirement"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"requirement",
|
||||||
|
"match-runtime"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"ExtraName": {
|
"ExtraName": {
|
||||||
"description": "The normalized name of an extra dependency.\n\nConverts the name to lowercase and collapses runs of `-`, `_`, and `.` down to a single `-`.\nFor example, `---`, `.`, and `__` are all converted to a single `-`.\n\nSee:\n- <https://peps.python.org/pep-0685/#specification/>\n- <https://packaging.python.org/en/latest/specifications/name-normalization/>",
|
"description": "The normalized name of an extra dependency.\n\nConverts the name to lowercase and collapses runs of `-`, `_`, and `.` down to a single `-`.\nFor example, `---`, `.`, and `__` are all converted to a single `-`.\n\nSee:\n- <https://peps.python.org/pep-0685/#specification/>\n- <https://packaging.python.org/en/latest/specifications/name-normalization/>",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue