mirror of
https://github.com/astral-sh/uv.git
synced 2025-08-02 18:12:17 +00:00
Allow conflicting extras in explicit index assignments (#9160)
## Summary This PR enables something like the "final boss" of PyTorch setups -- explicit support for CPU vs. GPU-enabled variants via extras: ```toml [project] name = "project" version = "0.1.0" requires-python = ">=3.13.0" dependencies = [] [project.optional-dependencies] cpu = [ "torch==2.5.1+cpu", ] gpu = [ "torch==2.5.1", ] [tool.uv.sources] torch = [ { index = "torch-cpu", extra = "cpu" }, { index = "torch-gpu", extra = "gpu" }, ] [[tool.uv.index]] name = "torch-cpu" url = "https://download.pytorch.org/whl/cpu" explicit = true [[tool.uv.index]] name = "torch-gpu" url = "https://download.pytorch.org/whl/cu124" explicit = true [tool.uv] conflicts = [ [ { extra = "cpu" }, { extra = "gpu" }, ], ] ``` It builds atop the conflicting extras work to allow sources to be marked as specific to a dedicated extra being enabled or disabled. As part of this work, sources now have an `extra` field. If a source has an `extra`, it means that the source is only applied to the requirement when defined within that optional group. For example, `{ index = "torch-cpu", extra = "cpu" }` above only applies to `"torch==2.5.1+cpu"`. The `extra` field does _not_ mean that the source is "enabled" when the extra is activated. For example, this wouldn't work: ```toml [project] name = "project" version = "0.1.0" requires-python = ">=3.13.0" dependencies = ["torch"] [tool.uv.sources] torch = [ { index = "torch-cpu", extra = "cpu" }, { index = "torch-gpu", extra = "gpu" }, ] [[tool.uv.index]] name = "torch-cpu" url = "https://download.pytorch.org/whl/cpu" explicit = true [[tool.uv.index]] name = "torch-gpu" url = "https://download.pytorch.org/whl/cu124" explicit = true ``` In this case, the sources would effectively be ignored. Extras are really confusing... but I think this is correct? We don't want enabling or disabling extras to affect resolution information that's _outside_ of the relevant optional group.
This commit is contained in:
parent
a88a3e5eba
commit
e4fc875afa
19 changed files with 1607 additions and 227 deletions
|
@ -216,6 +216,7 @@ impl From<&ResolvedDist> for RequirementSource {
|
|||
uv_pep440::VersionSpecifier::equals_version(version.clone()),
|
||||
),
|
||||
index: Some(wheels.best_wheel().index.url().clone()),
|
||||
conflict: None,
|
||||
},
|
||||
Dist::Built(BuiltDist::DirectUrl(wheel)) => {
|
||||
let mut location = wheel.url.to_url();
|
||||
|
@ -237,6 +238,7 @@ impl From<&ResolvedDist> for RequirementSource {
|
|||
uv_pep440::VersionSpecifier::equals_version(sdist.version.clone()),
|
||||
),
|
||||
index: Some(sdist.index.url().clone()),
|
||||
conflict: None,
|
||||
},
|
||||
Dist::Source(SourceDist::DirectUrl(sdist)) => {
|
||||
let mut location = sdist.url.to_url();
|
||||
|
@ -272,6 +274,7 @@ impl From<&ResolvedDist> for RequirementSource {
|
|||
uv_pep440::VersionSpecifier::equals_version(dist.version().clone()),
|
||||
),
|
||||
index: None,
|
||||
conflict: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,10 +10,12 @@ use uv_configuration::LowerBound;
|
|||
use uv_distribution_filename::DistExtension;
|
||||
use uv_distribution_types::{Index, IndexLocations, IndexName, Origin};
|
||||
use uv_git::GitReference;
|
||||
use uv_normalize::PackageName;
|
||||
use uv_normalize::{ExtraName, GroupName, PackageName};
|
||||
use uv_pep440::VersionSpecifiers;
|
||||
use uv_pep508::{MarkerTree, VerbatimUrl, VersionOrUrl};
|
||||
use uv_pypi_types::{ParsedUrlError, Requirement, RequirementSource, VerbatimParsedUrl};
|
||||
use uv_pypi_types::{
|
||||
ConflictItem, ParsedUrlError, Requirement, RequirementSource, VerbatimParsedUrl,
|
||||
};
|
||||
use uv_warnings::warn_user_once;
|
||||
use uv_workspace::pyproject::{PyProjectToml, Source, Sources};
|
||||
use uv_workspace::Workspace;
|
||||
|
@ -39,11 +41,14 @@ impl LoweredRequirement {
|
|||
project_dir: &'data Path,
|
||||
project_sources: &'data BTreeMap<PackageName, Sources>,
|
||||
project_indexes: &'data [Index],
|
||||
extra: Option<&ExtraName>,
|
||||
group: Option<&GroupName>,
|
||||
locations: &'data IndexLocations,
|
||||
workspace: &'data Workspace,
|
||||
lower_bound: LowerBound,
|
||||
git_member: Option<&'data GitWorkspaceMember<'data>>,
|
||||
) -> impl Iterator<Item = Result<Self, LoweringError>> + 'data {
|
||||
// Identify the source from the `tool.uv.sources` table.
|
||||
let (source, origin) = if let Some(source) = project_sources.get(&requirement.name) {
|
||||
(Some(source), RequirementOrigin::Project)
|
||||
} else if let Some(source) = workspace.sources().get(&requirement.name) {
|
||||
|
@ -51,7 +56,29 @@ impl LoweredRequirement {
|
|||
} else {
|
||||
(None, RequirementOrigin::Project)
|
||||
};
|
||||
let source = source.cloned();
|
||||
|
||||
// If the source only applies to a given extra or dependency group, filter it out.
|
||||
let source = source.map(|source| {
|
||||
source
|
||||
.iter()
|
||||
.filter(|source| {
|
||||
if let Some(target) = source.extra() {
|
||||
if extra != Some(target) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(target) = source.group() {
|
||||
if group != Some(target) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
})
|
||||
.cloned()
|
||||
.collect::<Sources>()
|
||||
});
|
||||
|
||||
let workspace_package_declared =
|
||||
// We require that when you use a package that's part of the workspace, ...
|
||||
|
@ -92,7 +119,7 @@ impl LoweredRequirement {
|
|||
// Determine the space covered by the sources.
|
||||
let mut total = MarkerTree::FALSE;
|
||||
for source in source.iter() {
|
||||
total.or(source.marker());
|
||||
total.or(source.marker().clone());
|
||||
}
|
||||
|
||||
// Determine the space covered by the requirement.
|
||||
|
@ -117,6 +144,7 @@ impl LoweredRequirement {
|
|||
tag,
|
||||
branch,
|
||||
marker,
|
||||
..
|
||||
} => {
|
||||
if matches!(requirement.version_or_url, Some(VersionOrUrl::Url(_))) {
|
||||
return Err(LoweringError::ConflictingUrls);
|
||||
|
@ -134,6 +162,7 @@ impl LoweredRequirement {
|
|||
url,
|
||||
subdirectory,
|
||||
marker,
|
||||
..
|
||||
} => {
|
||||
if matches!(requirement.version_or_url, Some(VersionOrUrl::Url(_))) {
|
||||
return Err(LoweringError::ConflictingUrls);
|
||||
|
@ -145,6 +174,7 @@ impl LoweredRequirement {
|
|||
path,
|
||||
editable,
|
||||
marker,
|
||||
..
|
||||
} => {
|
||||
if matches!(requirement.version_or_url, Some(VersionOrUrl::Url(_))) {
|
||||
return Err(LoweringError::ConflictingUrls);
|
||||
|
@ -158,7 +188,12 @@ impl LoweredRequirement {
|
|||
)?;
|
||||
(source, marker)
|
||||
}
|
||||
Source::Registry { index, marker } => {
|
||||
Source::Registry {
|
||||
index,
|
||||
marker,
|
||||
extra,
|
||||
group,
|
||||
} => {
|
||||
// Identify the named index from either the project indexes or the workspace indexes,
|
||||
// in that order.
|
||||
let Some(index) = locations
|
||||
|
@ -176,13 +211,23 @@ impl LoweredRequirement {
|
|||
index,
|
||||
));
|
||||
};
|
||||
let source =
|
||||
registry_source(&requirement, index.into_url(), lower_bound)?;
|
||||
let conflict = if let Some(extra) = extra {
|
||||
Some(ConflictItem::from((project_name.clone(), extra)))
|
||||
} else {
|
||||
group.map(|group| ConflictItem::from((project_name.clone(), group)))
|
||||
};
|
||||
let source = registry_source(
|
||||
&requirement,
|
||||
index.into_url(),
|
||||
conflict,
|
||||
lower_bound,
|
||||
)?;
|
||||
(source, marker)
|
||||
}
|
||||
Source::Workspace {
|
||||
workspace: is_workspace,
|
||||
marker,
|
||||
..
|
||||
} => {
|
||||
if !is_workspace {
|
||||
return Err(LoweringError::WorkspaceFalse);
|
||||
|
@ -291,13 +336,27 @@ impl LoweredRequirement {
|
|||
return Either::Left(std::iter::once(Ok(Self(Requirement::from(requirement)))));
|
||||
};
|
||||
|
||||
// If the source only applies to a given extra, filter it out.
|
||||
let source = source
|
||||
.iter()
|
||||
.filter(|source| {
|
||||
source.extra().map_or(true, |target| {
|
||||
requirement
|
||||
.marker
|
||||
.top_level_extra_name()
|
||||
.is_some_and(|extra| extra == *target)
|
||||
})
|
||||
})
|
||||
.cloned()
|
||||
.collect::<Sources>();
|
||||
|
||||
// Determine whether the markers cover the full space for the requirement. If not, fill the
|
||||
// remaining space with the negation of the sources.
|
||||
let remaining = {
|
||||
// Determine the space covered by the sources.
|
||||
let mut total = MarkerTree::FALSE;
|
||||
for source in source.iter() {
|
||||
total.or(source.marker());
|
||||
total.or(source.marker().clone());
|
||||
}
|
||||
|
||||
// Determine the space covered by the requirement.
|
||||
|
@ -322,6 +381,7 @@ impl LoweredRequirement {
|
|||
tag,
|
||||
branch,
|
||||
marker,
|
||||
..
|
||||
} => {
|
||||
if matches!(requirement.version_or_url, Some(VersionOrUrl::Url(_))) {
|
||||
return Err(LoweringError::ConflictingUrls);
|
||||
|
@ -339,6 +399,7 @@ impl LoweredRequirement {
|
|||
url,
|
||||
subdirectory,
|
||||
marker,
|
||||
..
|
||||
} => {
|
||||
if matches!(requirement.version_or_url, Some(VersionOrUrl::Url(_))) {
|
||||
return Err(LoweringError::ConflictingUrls);
|
||||
|
@ -350,6 +411,7 @@ impl LoweredRequirement {
|
|||
path,
|
||||
editable,
|
||||
marker,
|
||||
..
|
||||
} => {
|
||||
if matches!(requirement.version_or_url, Some(VersionOrUrl::Url(_))) {
|
||||
return Err(LoweringError::ConflictingUrls);
|
||||
|
@ -363,7 +425,7 @@ impl LoweredRequirement {
|
|||
)?;
|
||||
(source, marker)
|
||||
}
|
||||
Source::Registry { index, marker } => {
|
||||
Source::Registry { index, marker, .. } => {
|
||||
let Some(index) = locations
|
||||
.indexes()
|
||||
.filter(|index| matches!(index.origin, Some(Origin::Cli)))
|
||||
|
@ -378,8 +440,13 @@ impl LoweredRequirement {
|
|||
index,
|
||||
));
|
||||
};
|
||||
let source =
|
||||
registry_source(&requirement, index.into_url(), lower_bound)?;
|
||||
let conflict = None;
|
||||
let source = registry_source(
|
||||
&requirement,
|
||||
index.into_url(),
|
||||
conflict,
|
||||
lower_bound,
|
||||
)?;
|
||||
(source, marker)
|
||||
}
|
||||
Source::Workspace { .. } => {
|
||||
|
@ -512,6 +579,7 @@ fn url_source(url: Url, subdirectory: Option<PathBuf>) -> Result<RequirementSour
|
|||
fn registry_source(
|
||||
requirement: &uv_pep508::Requirement<VerbatimParsedUrl>,
|
||||
index: Url,
|
||||
conflict: Option<ConflictItem>,
|
||||
bounds: LowerBound,
|
||||
) -> Result<RequirementSource, LoweringError> {
|
||||
match &requirement.version_or_url {
|
||||
|
@ -525,11 +593,13 @@ fn registry_source(
|
|||
Ok(RequirementSource::Registry {
|
||||
specifier: VersionSpecifiers::empty(),
|
||||
index: Some(index),
|
||||
conflict,
|
||||
})
|
||||
}
|
||||
Some(VersionOrUrl::VersionSpecifier(version)) => Ok(RequirementSource::Registry {
|
||||
specifier: version.clone(),
|
||||
index: Some(index),
|
||||
conflict,
|
||||
}),
|
||||
Some(VersionOrUrl::Url(_)) => Err(LoweringError::ConflictingUrls),
|
||||
}
|
||||
|
|
|
@ -28,6 +28,14 @@ pub enum MetadataError {
|
|||
LoweringError(PackageName, #[source] Box<LoweringError>),
|
||||
#[error("Failed to parse entry in group `{0}`: `{1}`")]
|
||||
GroupLoweringError(GroupName, PackageName, #[source] Box<LoweringError>),
|
||||
#[error("Source entry for `{0}` only applies to extra `{1}`, but the `{1}` extra does not exist. When an extra is present on a source (e.g., `extra = \"{1}\"`), the relevant package must be included in the `project.optional-dependencies` section for that extra (e.g., `project.optional-dependencies = {{ \"{1}\" = [\"{0}\"] }}`).")]
|
||||
MissingSourceExtra(PackageName, ExtraName),
|
||||
#[error("Source entry for `{0}` only applies to extra `{1}`, but `{0}` was not found under the `project.optional-dependencies` section for that extra. When an extra is present on a source (e.g., `extra = \"{1}\"`), the relevant package must be included in the `project.optional-dependencies` section for that extra (e.g., `project.optional-dependencies = {{ \"{1}\" = [\"{0}\"] }}`).")]
|
||||
IncompleteSourceExtra(PackageName, ExtraName),
|
||||
#[error("Source entry for `{0}` only applies to dependency group `{1}`, but the `{1}` group does not exist. When a group is present on a source (e.g., `group = \"{1}\"`), the relevant package must be included in the `dependency-groups` section for that extra (e.g., `dependency-groups = {{ \"{1}\" = [\"{0}\"] }}`).")]
|
||||
MissingSourceGroup(PackageName, GroupName),
|
||||
#[error("Source entry for `{0}` only applies to dependency group `{1}`, but `{0}` was not found under the `dependency-groups` section for that group. When a group is present on a source (e.g., `group = \"{1}\"`), the relevant package must be included in the `dependency-groups` section for that extra (e.g., `dependency-groups = {{ \"{1}\" = [\"{0}\"] }}`).")]
|
||||
IncompleteSourceGroup(PackageName, GroupName),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
|
|
@ -7,7 +7,7 @@ use uv_configuration::{LowerBound, SourceStrategy};
|
|||
use uv_distribution_types::IndexLocations;
|
||||
use uv_normalize::{ExtraName, GroupName, PackageName, DEV_DEPENDENCIES};
|
||||
use uv_workspace::dependency_groups::FlatDependencyGroups;
|
||||
use uv_workspace::pyproject::ToolUvSources;
|
||||
use uv_workspace::pyproject::{Sources, ToolUvSources};
|
||||
use uv_workspace::{DiscoveryOptions, ProjectWorkspace};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
@ -111,6 +111,7 @@ impl RequiresDist {
|
|||
SourceStrategy::Disabled => &empty,
|
||||
};
|
||||
|
||||
// Collect the dependency groups.
|
||||
let dependency_groups = {
|
||||
// First, collect `tool.uv.dev_dependencies`
|
||||
let dev_dependencies = project_workspace
|
||||
|
@ -130,85 +131,90 @@ impl RequiresDist {
|
|||
.flatten()
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
|
||||
// Resolve any `include-group` entries in `dependency-groups`.
|
||||
let dependency_groups =
|
||||
// Flatten the dependency groups.
|
||||
let mut dependency_groups =
|
||||
FlatDependencyGroups::from_dependency_groups(&dependency_groups)
|
||||
.map_err(|err| err.with_dev_dependencies(dev_dependencies))?
|
||||
.into_iter()
|
||||
.chain(
|
||||
// Only add the `dev` group if `dev-dependencies` is defined.
|
||||
dev_dependencies
|
||||
.into_iter()
|
||||
.map(|requirements| (DEV_DEPENDENCIES.clone(), requirements.clone())),
|
||||
)
|
||||
.map(|(name, requirements)| {
|
||||
let requirements = match source_strategy {
|
||||
SourceStrategy::Enabled => requirements
|
||||
.into_iter()
|
||||
.flat_map(|requirement| {
|
||||
let group_name = name.clone();
|
||||
let requirement_name = requirement.name.clone();
|
||||
LoweredRequirement::from_requirement(
|
||||
requirement,
|
||||
&metadata.name,
|
||||
project_workspace.project_root(),
|
||||
project_sources,
|
||||
project_indexes,
|
||||
locations,
|
||||
project_workspace.workspace(),
|
||||
lower_bound,
|
||||
git_member,
|
||||
)
|
||||
.map(move |requirement| {
|
||||
match requirement {
|
||||
Ok(requirement) => Ok(requirement.into_inner()),
|
||||
Err(err) => Err(MetadataError::GroupLoweringError(
|
||||
group_name.clone(),
|
||||
requirement_name.clone(),
|
||||
Box::new(err),
|
||||
)),
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>(),
|
||||
SourceStrategy::Disabled => Ok(requirements
|
||||
.into_iter()
|
||||
.map(uv_pypi_types::Requirement::from)
|
||||
.collect()),
|
||||
}?;
|
||||
Ok::<(GroupName, Vec<uv_pypi_types::Requirement>), MetadataError>((
|
||||
name,
|
||||
requirements,
|
||||
))
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
.map_err(|err| err.with_dev_dependencies(dev_dependencies))?;
|
||||
|
||||
// Merge any overlapping groups.
|
||||
let mut map = BTreeMap::new();
|
||||
for (name, dependencies) in dependency_groups {
|
||||
match map.entry(name) {
|
||||
std::collections::btree_map::Entry::Vacant(entry) => {
|
||||
entry.insert(dependencies);
|
||||
}
|
||||
std::collections::btree_map::Entry::Occupied(mut entry) => {
|
||||
entry.get_mut().extend(dependencies);
|
||||
}
|
||||
}
|
||||
// Add the `dev` group, if `dev-dependencies` is defined.
|
||||
if let Some(dev_dependencies) = dev_dependencies {
|
||||
dependency_groups
|
||||
.entry(DEV_DEPENDENCIES.clone())
|
||||
.or_insert_with(Vec::new)
|
||||
.extend(dev_dependencies.clone());
|
||||
}
|
||||
map
|
||||
|
||||
dependency_groups
|
||||
};
|
||||
|
||||
// Now that we've resolved the dependency groups, we can validate that each source references
|
||||
// a valid extra or group, if present.
|
||||
Self::validate_sources(project_sources, &metadata, &dependency_groups)?;
|
||||
|
||||
// Lower the dependency groups.
|
||||
let dependency_groups = dependency_groups
|
||||
.into_iter()
|
||||
.map(|(name, requirements)| {
|
||||
let requirements = match source_strategy {
|
||||
SourceStrategy::Enabled => requirements
|
||||
.into_iter()
|
||||
.flat_map(|requirement| {
|
||||
let requirement_name = requirement.name.clone();
|
||||
let group = name.clone();
|
||||
let extra = None;
|
||||
LoweredRequirement::from_requirement(
|
||||
requirement,
|
||||
&metadata.name,
|
||||
project_workspace.project_root(),
|
||||
project_sources,
|
||||
project_indexes,
|
||||
extra,
|
||||
Some(&group),
|
||||
locations,
|
||||
project_workspace.workspace(),
|
||||
lower_bound,
|
||||
git_member,
|
||||
)
|
||||
.map(
|
||||
move |requirement| match requirement {
|
||||
Ok(requirement) => Ok(requirement.into_inner()),
|
||||
Err(err) => Err(MetadataError::GroupLoweringError(
|
||||
group.clone(),
|
||||
requirement_name.clone(),
|
||||
Box::new(err),
|
||||
)),
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>(),
|
||||
SourceStrategy::Disabled => Ok(requirements
|
||||
.into_iter()
|
||||
.map(uv_pypi_types::Requirement::from)
|
||||
.collect()),
|
||||
}?;
|
||||
Ok::<(GroupName, Vec<uv_pypi_types::Requirement>), MetadataError>((
|
||||
name,
|
||||
requirements,
|
||||
))
|
||||
})
|
||||
.collect::<Result<BTreeMap<_, _>, _>>()?;
|
||||
|
||||
// Lower the requirements.
|
||||
let requires_dist = metadata.requires_dist.into_iter();
|
||||
let requires_dist = match source_strategy {
|
||||
SourceStrategy::Enabled => requires_dist
|
||||
.flat_map(|requirement| {
|
||||
let requirement_name = requirement.name.clone();
|
||||
let extra = requirement.marker.top_level_extra_name();
|
||||
let group = None;
|
||||
LoweredRequirement::from_requirement(
|
||||
requirement,
|
||||
&metadata.name,
|
||||
project_workspace.project_root(),
|
||||
project_sources,
|
||||
project_indexes,
|
||||
extra.as_ref(),
|
||||
group,
|
||||
locations,
|
||||
project_workspace.workspace(),
|
||||
lower_bound,
|
||||
|
@ -236,6 +242,64 @@ impl RequiresDist {
|
|||
provides_extras: metadata.provides_extras,
|
||||
})
|
||||
}
|
||||
|
||||
/// Validate the sources for a given [`uv_pypi_types::RequiresDist`].
|
||||
///
|
||||
/// If a source is requested with an `extra` or `group`, ensure that the relevant dependency is
|
||||
/// present in the relevant `project.optional-dependencies` or `dependency-groups` section.
|
||||
fn validate_sources(
|
||||
sources: &BTreeMap<PackageName, Sources>,
|
||||
metadata: &uv_pypi_types::RequiresDist,
|
||||
dependency_groups: &FlatDependencyGroups,
|
||||
) -> Result<(), MetadataError> {
|
||||
for (name, sources) in sources {
|
||||
for source in sources.iter() {
|
||||
if let Some(extra) = source.extra() {
|
||||
// If the extra doesn't exist at all, error.
|
||||
if !metadata.provides_extras.contains(extra) {
|
||||
return Err(MetadataError::MissingSourceExtra(
|
||||
name.clone(),
|
||||
extra.clone(),
|
||||
));
|
||||
}
|
||||
|
||||
// If there is no such requirement with the extra, error.
|
||||
if !metadata.requires_dist.iter().any(|requirement| {
|
||||
requirement.name == *name
|
||||
&& requirement.marker.top_level_extra_name().as_ref() == Some(extra)
|
||||
}) {
|
||||
return Err(MetadataError::IncompleteSourceExtra(
|
||||
name.clone(),
|
||||
extra.clone(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(group) = source.group() {
|
||||
// If the group doesn't exist at all, error.
|
||||
let Some(dependencies) = dependency_groups.get(group) else {
|
||||
return Err(MetadataError::MissingSourceGroup(
|
||||
name.clone(),
|
||||
group.clone(),
|
||||
));
|
||||
};
|
||||
|
||||
// If there is no such requirement with the group, error.
|
||||
if !dependencies
|
||||
.iter()
|
||||
.any(|requirement| requirement.name == *name)
|
||||
{
|
||||
return Err(MetadataError::IncompleteSourceGroup(
|
||||
name.clone(),
|
||||
group.clone(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Metadata> for RequiresDist {
|
||||
|
@ -383,7 +447,28 @@ mod test {
|
|||
|
|
||||
8 | tqdm = { git = "https://github.com/tqdm/tqdm", ref = "baaaaaab" }
|
||||
| ^^^
|
||||
unknown field `ref`, expected one of `git`, `subdirectory`, `rev`, `tag`, `branch`, `url`, `path`, `editable`, `index`, `workspace`, `marker`
|
||||
unknown field `ref`, expected one of `git`, `subdirectory`, `rev`, `tag`, `branch`, `url`, `path`, `editable`, `index`, `workspace`, `marker`, `extra`, `group`
|
||||
"###);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn extra_and_group() {
|
||||
let input = indoc! {r#"
|
||||
[project]
|
||||
name = "foo"
|
||||
version = "0.0.0"
|
||||
dependencies = []
|
||||
|
||||
[tool.uv.sources]
|
||||
tqdm = { git = "https://github.com/tqdm/tqdm", extra = "torch", group = "dev" }
|
||||
"#};
|
||||
|
||||
assert_snapshot!(format_err(input).await, @r###"
|
||||
error: TOML parse error at line 7, column 8
|
||||
|
|
||||
7 | tqdm = { git = "https://github.com/tqdm/tqdm", extra = "torch", group = "dev" }
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
cannot specify both `extra` and `group`
|
||||
"###);
|
||||
}
|
||||
|
||||
|
|
|
@ -426,7 +426,16 @@ pub enum MarkerValueExtra {
|
|||
}
|
||||
|
||||
impl MarkerValueExtra {
|
||||
fn as_extra(&self) -> Option<&ExtraName> {
|
||||
/// Returns the [`ExtraName`] for this value, if it is a valid extra.
|
||||
pub fn as_extra(&self) -> Option<&ExtraName> {
|
||||
match self {
|
||||
Self::Extra(extra) => Some(extra),
|
||||
Self::Arbitrary(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert the [`MarkerValueExtra`] to an [`ExtraName`], if possible.
|
||||
fn into_extra(self) -> Option<ExtraName> {
|
||||
match self {
|
||||
Self::Extra(extra) => Some(extra),
|
||||
Self::Arbitrary(_) => None,
|
||||
|
@ -1113,6 +1122,19 @@ impl MarkerTree {
|
|||
extra_expression
|
||||
}
|
||||
|
||||
/// Find a top level `extra == "..."` name.
|
||||
///
|
||||
/// ASSUMPTION: There is one `extra = "..."`, and it's either the only marker or part of the
|
||||
/// main conjunction.
|
||||
pub fn top_level_extra_name(&self) -> Option<ExtraName> {
|
||||
let extra_expression = self.top_level_extra()?;
|
||||
|
||||
match extra_expression {
|
||||
MarkerExpression::Extra { name, .. } => name.into_extra(),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Simplify this marker by *assuming* that the Python version range
|
||||
/// provided is true and that the complement of it is false.
|
||||
///
|
||||
|
|
|
@ -16,8 +16,8 @@ use uv_pep508::{
|
|||
};
|
||||
|
||||
use crate::{
|
||||
Hashes, ParsedArchiveUrl, ParsedDirectoryUrl, ParsedGitUrl, ParsedPathUrl, ParsedUrl,
|
||||
ParsedUrlError, VerbatimParsedUrl,
|
||||
ConflictItem, Hashes, ParsedArchiveUrl, ParsedDirectoryUrl, ParsedGitUrl, ParsedPathUrl,
|
||||
ParsedUrl, ParsedUrlError, VerbatimParsedUrl,
|
||||
};
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
|
@ -192,11 +192,13 @@ impl From<uv_pep508::Requirement<VerbatimParsedUrl>> for Requirement {
|
|||
None => RequirementSource::Registry {
|
||||
specifier: VersionSpecifiers::empty(),
|
||||
index: None,
|
||||
conflict: None,
|
||||
},
|
||||
// The most popular case: just a name, a version range and maybe extras.
|
||||
Some(VersionOrUrl::VersionSpecifier(specifier)) => RequirementSource::Registry {
|
||||
specifier,
|
||||
index: None,
|
||||
conflict: None,
|
||||
},
|
||||
Some(VersionOrUrl::Url(url)) => {
|
||||
RequirementSource::from_parsed_url(url.parsed_url, url.verbatim)
|
||||
|
@ -229,7 +231,9 @@ impl Display for Requirement {
|
|||
)?;
|
||||
}
|
||||
match &self.source {
|
||||
RequirementSource::Registry { specifier, index } => {
|
||||
RequirementSource::Registry {
|
||||
specifier, index, ..
|
||||
} => {
|
||||
write!(f, "{specifier}")?;
|
||||
if let Some(index) = index {
|
||||
write!(f, " (index: {index})")?;
|
||||
|
@ -283,6 +287,8 @@ pub enum RequirementSource {
|
|||
specifier: VersionSpecifiers,
|
||||
/// Choose a version from the index at the given URL.
|
||||
index: Option<Url>,
|
||||
/// The conflict item associated with the source, if any.
|
||||
conflict: Option<ConflictItem>,
|
||||
},
|
||||
// TODO(konsti): Track and verify version specifier from `project.dependencies` matches the
|
||||
// version in remote location.
|
||||
|
@ -513,7 +519,9 @@ impl Display for RequirementSource {
|
|||
/// rather than for inclusion in a `requirements.txt` file.
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Registry { specifier, index } => {
|
||||
Self::Registry {
|
||||
specifier, index, ..
|
||||
} => {
|
||||
write!(f, "{specifier}")?;
|
||||
if let Some(index) = index {
|
||||
write!(f, " (index: {index})")?;
|
||||
|
@ -571,6 +579,7 @@ enum RequirementSourceWire {
|
|||
#[serde(skip_serializing_if = "VersionSpecifiers::is_empty", default)]
|
||||
specifier: VersionSpecifiers,
|
||||
index: Option<Url>,
|
||||
conflict: Option<ConflictItem>,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -580,11 +589,16 @@ impl From<RequirementSource> for RequirementSourceWire {
|
|||
RequirementSource::Registry {
|
||||
specifier,
|
||||
mut index,
|
||||
conflict,
|
||||
} => {
|
||||
if let Some(index) = index.as_mut() {
|
||||
redact_credentials(index);
|
||||
}
|
||||
Self::Registry { specifier, index }
|
||||
Self::Registry {
|
||||
specifier,
|
||||
index,
|
||||
conflict,
|
||||
}
|
||||
}
|
||||
RequirementSource::Url {
|
||||
subdirectory,
|
||||
|
@ -686,9 +700,15 @@ impl TryFrom<RequirementSourceWire> for RequirementSource {
|
|||
|
||||
fn try_from(wire: RequirementSourceWire) -> Result<RequirementSource, RequirementError> {
|
||||
match wire {
|
||||
RequirementSourceWire::Registry { specifier, index } => {
|
||||
Ok(Self::Registry { specifier, index })
|
||||
}
|
||||
RequirementSourceWire::Registry {
|
||||
specifier,
|
||||
index,
|
||||
conflict,
|
||||
} => Ok(Self::Registry {
|
||||
specifier,
|
||||
index,
|
||||
conflict,
|
||||
}),
|
||||
RequirementSourceWire::Git { git } => {
|
||||
let mut repository = Url::parse(&git)?;
|
||||
|
||||
|
@ -814,6 +834,7 @@ mod tests {
|
|||
source: RequirementSource::Registry {
|
||||
specifier: ">1,<2".parse().unwrap(),
|
||||
index: None,
|
||||
conflict: None,
|
||||
},
|
||||
origin: None,
|
||||
};
|
||||
|
|
|
@ -3757,6 +3757,7 @@ fn normalize_requirement(
|
|||
RequirementSource::Registry {
|
||||
specifier,
|
||||
mut index,
|
||||
conflict,
|
||||
} => {
|
||||
if let Some(index) = index.as_mut() {
|
||||
redact_credentials(index);
|
||||
|
@ -3765,7 +3766,11 @@ fn normalize_requirement(
|
|||
name: requirement.name,
|
||||
extras: requirement.extras,
|
||||
marker: requirement.marker,
|
||||
source: RequirementSource::Registry { specifier, index },
|
||||
source: RequirementSource::Registry {
|
||||
specifier,
|
||||
index,
|
||||
conflict,
|
||||
},
|
||||
origin: None,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use uv_distribution_types::IndexUrl;
|
||||
use uv_normalize::PackageName;
|
||||
use uv_pep508::VerbatimUrl;
|
||||
use uv_pypi_types::RequirementSource;
|
||||
use uv_pypi_types::{ConflictItem, RequirementSource};
|
||||
|
||||
use crate::resolver::ForkMap;
|
||||
use crate::{DependencyMode, Manifest, ResolverEnvironment};
|
||||
|
@ -20,7 +20,13 @@ use crate::{DependencyMode, Manifest, ResolverEnvironment};
|
|||
///
|
||||
/// [`Indexes`] would contain a single entry mapping `torch` to `https://download.pytorch.org/whl/cu121`.
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub(crate) struct Indexes(ForkMap<IndexUrl>);
|
||||
pub(crate) struct Indexes(ForkMap<Entry>);
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct Entry {
|
||||
index: IndexUrl,
|
||||
conflict: Option<ConflictItem>,
|
||||
}
|
||||
|
||||
impl Indexes {
|
||||
/// Determine the set of explicit, pinned indexes in the [`Manifest`].
|
||||
|
@ -33,13 +39,16 @@ impl Indexes {
|
|||
|
||||
for requirement in manifest.requirements(env, dependencies) {
|
||||
let RequirementSource::Registry {
|
||||
index: Some(index), ..
|
||||
index: Some(index),
|
||||
conflict,
|
||||
..
|
||||
} = &requirement.source
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let index = IndexUrl::from(VerbatimUrl::from_url(index.clone()));
|
||||
indexes.add(&requirement, index);
|
||||
let conflict = conflict.clone();
|
||||
indexes.add(&requirement, Entry { index, conflict });
|
||||
}
|
||||
|
||||
Self(indexes)
|
||||
|
@ -51,11 +60,17 @@ impl Indexes {
|
|||
}
|
||||
|
||||
/// Return the explicit index used for a package in the given fork.
|
||||
pub(crate) fn get(
|
||||
&self,
|
||||
package_name: &PackageName,
|
||||
env: &ResolverEnvironment,
|
||||
) -> Vec<&IndexUrl> {
|
||||
self.0.get(package_name, env)
|
||||
pub(crate) fn get(&self, name: &PackageName, env: &ResolverEnvironment) -> Vec<&IndexUrl> {
|
||||
let entries = self.0.get(name, env);
|
||||
entries
|
||||
.iter()
|
||||
.filter(|entry| {
|
||||
entry
|
||||
.conflict
|
||||
.as_ref()
|
||||
.map_or(true, |conflict| env.included_by_group(conflict.as_ref()))
|
||||
})
|
||||
.map(|entry| &entry.index)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -512,11 +512,6 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
|||
continue;
|
||||
}
|
||||
|
||||
let for_package = if let PubGrubPackageInner::Root(_) = &*state.next {
|
||||
None
|
||||
} else {
|
||||
state.next.name().map(|name| format!("{name}=={version}"))
|
||||
};
|
||||
// Retrieve that package dependencies.
|
||||
let forked_deps = self.get_dependencies_forking(
|
||||
&state.next,
|
||||
|
@ -540,7 +535,6 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
|||
}
|
||||
ForkedDependencies::Unforked(dependencies) => {
|
||||
state.add_package_version_dependencies(
|
||||
for_package.as_deref(),
|
||||
&version,
|
||||
&self.urls,
|
||||
&self.indexes,
|
||||
|
@ -578,7 +572,6 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
|||
&version,
|
||||
forks,
|
||||
&request_sink,
|
||||
for_package.as_deref(),
|
||||
&diverging_packages,
|
||||
) {
|
||||
forked_states.push(new_fork_state?);
|
||||
|
@ -673,7 +666,6 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
|||
version: &'a Version,
|
||||
forks: Vec<Fork>,
|
||||
request_sink: &'a Sender<Request>,
|
||||
for_package: Option<&'a str>,
|
||||
diverging_packages: &'a [PackageName],
|
||||
) -> impl Iterator<Item = Result<ForkState, ResolveError>> + 'a {
|
||||
debug!(
|
||||
|
@ -709,7 +701,6 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
|||
})
|
||||
.map(move |(fork, mut forked_state)| {
|
||||
forked_state.add_package_version_dependencies(
|
||||
for_package,
|
||||
version,
|
||||
&self.urls,
|
||||
&self.indexes,
|
||||
|
@ -2238,8 +2229,7 @@ impl ForkState {
|
|||
/// self-dependencies and handling URLs.
|
||||
fn add_package_version_dependencies(
|
||||
&mut self,
|
||||
for_package: Option<&str>,
|
||||
version: &Version,
|
||||
for_version: &Version,
|
||||
urls: &Urls,
|
||||
indexes: &Indexes,
|
||||
mut dependencies: Vec<PubGrubDependency>,
|
||||
|
@ -2271,8 +2261,10 @@ impl ForkState {
|
|||
}
|
||||
}
|
||||
|
||||
if let Some(for_package) = for_package {
|
||||
debug!("Adding transitive dependency for {for_package}: {package}{version}");
|
||||
if let Some(name) = self.next.name_no_root() {
|
||||
debug!(
|
||||
"Adding transitive dependency for {name}=={for_version}: {package}{version}"
|
||||
);
|
||||
} else {
|
||||
// A dependency from the root package or requirements.txt.
|
||||
debug!("Adding direct dependency: {package}{version}");
|
||||
|
@ -2301,7 +2293,7 @@ impl ForkState {
|
|||
|
||||
self.pubgrub.add_package_version_dependencies(
|
||||
self.next.clone(),
|
||||
version.clone(),
|
||||
for_version.clone(),
|
||||
dependencies.into_iter().map(|dependency| {
|
||||
let PubGrubDependency {
|
||||
package,
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
use std::collections::btree_map::Entry;
|
||||
use std::collections::BTreeMap;
|
||||
use std::str::FromStr;
|
||||
|
||||
|
@ -102,6 +103,13 @@ impl FlatDependencyGroups {
|
|||
) -> Option<&Vec<uv_pep508::Requirement<VerbatimParsedUrl>>> {
|
||||
self.0.get(group)
|
||||
}
|
||||
|
||||
pub fn entry(
|
||||
&mut self,
|
||||
group: GroupName,
|
||||
) -> Entry<GroupName, Vec<uv_pep508::Requirement<VerbatimParsedUrl>>> {
|
||||
self.0.entry(group)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoIterator for FlatDependencyGroups {
|
||||
|
|
|
@ -729,6 +729,12 @@ impl Sources {
|
|||
}
|
||||
}
|
||||
|
||||
impl FromIterator<Source> for Sources {
|
||||
fn from_iter<T: IntoIterator<Item = Source>>(iter: T) -> Self {
|
||||
Self(iter.into_iter().collect())
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoIterator for Sources {
|
||||
type Item = Source;
|
||||
type IntoIter = std::vec::IntoIter<Source>;
|
||||
|
@ -792,13 +798,17 @@ impl TryFrom<SourcesWire> for Sources {
|
|||
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) {
|
||||
for (lhs, rhs) in sources.iter().zip(sources.iter().skip(1)) {
|
||||
if lhs.extra() != rhs.extra() {
|
||||
continue;
|
||||
};
|
||||
if lhs.group() != rhs.group() {
|
||||
continue;
|
||||
};
|
||||
|
||||
let lhs = lhs.marker();
|
||||
let rhs = rhs.marker();
|
||||
if !lhs.is_disjoint(rhs) {
|
||||
let Some(left) = lhs.contents().map(|contents| contents.to_string()) else {
|
||||
return Err(SourceError::MissingMarkers);
|
||||
};
|
||||
|
@ -856,6 +866,8 @@ pub enum Source {
|
|||
default
|
||||
)]
|
||||
marker: MarkerTree,
|
||||
extra: Option<ExtraName>,
|
||||
group: Option<GroupName>,
|
||||
},
|
||||
/// A remote `http://` or `https://` URL, either a wheel (`.whl`) or a source distribution
|
||||
/// (`.zip`, `.tar.gz`).
|
||||
|
@ -875,6 +887,8 @@ pub enum Source {
|
|||
default
|
||||
)]
|
||||
marker: MarkerTree,
|
||||
extra: Option<ExtraName>,
|
||||
group: Option<GroupName>,
|
||||
},
|
||||
/// 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
|
||||
|
@ -889,6 +903,8 @@ pub enum Source {
|
|||
default
|
||||
)]
|
||||
marker: MarkerTree,
|
||||
extra: Option<ExtraName>,
|
||||
group: Option<GroupName>,
|
||||
},
|
||||
/// A dependency pinned to a specific index, e.g., `torch` after setting `torch` to `https://download.pytorch.org/whl/cu118`.
|
||||
Registry {
|
||||
|
@ -899,6 +915,8 @@ pub enum Source {
|
|||
default
|
||||
)]
|
||||
marker: MarkerTree,
|
||||
extra: Option<ExtraName>,
|
||||
group: Option<GroupName>,
|
||||
},
|
||||
/// A dependency on another package in the workspace.
|
||||
Workspace {
|
||||
|
@ -911,6 +929,8 @@ pub enum Source {
|
|||
default
|
||||
)]
|
||||
marker: MarkerTree,
|
||||
extra: Option<ExtraName>,
|
||||
group: Option<GroupName>,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -940,6 +960,8 @@ impl<'de> Deserialize<'de> for Source {
|
|||
default
|
||||
)]
|
||||
marker: MarkerTree,
|
||||
extra: Option<ExtraName>,
|
||||
group: Option<GroupName>,
|
||||
}
|
||||
|
||||
// Attempt to deserialize as `CatchAll`.
|
||||
|
@ -955,8 +977,17 @@ impl<'de> Deserialize<'de> for Source {
|
|||
index,
|
||||
workspace,
|
||||
marker,
|
||||
extra,
|
||||
group,
|
||||
} = CatchAll::deserialize(deserializer)?;
|
||||
|
||||
// If both `extra` and `group` are set, return an error.
|
||||
if extra.is_some() && group.is_some() {
|
||||
return Err(serde::de::Error::custom(
|
||||
"cannot specify both `extra` and `group`",
|
||||
));
|
||||
}
|
||||
|
||||
// If the `git` field is set, we're dealing with a Git source.
|
||||
if let Some(git) = git {
|
||||
if index.is_some() {
|
||||
|
@ -1012,6 +1043,8 @@ impl<'de> Deserialize<'de> for Source {
|
|||
tag,
|
||||
branch,
|
||||
marker,
|
||||
extra,
|
||||
group,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1062,6 +1095,8 @@ impl<'de> Deserialize<'de> for Source {
|
|||
url,
|
||||
subdirectory,
|
||||
marker,
|
||||
extra,
|
||||
group,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1107,6 +1142,8 @@ impl<'de> Deserialize<'de> for Source {
|
|||
path,
|
||||
editable,
|
||||
marker,
|
||||
extra,
|
||||
group,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1153,7 +1190,12 @@ impl<'de> Deserialize<'de> for Source {
|
|||
));
|
||||
}
|
||||
|
||||
return Ok(Self::Registry { index, marker });
|
||||
return Ok(Self::Registry {
|
||||
index,
|
||||
marker,
|
||||
extra,
|
||||
group,
|
||||
});
|
||||
}
|
||||
|
||||
// If the `workspace` field is set, we're dealing with a workspace source.
|
||||
|
@ -1199,7 +1241,12 @@ impl<'de> Deserialize<'de> for Source {
|
|||
));
|
||||
}
|
||||
|
||||
return Ok(Self::Workspace { workspace, marker });
|
||||
return Ok(Self::Workspace {
|
||||
workspace,
|
||||
marker,
|
||||
extra,
|
||||
group,
|
||||
});
|
||||
}
|
||||
|
||||
// If none of the fields are set, we're dealing with an error.
|
||||
|
@ -1269,6 +1316,8 @@ impl Source {
|
|||
Ok(Some(Source::Workspace {
|
||||
workspace: true,
|
||||
marker: MarkerTree::TRUE,
|
||||
extra: None,
|
||||
group: None,
|
||||
}))
|
||||
}
|
||||
RequirementSource::Url { .. } => {
|
||||
|
@ -1292,6 +1341,8 @@ impl Source {
|
|||
Source::Registry {
|
||||
index,
|
||||
marker: MarkerTree::TRUE,
|
||||
extra: None,
|
||||
group: None,
|
||||
}
|
||||
} else {
|
||||
return Ok(None);
|
||||
|
@ -1306,6 +1357,8 @@ impl Source {
|
|||
.map_err(SourceError::Absolute)?,
|
||||
),
|
||||
marker: MarkerTree::TRUE,
|
||||
extra: None,
|
||||
group: None,
|
||||
},
|
||||
RequirementSource::Url {
|
||||
subdirectory, url, ..
|
||||
|
@ -1313,6 +1366,8 @@ impl Source {
|
|||
url: url.to_url(),
|
||||
subdirectory: subdirectory.map(PortablePathBuf::from),
|
||||
marker: MarkerTree::TRUE,
|
||||
extra: None,
|
||||
group: None,
|
||||
},
|
||||
RequirementSource::Git {
|
||||
repository,
|
||||
|
@ -1338,6 +1393,8 @@ impl Source {
|
|||
git: repository,
|
||||
subdirectory: subdirectory.map(PortablePathBuf::from),
|
||||
marker: MarkerTree::TRUE,
|
||||
extra: None,
|
||||
group: None,
|
||||
}
|
||||
} else {
|
||||
Source::Git {
|
||||
|
@ -1347,6 +1404,8 @@ impl Source {
|
|||
git: repository,
|
||||
subdirectory: subdirectory.map(PortablePathBuf::from),
|
||||
marker: MarkerTree::TRUE,
|
||||
extra: None,
|
||||
group: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1356,13 +1415,35 @@ impl Source {
|
|||
}
|
||||
|
||||
/// Return the [`MarkerTree`] for the source.
|
||||
pub fn marker(&self) -> MarkerTree {
|
||||
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::Git { marker, .. } => marker,
|
||||
Source::Url { marker, .. } => marker,
|
||||
Source::Path { marker, .. } => marker,
|
||||
Source::Registry { marker, .. } => marker,
|
||||
Source::Workspace { marker, .. } => marker,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the extra name for the source.
|
||||
pub fn extra(&self) -> Option<&ExtraName> {
|
||||
match self {
|
||||
Source::Git { extra, .. } => extra.as_ref(),
|
||||
Source::Url { extra, .. } => extra.as_ref(),
|
||||
Source::Path { extra, .. } => extra.as_ref(),
|
||||
Source::Registry { extra, .. } => extra.as_ref(),
|
||||
Source::Workspace { extra, .. } => extra.as_ref(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the dependency group name for the source.
|
||||
pub fn group(&self) -> Option<&GroupName> {
|
||||
match self {
|
||||
Source::Git { group, .. } => group.as_ref(),
|
||||
Source::Url { group, .. } => group.as_ref(),
|
||||
Source::Path { group, .. } => group.as_ref(),
|
||||
Source::Registry { group, .. } => group.as_ref(),
|
||||
Source::Workspace { group, .. } => group.as_ref(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1607,104 +1607,108 @@ mod tests {
|
|||
".workspace.packages.*.pyproject_toml" => "[PYPROJECT_TOML]"
|
||||
},
|
||||
@r###"
|
||||
{
|
||||
"project_root": "[ROOT]/albatross-root-workspace",
|
||||
"project_name": "albatross",
|
||||
"workspace": {
|
||||
"install_path": "[ROOT]/albatross-root-workspace",
|
||||
"packages": {
|
||||
"albatross": {
|
||||
"root": "[ROOT]/albatross-root-workspace",
|
||||
"project": {
|
||||
"name": "albatross",
|
||||
"version": "0.1.0",
|
||||
"requires-python": ">=3.12",
|
||||
"dependencies": [
|
||||
"bird-feeder",
|
||||
"tqdm>=4,<5"
|
||||
],
|
||||
"optional-dependencies": null
|
||||
},
|
||||
"pyproject_toml": "[PYPROJECT_TOML]"
|
||||
},
|
||||
"bird-feeder": {
|
||||
"root": "[ROOT]/albatross-root-workspace/packages/bird-feeder",
|
||||
"project": {
|
||||
"name": "bird-feeder",
|
||||
"version": "1.0.0",
|
||||
"requires-python": ">=3.8",
|
||||
"dependencies": [
|
||||
"anyio>=4.3.0,<5",
|
||||
"seeds"
|
||||
],
|
||||
"optional-dependencies": null
|
||||
},
|
||||
"pyproject_toml": "[PYPROJECT_TOML]"
|
||||
},
|
||||
"seeds": {
|
||||
"root": "[ROOT]/albatross-root-workspace/packages/seeds",
|
||||
"project": {
|
||||
"name": "seeds",
|
||||
"version": "1.0.0",
|
||||
"requires-python": ">=3.12",
|
||||
"dependencies": [
|
||||
"idna==3.6"
|
||||
],
|
||||
"optional-dependencies": null
|
||||
},
|
||||
"pyproject_toml": "[PYPROJECT_TOML]"
|
||||
}
|
||||
},
|
||||
"sources": {
|
||||
"bird-feeder": [
|
||||
{
|
||||
"workspace": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"indexes": [],
|
||||
"pyproject_toml": {
|
||||
"project": {
|
||||
"name": "albatross",
|
||||
"version": "0.1.0",
|
||||
"requires-python": ">=3.12",
|
||||
"dependencies": [
|
||||
"bird-feeder",
|
||||
"tqdm>=4,<5"
|
||||
],
|
||||
"optional-dependencies": null
|
||||
},
|
||||
"tool": {
|
||||
"uv": {
|
||||
"sources": {
|
||||
"bird-feeder": [
|
||||
{
|
||||
"workspace": true
|
||||
}
|
||||
]
|
||||
{
|
||||
"project_root": "[ROOT]/albatross-root-workspace",
|
||||
"project_name": "albatross",
|
||||
"workspace": {
|
||||
"install_path": "[ROOT]/albatross-root-workspace",
|
||||
"packages": {
|
||||
"albatross": {
|
||||
"root": "[ROOT]/albatross-root-workspace",
|
||||
"project": {
|
||||
"name": "albatross",
|
||||
"version": "0.1.0",
|
||||
"requires-python": ">=3.12",
|
||||
"dependencies": [
|
||||
"bird-feeder",
|
||||
"tqdm>=4,<5"
|
||||
],
|
||||
"optional-dependencies": null
|
||||
},
|
||||
"pyproject_toml": "[PYPROJECT_TOML]"
|
||||
},
|
||||
"index": null,
|
||||
"workspace": {
|
||||
"members": [
|
||||
"packages/*"
|
||||
"bird-feeder": {
|
||||
"root": "[ROOT]/albatross-root-workspace/packages/bird-feeder",
|
||||
"project": {
|
||||
"name": "bird-feeder",
|
||||
"version": "1.0.0",
|
||||
"requires-python": ">=3.8",
|
||||
"dependencies": [
|
||||
"anyio>=4.3.0,<5",
|
||||
"seeds"
|
||||
],
|
||||
"optional-dependencies": null
|
||||
},
|
||||
"pyproject_toml": "[PYPROJECT_TOML]"
|
||||
},
|
||||
"seeds": {
|
||||
"root": "[ROOT]/albatross-root-workspace/packages/seeds",
|
||||
"project": {
|
||||
"name": "seeds",
|
||||
"version": "1.0.0",
|
||||
"requires-python": ">=3.12",
|
||||
"dependencies": [
|
||||
"idna==3.6"
|
||||
],
|
||||
"optional-dependencies": null
|
||||
},
|
||||
"pyproject_toml": "[PYPROJECT_TOML]"
|
||||
}
|
||||
},
|
||||
"sources": {
|
||||
"bird-feeder": [
|
||||
{
|
||||
"workspace": true,
|
||||
"extra": null,
|
||||
"group": null
|
||||
}
|
||||
]
|
||||
},
|
||||
"indexes": [],
|
||||
"pyproject_toml": {
|
||||
"project": {
|
||||
"name": "albatross",
|
||||
"version": "0.1.0",
|
||||
"requires-python": ">=3.12",
|
||||
"dependencies": [
|
||||
"bird-feeder",
|
||||
"tqdm>=4,<5"
|
||||
],
|
||||
"exclude": null
|
||||
"optional-dependencies": null
|
||||
},
|
||||
"managed": null,
|
||||
"package": null,
|
||||
"default-groups": null,
|
||||
"dev-dependencies": null,
|
||||
"override-dependencies": null,
|
||||
"constraint-dependencies": null,
|
||||
"environments": null,
|
||||
"conflicts": null
|
||||
"tool": {
|
||||
"uv": {
|
||||
"sources": {
|
||||
"bird-feeder": [
|
||||
{
|
||||
"workspace": true,
|
||||
"extra": null,
|
||||
"group": null
|
||||
}
|
||||
]
|
||||
},
|
||||
"index": null,
|
||||
"workspace": {
|
||||
"members": [
|
||||
"packages/*"
|
||||
],
|
||||
"exclude": null
|
||||
},
|
||||
"managed": null,
|
||||
"package": null,
|
||||
"default-groups": null,
|
||||
"dev-dependencies": null,
|
||||
"override-dependencies": null,
|
||||
"constraint-dependencies": null,
|
||||
"environments": null,
|
||||
"conflicts": null
|
||||
}
|
||||
},
|
||||
"dependency-groups": null
|
||||
}
|
||||
},
|
||||
"dependency-groups": null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"###);
|
||||
"###);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -475,6 +475,8 @@ pub(crate) async fn add(
|
|||
tag,
|
||||
branch,
|
||||
marker,
|
||||
extra,
|
||||
group,
|
||||
}) => {
|
||||
let credentials = uv_auth::Credentials::from_url(&git);
|
||||
if let Some(credentials) = credentials {
|
||||
|
@ -491,6 +493,8 @@ pub(crate) async fn add(
|
|||
tag,
|
||||
branch,
|
||||
marker,
|
||||
extra,
|
||||
group,
|
||||
})
|
||||
}
|
||||
_ => source,
|
||||
|
|
|
@ -142,6 +142,7 @@ pub(crate) async fn install(
|
|||
version.clone(),
|
||||
)),
|
||||
index: None,
|
||||
conflict: None,
|
||||
},
|
||||
origin: None,
|
||||
}
|
||||
|
@ -159,6 +160,7 @@ pub(crate) async fn install(
|
|||
source: RequirementSource::Registry {
|
||||
specifier: VersionSpecifiers::empty(),
|
||||
index: None,
|
||||
conflict: None,
|
||||
},
|
||||
origin: None,
|
||||
}
|
||||
|
|
|
@ -478,6 +478,7 @@ async fn get_or_create_environment(
|
|||
source: RequirementSource::Registry {
|
||||
specifier: VersionSpecifiers::empty(),
|
||||
index: None,
|
||||
conflict: None,
|
||||
},
|
||||
origin: None,
|
||||
},
|
||||
|
@ -491,6 +492,7 @@ async fn get_or_create_environment(
|
|||
version.clone(),
|
||||
)),
|
||||
index: None,
|
||||
conflict: None,
|
||||
},
|
||||
origin: None,
|
||||
},
|
||||
|
@ -502,6 +504,7 @@ async fn get_or_create_environment(
|
|||
source: RequirementSource::Registry {
|
||||
specifier: VersionSpecifiers::empty(),
|
||||
index: None,
|
||||
conflict: None,
|
||||
},
|
||||
origin: None,
|
||||
},
|
||||
|
|
|
@ -15111,8 +15111,8 @@ fn lock_explicit_default_index() -> Result<()> {
|
|||
DEBUG Found static `pyproject.toml` for: project @ file://[TEMP_DIR]/
|
||||
DEBUG No workspace root found, using project root
|
||||
DEBUG Ignoring existing lockfile due to mismatched `requires-dist` for: `project==0.1.0`
|
||||
Expected: {Requirement { name: PackageName("anyio"), extras: [], marker: true, source: Registry { specifier: VersionSpecifiers([]), index: None }, origin: None }}
|
||||
Actual: {Requirement { name: PackageName("iniconfig"), extras: [], marker: true, source: Registry { specifier: VersionSpecifiers([VersionSpecifier { operator: Equal, version: "2.0.0" }]), index: Some(Url { scheme: "https", cannot_be_a_base: false, username: "", password: None, host: Some(Domain("test.pypi.org")), port: None, path: "/simple", query: None, fragment: None }) }, origin: None }}
|
||||
Expected: {Requirement { name: PackageName("anyio"), extras: [], marker: true, source: Registry { specifier: VersionSpecifiers([]), index: None, conflict: None }, origin: None }}
|
||||
Actual: {Requirement { name: PackageName("iniconfig"), extras: [], marker: true, source: Registry { specifier: VersionSpecifiers([VersionSpecifier { operator: Equal, version: "2.0.0" }]), index: Some(Url { scheme: "https", cannot_be_a_base: false, username: "", password: None, host: Some(Domain("test.pypi.org")), port: None, path: "/simple", query: None, fragment: None }), conflict: None }, origin: None }}
|
||||
DEBUG Solving with installed Python version: 3.12.[X]
|
||||
DEBUG Solving with target Python version: >=3.12
|
||||
DEBUG Adding direct dependency: project*
|
||||
|
@ -18075,7 +18075,7 @@ fn lock_multiple_sources_no_marker() -> Result<()> {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn lock_multiple_sources_index() -> Result<()> {
|
||||
fn lock_multiple_sources_index_disjoint_markers() -> Result<()> {
|
||||
let context = TestContext::new("3.12");
|
||||
|
||||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||||
|
@ -18789,6 +18789,867 @@ fn lock_multiple_sources_extra() -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lock_multiple_sources_index_disjoint_extras() -> 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 = []
|
||||
|
||||
[project.optional-dependencies]
|
||||
cu118 = ["jinja2==3.1.2"]
|
||||
cu124 = ["jinja2==3.1.3"]
|
||||
|
||||
[tool.uv]
|
||||
constraint-dependencies = ["markupsafe<3"]
|
||||
conflicts = [
|
||||
[
|
||||
{ extra = "cu118" },
|
||||
{ extra = "cu124" },
|
||||
],
|
||||
]
|
||||
|
||||
[tool.uv.sources]
|
||||
jinja2 = [
|
||||
{ index = "torch-cu118", extra = "cu118" },
|
||||
{ index = "torch-cu124", extra = "cu124" },
|
||||
]
|
||||
|
||||
[[tool.uv.index]]
|
||||
name = "torch-cu118"
|
||||
url = "https://download.pytorch.org/whl/cu118"
|
||||
explicit = true
|
||||
|
||||
[[tool.uv.index]]
|
||||
name = "torch-cu124"
|
||||
url = "https://download.pytorch.org/whl/cu124"
|
||||
explicit = true
|
||||
"#,
|
||||
)?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.lock().env_remove(EnvVars::UV_EXCLUDE_NEWER), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 4 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 = [
|
||||
]
|
||||
conflicts = [[
|
||||
{ package = "project", extra = "cu118" },
|
||||
{ package = "project", extra = "cu124" },
|
||||
]]
|
||||
|
||||
[manifest]
|
||||
constraints = [{ name = "markupsafe", specifier = "<3" }]
|
||||
|
||||
[[package]]
|
||||
name = "jinja2"
|
||||
version = "3.1.2"
|
||||
source = { registry = "https://download.pytorch.org/whl/cu118" }
|
||||
resolution-markers = [
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "markupsafe" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://download.pytorch.org/whl/Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jinja2"
|
||||
version = "3.1.3"
|
||||
source = { registry = "https://download.pytorch.org/whl/cu124" }
|
||||
resolution-markers = [
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "markupsafe" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://download.pytorch.org/whl/Jinja2-3.1.3-py3-none-any.whl", hash = "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markupsafe"
|
||||
version = "2.1.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/87/5b/aae44c6655f3801e81aa3eef09dbbf012431987ba564d7231722f68df02d/MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b", size = 19384 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/53/bd/583bf3e4c8d6a321938c13f49d44024dbe5ed63e0a7ba127e454a66da974/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1", size = 18215 },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/d6/e7cd795fc710292c3af3a06d80868ce4b02bfbbf370b7cee11d282815a2a/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4", size = 14069 },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/b5/5d8ec796e2a08fc814a2c7d2584b55f889a55cf17dd1a90f2beb70744e5c/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee", size = 29452 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/0d/2454f072fae3b5a137c119abf15465d1771319dfe9e4acbb31722a0fff91/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5", size = 28462 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/75/fd6cb2e68780f72d47e6671840ca517bda5ef663d30ada7616b0462ad1e3/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b", size = 27869 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/81/147c477391c2750e8fc7705829f7351cf1cd3be64406edcf900dc633feb2/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a", size = 33906 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/ff/9a52b71839d7a256b563e85d11050e307121000dcebc97df120176b3ad93/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f", size = 32296 },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/07/2dc76aa51b481eb96a4c3198894f38b480490e834479611a4053fbf08623/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169", size = 33038 },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/0c/620c1fb3661858c0e37eb3cbffd8c6f732a67cd97296f725789679801b31/MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad", size = 16572 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/14/c3554d512d5f9100a95e737502f4a2323a1959f6d0d01e0d0997b35f7b10/MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb", size = 17127 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
|
||||
[package.optional-dependencies]
|
||||
cu118 = [
|
||||
{ name = "jinja2", version = "3.1.2", source = { registry = "https://download.pytorch.org/whl/cu118" } },
|
||||
]
|
||||
cu124 = [
|
||||
{ name = "jinja2", version = "3.1.3", source = { registry = "https://download.pytorch.org/whl/cu124" } },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "jinja2", marker = "extra == 'cu118'", specifier = "==3.1.2", index = "https://download.pytorch.org/whl/cu118", conflict = { package = "project", extra = "cu118" } },
|
||||
{ name = "jinja2", marker = "extra == 'cu124'", specifier = "==3.1.3", index = "https://download.pytorch.org/whl/cu124", conflict = { package = "project", extra = "cu124" } },
|
||||
]
|
||||
"###
|
||||
);
|
||||
});
|
||||
|
||||
// Re-run with `--locked`.
|
||||
uv_snapshot!(context.filters(), context.lock().arg("--locked").env_remove(EnvVars::UV_EXCLUDE_NEWER), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 4 packages in [TIME]
|
||||
"###);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lock_multiple_sources_index_disjoint_groups() -> 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 = []
|
||||
|
||||
[dependency-groups]
|
||||
cu118 = ["jinja2==3.1.2"]
|
||||
cu124 = ["jinja2==3.1.3"]
|
||||
|
||||
[tool.uv]
|
||||
constraint-dependencies = ["markupsafe<3"]
|
||||
conflicts = [
|
||||
[
|
||||
{ group = "cu118" },
|
||||
{ group = "cu124" },
|
||||
],
|
||||
]
|
||||
|
||||
[tool.uv.sources]
|
||||
jinja2 = [
|
||||
{ index = "torch-cu118", group = "cu118" },
|
||||
{ index = "torch-cu124", group = "cu124" },
|
||||
]
|
||||
|
||||
[[tool.uv.index]]
|
||||
name = "torch-cu118"
|
||||
url = "https://download.pytorch.org/whl/cu118"
|
||||
explicit = true
|
||||
|
||||
[[tool.uv.index]]
|
||||
name = "torch-cu124"
|
||||
url = "https://download.pytorch.org/whl/cu124"
|
||||
explicit = true
|
||||
"#,
|
||||
)?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.lock().env_remove(EnvVars::UV_EXCLUDE_NEWER), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 4 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 = [
|
||||
]
|
||||
conflicts = [[
|
||||
{ package = "project", group = "cu118" },
|
||||
{ package = "project", group = "cu124" },
|
||||
]]
|
||||
|
||||
[manifest]
|
||||
constraints = [{ name = "markupsafe", specifier = "<3" }]
|
||||
|
||||
[[package]]
|
||||
name = "jinja2"
|
||||
version = "3.1.2"
|
||||
source = { registry = "https://download.pytorch.org/whl/cu118" }
|
||||
resolution-markers = [
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "markupsafe" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://download.pytorch.org/whl/Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jinja2"
|
||||
version = "3.1.3"
|
||||
source = { registry = "https://download.pytorch.org/whl/cu124" }
|
||||
resolution-markers = [
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "markupsafe" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://download.pytorch.org/whl/Jinja2-3.1.3-py3-none-any.whl", hash = "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markupsafe"
|
||||
version = "2.1.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/87/5b/aae44c6655f3801e81aa3eef09dbbf012431987ba564d7231722f68df02d/MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b", size = 19384 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/53/bd/583bf3e4c8d6a321938c13f49d44024dbe5ed63e0a7ba127e454a66da974/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1", size = 18215 },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/d6/e7cd795fc710292c3af3a06d80868ce4b02bfbbf370b7cee11d282815a2a/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4", size = 14069 },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/b5/5d8ec796e2a08fc814a2c7d2584b55f889a55cf17dd1a90f2beb70744e5c/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee", size = 29452 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/0d/2454f072fae3b5a137c119abf15465d1771319dfe9e4acbb31722a0fff91/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5", size = 28462 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/75/fd6cb2e68780f72d47e6671840ca517bda5ef663d30ada7616b0462ad1e3/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b", size = 27869 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/81/147c477391c2750e8fc7705829f7351cf1cd3be64406edcf900dc633feb2/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a", size = 33906 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/ff/9a52b71839d7a256b563e85d11050e307121000dcebc97df120176b3ad93/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f", size = 32296 },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/07/2dc76aa51b481eb96a4c3198894f38b480490e834479611a4053fbf08623/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169", size = 33038 },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/0c/620c1fb3661858c0e37eb3cbffd8c6f732a67cd97296f725789679801b31/MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad", size = 16572 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/14/c3554d512d5f9100a95e737502f4a2323a1959f6d0d01e0d0997b35f7b10/MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb", size = 17127 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
|
||||
[package.dev-dependencies]
|
||||
cu118 = [
|
||||
{ name = "jinja2", version = "3.1.2", source = { registry = "https://download.pytorch.org/whl/cu118" } },
|
||||
]
|
||||
cu124 = [
|
||||
{ name = "jinja2", version = "3.1.3", source = { registry = "https://download.pytorch.org/whl/cu124" } },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
cu118 = [{ name = "jinja2", specifier = "==3.1.2", index = "https://download.pytorch.org/whl/cu118", conflict = { package = "project", group = "cu118" } }]
|
||||
cu124 = [{ name = "jinja2", specifier = "==3.1.3", index = "https://download.pytorch.org/whl/cu124", conflict = { package = "project", group = "cu124" } }]
|
||||
"###
|
||||
);
|
||||
});
|
||||
|
||||
// Re-run with `--locked`.
|
||||
uv_snapshot!(context.filters(), context.lock().arg("--locked").env_remove(EnvVars::UV_EXCLUDE_NEWER), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 4 packages in [TIME]
|
||||
"###);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lock_multiple_sources_index_disjoint_extras_with_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 = []
|
||||
|
||||
[project.optional-dependencies]
|
||||
cu118 = ["jinja2[i18n]==3.1.2"]
|
||||
cu124 = ["jinja2[i18n]==3.1.3"]
|
||||
|
||||
[tool.uv]
|
||||
constraint-dependencies = ["markupsafe<3"]
|
||||
conflicts = [
|
||||
[
|
||||
{ extra = "cu118" },
|
||||
{ extra = "cu124" },
|
||||
],
|
||||
]
|
||||
|
||||
[tool.uv.sources]
|
||||
jinja2 = [
|
||||
{ index = "torch-cu118", extra = "cu118" },
|
||||
{ index = "torch-cu124", extra = "cu124" },
|
||||
]
|
||||
|
||||
[[tool.uv.index]]
|
||||
name = "torch-cu118"
|
||||
url = "https://download.pytorch.org/whl/cu118"
|
||||
explicit = true
|
||||
|
||||
[[tool.uv.index]]
|
||||
name = "torch-cu124"
|
||||
url = "https://download.pytorch.org/whl/cu124"
|
||||
explicit = true
|
||||
"#,
|
||||
)?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.lock().env_remove(EnvVars::UV_EXCLUDE_NEWER), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 5 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 = [
|
||||
]
|
||||
conflicts = [[
|
||||
{ package = "project", extra = "cu118" },
|
||||
{ package = "project", extra = "cu124" },
|
||||
]]
|
||||
|
||||
[manifest]
|
||||
constraints = [{ name = "markupsafe", specifier = "<3" }]
|
||||
|
||||
[[package]]
|
||||
name = "babel"
|
||||
version = "2.16.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/2a/74/f1bc80f23eeba13393b7222b11d95ca3af2c1e28edca18af487137eefed9/babel-2.16.0.tar.gz", hash = "sha256:d1f3554ca26605fe173f3de0c65f750f5a42f924499bf134de6423582298e316", size = 9348104 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/20/bc79bc575ba2e2a7f70e8a1155618bb1301eaa5132a8271373a6903f73f8/babel-2.16.0-py3-none-any.whl", hash = "sha256:368b5b98b37c06b7daf6696391c3240c938b37767d4584413e8438c5c435fa8b", size = 9587599 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jinja2"
|
||||
version = "3.1.2"
|
||||
source = { registry = "https://download.pytorch.org/whl/cu118" }
|
||||
resolution-markers = [
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "markupsafe" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://download.pytorch.org/whl/Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
i18n = [
|
||||
{ name = "babel" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jinja2"
|
||||
version = "3.1.3"
|
||||
source = { registry = "https://download.pytorch.org/whl/cu124" }
|
||||
resolution-markers = [
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "markupsafe" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://download.pytorch.org/whl/Jinja2-3.1.3-py3-none-any.whl", hash = "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
i18n = [
|
||||
{ name = "babel" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markupsafe"
|
||||
version = "2.1.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/87/5b/aae44c6655f3801e81aa3eef09dbbf012431987ba564d7231722f68df02d/MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b", size = 19384 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/53/bd/583bf3e4c8d6a321938c13f49d44024dbe5ed63e0a7ba127e454a66da974/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1", size = 18215 },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/d6/e7cd795fc710292c3af3a06d80868ce4b02bfbbf370b7cee11d282815a2a/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4", size = 14069 },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/b5/5d8ec796e2a08fc814a2c7d2584b55f889a55cf17dd1a90f2beb70744e5c/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee", size = 29452 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/0d/2454f072fae3b5a137c119abf15465d1771319dfe9e4acbb31722a0fff91/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5", size = 28462 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/75/fd6cb2e68780f72d47e6671840ca517bda5ef663d30ada7616b0462ad1e3/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b", size = 27869 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/81/147c477391c2750e8fc7705829f7351cf1cd3be64406edcf900dc633feb2/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a", size = 33906 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/ff/9a52b71839d7a256b563e85d11050e307121000dcebc97df120176b3ad93/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f", size = 32296 },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/07/2dc76aa51b481eb96a4c3198894f38b480490e834479611a4053fbf08623/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169", size = 33038 },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/0c/620c1fb3661858c0e37eb3cbffd8c6f732a67cd97296f725789679801b31/MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad", size = 16572 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/14/c3554d512d5f9100a95e737502f4a2323a1959f6d0d01e0d0997b35f7b10/MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb", size = 17127 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
|
||||
[package.optional-dependencies]
|
||||
cu118 = [
|
||||
{ name = "jinja2", version = "3.1.2", source = { registry = "https://download.pytorch.org/whl/cu118" }, extra = ["i18n"] },
|
||||
]
|
||||
cu124 = [
|
||||
{ name = "jinja2", version = "3.1.3", source = { registry = "https://download.pytorch.org/whl/cu124" }, extra = ["i18n"] },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "jinja2", extras = ["i18n"], marker = "extra == 'cu118'", specifier = "==3.1.2", index = "https://download.pytorch.org/whl/cu118", conflict = { package = "project", extra = "cu118" } },
|
||||
{ name = "jinja2", extras = ["i18n"], marker = "extra == 'cu124'", specifier = "==3.1.3", index = "https://download.pytorch.org/whl/cu124", conflict = { package = "project", extra = "cu124" } },
|
||||
]
|
||||
"###
|
||||
);
|
||||
});
|
||||
|
||||
// Re-run with `--locked`.
|
||||
uv_snapshot!(context.filters(), context.lock().arg("--locked").env_remove(EnvVars::UV_EXCLUDE_NEWER), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 5 packages in [TIME]
|
||||
"###);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lock_multiple_sources_index_overlapping_extras() -> 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 = []
|
||||
|
||||
[project.optional-dependencies]
|
||||
cu118 = ["jinja2==3.1.2"]
|
||||
cu124 = ["jinja2==3.1.3"]
|
||||
|
||||
[tool.uv]
|
||||
constraint-dependencies = ["markupsafe<3"]
|
||||
|
||||
[tool.uv.sources]
|
||||
jinja2 = [
|
||||
{ index = "torch-cu118", extra = "cu118" },
|
||||
{ index = "torch-cu124", extra = "cu124" },
|
||||
]
|
||||
|
||||
[[tool.uv.index]]
|
||||
name = "torch-cu118"
|
||||
url = "https://download.pytorch.org/whl/cu118"
|
||||
explicit = true
|
||||
|
||||
[[tool.uv.index]]
|
||||
name = "torch-cu124"
|
||||
url = "https://download.pytorch.org/whl/cu124"
|
||||
explicit = true
|
||||
"#,
|
||||
)?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.lock().env_remove(EnvVars::UV_EXCLUDE_NEWER), @r###"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
error: Requirements contain conflicting indexes for package `jinja2` in all marker environments:
|
||||
- https://download.pytorch.org/whl/cu118
|
||||
- https://download.pytorch.org/whl/cu124
|
||||
"###);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lock_multiple_sources_index_disjoint_extras_with_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 = []
|
||||
|
||||
[project.optional-dependencies]
|
||||
cu118 = ["jinja2==3.1.2"]
|
||||
cu124 = ["jinja2==3.1.3"]
|
||||
|
||||
[tool.uv]
|
||||
constraint-dependencies = ["markupsafe<3"]
|
||||
conflicts = [
|
||||
[
|
||||
{ extra = "cu118" },
|
||||
{ extra = "cu124" },
|
||||
],
|
||||
]
|
||||
|
||||
[tool.uv.sources]
|
||||
jinja2 = [
|
||||
{ index = "torch-cu118", extra = "cu118", marker = "sys_platform == 'darwin'" },
|
||||
{ index = "torch-cu124", extra = "cu124" },
|
||||
]
|
||||
|
||||
[[tool.uv.index]]
|
||||
name = "torch-cu118"
|
||||
url = "https://download.pytorch.org/whl/cu118"
|
||||
explicit = true
|
||||
|
||||
[[tool.uv.index]]
|
||||
name = "torch-cu124"
|
||||
url = "https://download.pytorch.org/whl/cu124"
|
||||
explicit = true
|
||||
"#,
|
||||
)?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.lock().env_remove(EnvVars::UV_EXCLUDE_NEWER), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 5 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'",
|
||||
]
|
||||
conflicts = [[
|
||||
{ package = "project", extra = "cu118" },
|
||||
{ package = "project", extra = "cu124" },
|
||||
]]
|
||||
|
||||
[manifest]
|
||||
constraints = [{ name = "markupsafe", specifier = "<3" }]
|
||||
|
||||
[[package]]
|
||||
name = "jinja2"
|
||||
version = "3.1.2"
|
||||
source = { registry = "https://download.pytorch.org/whl/cu118" }
|
||||
resolution-markers = [
|
||||
"sys_platform == 'darwin'",
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "markupsafe", marker = "sys_platform == 'darwin'" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://download.pytorch.org/whl/Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jinja2"
|
||||
version = "3.1.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"sys_platform != 'darwin'",
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "markupsafe", marker = "sys_platform != 'darwin'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/7a/ff/75c28576a1d900e87eb6335b063fab47a8ef3c8b4d88524c4bf78f670cce/Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852", size = 268239 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/c3/f068337a370801f372f2f8f6bad74a5c140f6fda3d9de154052708dd3c65/Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61", size = 133101 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jinja2"
|
||||
version = "3.1.3"
|
||||
source = { registry = "https://download.pytorch.org/whl/cu124" }
|
||||
resolution-markers = [
|
||||
"sys_platform == 'darwin'",
|
||||
"sys_platform != 'darwin'",
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "markupsafe" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://download.pytorch.org/whl/Jinja2-3.1.3-py3-none-any.whl", hash = "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markupsafe"
|
||||
version = "2.1.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/87/5b/aae44c6655f3801e81aa3eef09dbbf012431987ba564d7231722f68df02d/MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b", size = 19384 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/53/bd/583bf3e4c8d6a321938c13f49d44024dbe5ed63e0a7ba127e454a66da974/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1", size = 18215 },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/d6/e7cd795fc710292c3af3a06d80868ce4b02bfbbf370b7cee11d282815a2a/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4", size = 14069 },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/b5/5d8ec796e2a08fc814a2c7d2584b55f889a55cf17dd1a90f2beb70744e5c/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee", size = 29452 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/0d/2454f072fae3b5a137c119abf15465d1771319dfe9e4acbb31722a0fff91/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5", size = 28462 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/75/fd6cb2e68780f72d47e6671840ca517bda5ef663d30ada7616b0462ad1e3/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b", size = 27869 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/81/147c477391c2750e8fc7705829f7351cf1cd3be64406edcf900dc633feb2/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a", size = 33906 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/ff/9a52b71839d7a256b563e85d11050e307121000dcebc97df120176b3ad93/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f", size = 32296 },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/07/2dc76aa51b481eb96a4c3198894f38b480490e834479611a4053fbf08623/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169", size = 33038 },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/0c/620c1fb3661858c0e37eb3cbffd8c6f732a67cd97296f725789679801b31/MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad", size = 16572 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/14/c3554d512d5f9100a95e737502f4a2323a1959f6d0d01e0d0997b35f7b10/MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb", size = 17127 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
|
||||
[package.optional-dependencies]
|
||||
cu118 = [
|
||||
{ name = "jinja2", version = "3.1.2", source = { registry = "https://download.pytorch.org/whl/cu118" }, marker = "sys_platform == 'darwin'" },
|
||||
{ name = "jinja2", version = "3.1.2", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'darwin'" },
|
||||
]
|
||||
cu124 = [
|
||||
{ name = "jinja2", version = "3.1.3", source = { registry = "https://download.pytorch.org/whl/cu124" } },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "jinja2", marker = "sys_platform == 'darwin' and extra == 'cu118'", specifier = "==3.1.2", index = "https://download.pytorch.org/whl/cu118", conflict = { package = "project", extra = "cu118" } },
|
||||
{ name = "jinja2", marker = "sys_platform != 'darwin' and extra == 'cu118'", specifier = "==3.1.2" },
|
||||
{ name = "jinja2", marker = "extra == 'cu124'", specifier = "==3.1.3", index = "https://download.pytorch.org/whl/cu124", conflict = { package = "project", extra = "cu124" } },
|
||||
]
|
||||
"###
|
||||
);
|
||||
});
|
||||
|
||||
// Re-run with `--locked`.
|
||||
uv_snapshot!(context.filters(), context.lock().arg("--locked").env_remove(EnvVars::UV_EXCLUDE_NEWER), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 5 packages in [TIME]
|
||||
"###);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sources will be ignored when an `extra` is applied, but references a non-existent extra.
|
||||
#[test]
|
||||
fn lock_multiple_index_with_missing_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 = ["jinja2"]
|
||||
|
||||
[tool.uv.sources]
|
||||
jinja2 = [
|
||||
{ index = "torch-cu118", extra = "cu118" },
|
||||
]
|
||||
|
||||
[[tool.uv.index]]
|
||||
name = "torch-cu118"
|
||||
url = "https://download.pytorch.org/whl/cu118"
|
||||
explicit = true
|
||||
"#,
|
||||
)?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.lock().env_remove(EnvVars::UV_EXCLUDE_NEWER), @r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
× Failed to build `project @ file://[TEMP_DIR]/`
|
||||
╰─▶ Source entry for `jinja2` only applies to extra `cu118`, but the `cu118` extra does not exist. When an extra is present on a source (e.g., `extra = "cu118"`), the relevant package must be included in the `project.optional-dependencies` section for that extra (e.g., `project.optional-dependencies = { "cu118" = ["jinja2"] }`).
|
||||
"###);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sources will be ignored when an `extra` is applied, but the dependency isn't in an optional
|
||||
/// group.
|
||||
#[test]
|
||||
fn lock_multiple_index_with_absent_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 = ["jinja2>=3"]
|
||||
|
||||
[project.optional-dependencies]
|
||||
cu118 = []
|
||||
|
||||
[tool.uv.sources]
|
||||
jinja2 = [
|
||||
{ index = "torch-cu118", extra = "cu118" },
|
||||
]
|
||||
|
||||
[[tool.uv.index]]
|
||||
name = "torch-cu118"
|
||||
url = "https://download.pytorch.org/whl/cu118"
|
||||
explicit = true
|
||||
"#,
|
||||
)?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.lock().env_remove(EnvVars::UV_EXCLUDE_NEWER), @r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
× Failed to build `project @ file://[TEMP_DIR]/`
|
||||
╰─▶ Source entry for `jinja2` only applies to extra `cu118`, but `jinja2` was not found under the `project.optional-dependencies` section for that extra. When an extra is present on a source (e.g., `extra = "cu118"`), the relevant package must be included in the `project.optional-dependencies` section for that extra (e.g., `project.optional-dependencies = { "cu118" = ["jinja2"] }`).
|
||||
"###);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sources will be ignored when a `group` is applied, but references a non-existent group.
|
||||
#[test]
|
||||
fn lock_multiple_index_with_missing_group() -> 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 = ["jinja2"]
|
||||
|
||||
[tool.uv.sources]
|
||||
jinja2 = [
|
||||
{ index = "torch-cu118", group = "cu118" },
|
||||
]
|
||||
|
||||
[[tool.uv.index]]
|
||||
name = "torch-cu118"
|
||||
url = "https://download.pytorch.org/whl/cu118"
|
||||
explicit = true
|
||||
"#,
|
||||
)?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.lock().env_remove(EnvVars::UV_EXCLUDE_NEWER), @r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
× Failed to build `project @ file://[TEMP_DIR]/`
|
||||
╰─▶ Source entry for `jinja2` only applies to dependency group `cu118`, but the `cu118` group does not exist. When a group is present on a source (e.g., `group = "cu118"`), the relevant package must be included in the `dependency-groups` section for that extra (e.g., `dependency-groups = { "cu118" = ["jinja2"] }`).
|
||||
"###);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sources will be ignored when a `group` is applied, but the dependency isn't in a dependency
|
||||
/// group.
|
||||
#[test]
|
||||
fn lock_multiple_index_with_absent_group() -> 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 = ["jinja2>=3"]
|
||||
|
||||
[dependency-groups]
|
||||
cu118 = []
|
||||
|
||||
[tool.uv.sources]
|
||||
jinja2 = [
|
||||
{ index = "torch-cu118", group = "cu118" },
|
||||
]
|
||||
|
||||
[[tool.uv.index]]
|
||||
name = "torch-cu118"
|
||||
url = "https://download.pytorch.org/whl/cu118"
|
||||
explicit = true
|
||||
"#,
|
||||
)?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.lock().env_remove(EnvVars::UV_EXCLUDE_NEWER), @r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
× Failed to build `project @ file://[TEMP_DIR]/`
|
||||
╰─▶ Source entry for `jinja2` only applies to dependency group `cu118`, but `jinja2` was not found under the `dependency-groups` section for that group. When a group is present on a source (e.g., `group = "cu118"`), the relevant package must be included in the `dependency-groups` section for that extra (e.g., `dependency-groups = { "cu118" = ["jinja2"] }`).
|
||||
"###);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lock_dry_run() -> Result<()> {
|
||||
let context = TestContext::new("3.12");
|
||||
|
|
|
@ -4277,6 +4277,73 @@ fn sync_all_groups() -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sync_multiple_sources_index_disjoint_extras() -> 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 = []
|
||||
|
||||
[project.optional-dependencies]
|
||||
cu118 = ["jinja2==3.1.2"]
|
||||
cu124 = ["jinja2==3.1.3"]
|
||||
|
||||
[tool.uv]
|
||||
constraint-dependencies = ["markupsafe<3"]
|
||||
conflicts = [
|
||||
[
|
||||
{ extra = "cu118" },
|
||||
{ extra = "cu124" },
|
||||
],
|
||||
]
|
||||
|
||||
[tool.uv.sources]
|
||||
jinja2 = [
|
||||
{ index = "torch-cu118", extra = "cu118" },
|
||||
{ index = "torch-cu124", extra = "cu124" },
|
||||
]
|
||||
|
||||
[[tool.uv.index]]
|
||||
name = "torch-cu118"
|
||||
url = "https://download.pytorch.org/whl/cu118"
|
||||
explicit = true
|
||||
|
||||
[[tool.uv.index]]
|
||||
name = "torch-cu124"
|
||||
url = "https://download.pytorch.org/whl/cu124"
|
||||
explicit = true
|
||||
"#,
|
||||
)?;
|
||||
|
||||
// Generate a lockfile.
|
||||
context
|
||||
.lock()
|
||||
.env_remove(EnvVars::UV_EXCLUDE_NEWER)
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
uv_snapshot!(context.filters(), context.sync().arg("--extra").arg("cu124").env_remove(EnvVars::UV_EXCLUDE_NEWER), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 4 packages in [TIME]
|
||||
Prepared 2 packages in [TIME]
|
||||
Installed 2 packages in [TIME]
|
||||
+ jinja2==3.1.3
|
||||
+ markupsafe==2.1.5
|
||||
"###);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sync_derivation_chain() -> Result<()> {
|
||||
let context = TestContext::new("3.12");
|
||||
|
|
|
@ -335,18 +335,17 @@ dependencies = ["torch"]
|
|||
|
||||
[tool.uv.sources]
|
||||
torch = [
|
||||
{ index = "torch-cu118", marker = "sys_platform == 'darwin'"},
|
||||
{ index = "torch-cu124", marker = "sys_platform != 'darwin'"},
|
||||
{ index = "torch-cpu", marker = "platform_system == 'Darwin'"},
|
||||
{ index = "torch-gpu", marker = "platform_system == 'Linux'"},
|
||||
]
|
||||
|
||||
[[tool.uv.index]]
|
||||
name = "torch-cu118"
|
||||
url = "https://download.pytorch.org/whl/cu118"
|
||||
name = "torch-cpu"
|
||||
url = "https://download.pytorch.org/whl/cpu"
|
||||
|
||||
[[tool.uv.index]]
|
||||
name = "torch-cu124"
|
||||
name = "torch-gpu"
|
||||
url = "https://download.pytorch.org/whl/cu124"
|
||||
|
||||
```
|
||||
|
||||
## Optional dependencies
|
||||
|
@ -394,6 +393,36 @@ $ uv add httpx --optional network
|
|||
If you have optional dependencies that conflict with one another, resolution will fail
|
||||
unless you explicitly [declare them as conflicting](./projects.md#optional-dependencies).
|
||||
|
||||
Sources can also be declared as applying only to a specific optional dependency. For example, to
|
||||
pull `torch` from different PyTorch indexes based on an optional `cpu` or `gpu` extra:
|
||||
|
||||
```toml title="pyproject.toml"
|
||||
[project]
|
||||
dependencies = []
|
||||
|
||||
[project.optional-dependencies]
|
||||
cpu = [
|
||||
"torch",
|
||||
]
|
||||
gpu = [
|
||||
"torch",
|
||||
]
|
||||
|
||||
[tool.uv.sources]
|
||||
torch = [
|
||||
{ index = "torch-cpu", extra = "cpu" },
|
||||
{ index = "torch-gpu", extra = "gpu" },
|
||||
]
|
||||
|
||||
[[tool.uv.index]]
|
||||
name = "torch-cpu"
|
||||
url = "https://download.pytorch.org/whl/cpu"
|
||||
|
||||
[[tool.uv.index]]
|
||||
name = "torch-gpu"
|
||||
url = "https://download.pytorch.org/whl/cu124"
|
||||
```
|
||||
|
||||
## Development dependencies
|
||||
|
||||
Unlike optional dependencies, development dependencies are local-only and will _not_ be included in
|
||||
|
|
100
uv.schema.json
generated
100
uv.schema.json
generated
|
@ -1409,11 +1409,31 @@
|
|||
"null"
|
||||
]
|
||||
},
|
||||
"extra": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ExtraName"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"git": {
|
||||
"description": "The repository URL (without the `git+` prefix).",
|
||||
"type": "string",
|
||||
"format": "uri"
|
||||
},
|
||||
"group": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/GroupName"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"marker": {
|
||||
"$ref": "#/definitions/MarkerTree"
|
||||
},
|
||||
|
@ -1450,6 +1470,26 @@
|
|||
"url"
|
||||
],
|
||||
"properties": {
|
||||
"extra": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ExtraName"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"group": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/GroupName"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"marker": {
|
||||
"$ref": "#/definitions/MarkerTree"
|
||||
},
|
||||
|
@ -1485,6 +1525,26 @@
|
|||
"null"
|
||||
]
|
||||
},
|
||||
"extra": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ExtraName"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"group": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/GroupName"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"marker": {
|
||||
"$ref": "#/definitions/MarkerTree"
|
||||
},
|
||||
|
@ -1501,6 +1561,26 @@
|
|||
"index"
|
||||
],
|
||||
"properties": {
|
||||
"extra": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ExtraName"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"group": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/GroupName"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"index": {
|
||||
"$ref": "#/definitions/IndexName"
|
||||
},
|
||||
|
@ -1517,6 +1597,26 @@
|
|||
"workspace"
|
||||
],
|
||||
"properties": {
|
||||
"extra": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ExtraName"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"group": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/GroupName"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"marker": {
|
||||
"$ref": "#/definitions/MarkerTree"
|
||||
},
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue