mirror of
https://github.com/astral-sh/uv.git
synced 2025-07-07 13:25:00 +00:00
Add [tool.uv.dependency-groups].mygroup.requires-python
(#13735)
This allows you to specify requires-python on individual dependency-groups, with the intended usecase being "oh my dev-dependencies have a higher requires-python than my actual project". This includes a large driveby move of the RequiresPython type to uv-distribution-types to allow us to generate the appropriate markers at this point in the code. It also migrates RequiresPython from pubgrub::Range to version_ranges::Ranges, and makes several pub(crate) items pub, as it's no longer defined in uv_resolver. Fixes #11606
This commit is contained in:
parent
26db29caac
commit
5021840919
54 changed files with 2072 additions and 538 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -6014,6 +6014,7 @@ dependencies = [
|
|||
"tracing",
|
||||
"uv-build-backend",
|
||||
"uv-cache-key",
|
||||
"uv-configuration",
|
||||
"uv-distribution-types",
|
||||
"uv-fs",
|
||||
"uv-git-types",
|
||||
|
|
|
@ -91,7 +91,7 @@ mod resolver {
|
|||
};
|
||||
use uv_dispatch::{BuildDispatch, SharedState};
|
||||
use uv_distribution::DistributionDatabase;
|
||||
use uv_distribution_types::{DependencyMetadata, IndexLocations};
|
||||
use uv_distribution_types::{DependencyMetadata, IndexLocations, RequiresPython};
|
||||
use uv_install_wheel::LinkMode;
|
||||
use uv_pep440::Version;
|
||||
use uv_pep508::{MarkerEnvironment, MarkerEnvironmentBuilder};
|
||||
|
@ -99,8 +99,8 @@ mod resolver {
|
|||
use uv_pypi_types::{Conflicts, ResolverMarkerEnvironment};
|
||||
use uv_python::Interpreter;
|
||||
use uv_resolver::{
|
||||
FlatIndex, InMemoryIndex, Manifest, OptionsBuilder, PythonRequirement, RequiresPython,
|
||||
Resolver, ResolverEnvironment, ResolverOutput,
|
||||
FlatIndex, InMemoryIndex, Manifest, OptionsBuilder, PythonRequirement, Resolver,
|
||||
ResolverEnvironment, ResolverOutput,
|
||||
};
|
||||
use uv_types::{BuildIsolation, EmptyInstalledPackages, HashStrategy};
|
||||
use uv_workspace::WorkspaceCache;
|
||||
|
|
|
@ -295,6 +295,15 @@ pub struct DependencyGroupsWithDefaults {
|
|||
}
|
||||
|
||||
impl DependencyGroupsWithDefaults {
|
||||
/// Do not enable any groups
|
||||
///
|
||||
/// Many places in the code need to know what dependency-groups are active,
|
||||
/// but various commands or subsystems never enable any dependency-groups,
|
||||
/// in which case they want this.
|
||||
pub fn none() -> Self {
|
||||
DependencyGroups::default().with_defaults(DefaultGroups::default())
|
||||
}
|
||||
|
||||
/// Returns `true` if the specification was enabled, and *only* because it was a default
|
||||
pub fn contains_because_default(&self, group: &GroupName) -> bool {
|
||||
self.cur.contains(group) && !self.prev.contains(group)
|
||||
|
|
|
@ -263,6 +263,14 @@ pub struct ExtrasSpecificationWithDefaults {
|
|||
}
|
||||
|
||||
impl ExtrasSpecificationWithDefaults {
|
||||
/// Do not enable any extras
|
||||
///
|
||||
/// Many places in the code need to know what extras are active,
|
||||
/// but various commands or subsystems never enable any extras,
|
||||
/// in which case they want this.
|
||||
pub fn none() -> Self {
|
||||
ExtrasSpecification::default().with_defaults(DefaultExtras::default())
|
||||
}
|
||||
/// Returns `true` if the specification was enabled, and *only* because it was a default
|
||||
pub fn contains_because_default(&self, extra: &ExtraName) -> bool {
|
||||
self.cur.contains(extra) && !self.prev.contains(extra)
|
||||
|
|
|
@ -73,6 +73,7 @@ pub use crate::pip_index::*;
|
|||
pub use crate::prioritized_distribution::*;
|
||||
pub use crate::requested::*;
|
||||
pub use crate::requirement::*;
|
||||
pub use crate::requires_python::*;
|
||||
pub use crate::resolution::*;
|
||||
pub use crate::resolved::*;
|
||||
pub use crate::specified_requirement::*;
|
||||
|
@ -100,6 +101,7 @@ mod pip_index;
|
|||
mod prioritized_distribution;
|
||||
mod requested;
|
||||
mod requirement;
|
||||
mod requires_python;
|
||||
mod resolution;
|
||||
mod resolved;
|
||||
mod specified_requirement;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use std::collections::Bound;
|
||||
|
||||
use pubgrub::Range;
|
||||
use version_ranges::Ranges;
|
||||
|
||||
use uv_distribution_filename::WheelFilename;
|
||||
use uv_pep440::{
|
||||
|
@ -68,7 +68,7 @@ impl RequiresPython {
|
|||
let range = specifiers
|
||||
.into_iter()
|
||||
.map(|specifier| release_specifiers_to_ranges(specifier.clone()))
|
||||
.fold(None, |range: Option<Range<Version>>, requires_python| {
|
||||
.fold(None, |range: Option<Ranges<Version>>, requires_python| {
|
||||
if let Some(range) = range {
|
||||
Some(range.intersection(&requires_python))
|
||||
} else {
|
||||
|
@ -97,12 +97,12 @@ impl RequiresPython {
|
|||
pub fn split(&self, bound: Bound<Version>) -> Option<(Self, Self)> {
|
||||
let RequiresPythonRange(.., upper) = &self.range;
|
||||
|
||||
let upper = Range::from_range_bounds((bound, upper.clone().into()));
|
||||
let upper = Ranges::from_range_bounds((bound, upper.clone().into()));
|
||||
let lower = upper.complement();
|
||||
|
||||
// Intersect left and right with the existing range.
|
||||
let lower = lower.intersection(&Range::from(self.range.clone()));
|
||||
let upper = upper.intersection(&Range::from(self.range.clone()));
|
||||
let lower = lower.intersection(&Ranges::from(self.range.clone()));
|
||||
let upper = upper.intersection(&Ranges::from(self.range.clone()));
|
||||
|
||||
if lower.is_empty() || upper.is_empty() {
|
||||
None
|
||||
|
@ -353,7 +353,7 @@ impl RequiresPython {
|
|||
/// a lock file are deserialized and turned into a `ResolutionGraph`, the
|
||||
/// markers are "complexified" to put the `requires-python` assumption back
|
||||
/// into the marker explicitly.
|
||||
pub(crate) fn simplify_markers(&self, marker: MarkerTree) -> MarkerTree {
|
||||
pub fn simplify_markers(&self, marker: MarkerTree) -> MarkerTree {
|
||||
let (lower, upper) = (self.range().lower(), self.range().upper());
|
||||
marker.simplify_python_versions(lower.as_ref(), upper.as_ref())
|
||||
}
|
||||
|
@ -373,7 +373,7 @@ impl RequiresPython {
|
|||
/// ```text
|
||||
/// python_full_version >= '3.8' and python_full_version < '3.12'
|
||||
/// ```
|
||||
pub(crate) fn complexify_markers(&self, marker: MarkerTree) -> MarkerTree {
|
||||
pub fn complexify_markers(&self, marker: MarkerTree) -> MarkerTree {
|
||||
let (lower, upper) = (self.range().lower(), self.range().upper());
|
||||
marker.complexify_python_versions(lower.as_ref(), upper.as_ref())
|
||||
}
|
||||
|
@ -537,7 +537,7 @@ pub struct RequiresPythonRange(LowerBound, UpperBound);
|
|||
|
||||
impl RequiresPythonRange {
|
||||
/// Initialize a [`RequiresPythonRange`] from a [`Range`].
|
||||
pub fn from_range(range: &Range<Version>) -> Self {
|
||||
pub fn from_range(range: &Ranges<Version>) -> Self {
|
||||
let (lower, upper) = range
|
||||
.bounding_range()
|
||||
.map(|(lower_bound, upper_bound)| (lower_bound.cloned(), upper_bound.cloned()))
|
||||
|
@ -575,9 +575,9 @@ impl Default for RequiresPythonRange {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<RequiresPythonRange> for Range<Version> {
|
||||
impl From<RequiresPythonRange> for Ranges<Version> {
|
||||
fn from(value: RequiresPythonRange) -> Self {
|
||||
Range::from_range_bounds::<(Bound<Version>, Bound<Version>), _>((
|
||||
Ranges::from_range_bounds::<(Bound<Version>, Bound<Version>), _>((
|
||||
value.0.into(),
|
||||
value.1.into(),
|
||||
))
|
||||
|
@ -592,21 +592,18 @@ impl From<RequiresPythonRange> for Range<Version> {
|
|||
/// a simplified marker, one must re-contextualize it by adding the
|
||||
/// `requires-python` constraint back to the marker.
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, PartialOrd, Ord, serde::Deserialize)]
|
||||
pub(crate) struct SimplifiedMarkerTree(MarkerTree);
|
||||
pub struct SimplifiedMarkerTree(MarkerTree);
|
||||
|
||||
impl SimplifiedMarkerTree {
|
||||
/// Simplifies the given markers by assuming the given `requires-python`
|
||||
/// bound is true.
|
||||
pub(crate) fn new(
|
||||
requires_python: &RequiresPython,
|
||||
marker: MarkerTree,
|
||||
) -> SimplifiedMarkerTree {
|
||||
pub fn new(requires_python: &RequiresPython, marker: MarkerTree) -> SimplifiedMarkerTree {
|
||||
SimplifiedMarkerTree(requires_python.simplify_markers(marker))
|
||||
}
|
||||
|
||||
/// Complexifies the given markers by adding the given `requires-python` as
|
||||
/// a constraint to these simplified markers.
|
||||
pub(crate) fn into_marker(self, requires_python: &RequiresPython) -> MarkerTree {
|
||||
pub fn into_marker(self, requires_python: &RequiresPython) -> MarkerTree {
|
||||
requires_python.complexify_markers(self.0)
|
||||
}
|
||||
|
||||
|
@ -614,12 +611,12 @@ impl SimplifiedMarkerTree {
|
|||
///
|
||||
/// This only returns `None` when the underlying marker is always true,
|
||||
/// i.e., it matches all possible marker environments.
|
||||
pub(crate) fn try_to_string(self) -> Option<String> {
|
||||
pub fn try_to_string(self) -> Option<String> {
|
||||
self.0.try_to_string()
|
||||
}
|
||||
|
||||
/// Returns the underlying marker tree without re-complexifying them.
|
||||
pub(crate) fn as_simplified_marker_tree(self) -> MarkerTree {
|
||||
pub fn as_simplified_marker_tree(self) -> MarkerTree {
|
||||
self.0
|
||||
}
|
||||
}
|
|
@ -6,7 +6,7 @@ use rustc_hash::FxHashSet;
|
|||
|
||||
use uv_configuration::SourceStrategy;
|
||||
use uv_distribution_types::{IndexLocations, Requirement};
|
||||
use uv_normalize::{DEV_DEPENDENCIES, ExtraName, GroupName, PackageName};
|
||||
use uv_normalize::{ExtraName, GroupName, PackageName};
|
||||
use uv_pep508::MarkerTree;
|
||||
use uv_workspace::dependency_groups::FlatDependencyGroups;
|
||||
use uv_workspace::pyproject::{Sources, ToolUvSources};
|
||||
|
@ -107,41 +107,10 @@ impl RequiresDist {
|
|||
SourceStrategy::Disabled => &empty,
|
||||
};
|
||||
|
||||
// Collect the dependency groups.
|
||||
let dependency_groups = {
|
||||
// First, collect `tool.uv.dev_dependencies`
|
||||
let dev_dependencies = project_workspace
|
||||
.current_project()
|
||||
.pyproject_toml()
|
||||
.tool
|
||||
.as_ref()
|
||||
.and_then(|tool| tool.uv.as_ref())
|
||||
.and_then(|uv| uv.dev_dependencies.as_ref());
|
||||
|
||||
// Then, collect `dependency-groups`
|
||||
let dependency_groups = project_workspace
|
||||
.current_project()
|
||||
.pyproject_toml()
|
||||
.dependency_groups
|
||||
.iter()
|
||||
.flatten()
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
|
||||
// Flatten the dependency groups.
|
||||
let mut dependency_groups =
|
||||
FlatDependencyGroups::from_dependency_groups(&dependency_groups)
|
||||
.map_err(|err| err.with_dev_dependencies(dev_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());
|
||||
}
|
||||
|
||||
dependency_groups
|
||||
};
|
||||
let dependency_groups = FlatDependencyGroups::from_pyproject_toml(
|
||||
project_workspace.current_project().root(),
|
||||
project_workspace.current_project().pyproject_toml(),
|
||||
)?;
|
||||
|
||||
// Now that we've resolved the dependency groups, we can validate that each source references
|
||||
// a valid extra or group, if present.
|
||||
|
@ -150,9 +119,10 @@ impl RequiresDist {
|
|||
// Lower the dependency groups.
|
||||
let dependency_groups = dependency_groups
|
||||
.into_iter()
|
||||
.map(|(name, requirements)| {
|
||||
.map(|(name, flat_group)| {
|
||||
let requirements = match source_strategy {
|
||||
SourceStrategy::Enabled => requirements
|
||||
SourceStrategy::Enabled => flat_group
|
||||
.requirements
|
||||
.into_iter()
|
||||
.flat_map(|requirement| {
|
||||
let requirement_name = requirement.name.clone();
|
||||
|
@ -182,9 +152,11 @@ impl RequiresDist {
|
|||
)
|
||||
})
|
||||
.collect::<Result<Box<_>, _>>(),
|
||||
SourceStrategy::Disabled => {
|
||||
Ok(requirements.into_iter().map(Requirement::from).collect())
|
||||
}
|
||||
SourceStrategy::Disabled => Ok(flat_group
|
||||
.requirements
|
||||
.into_iter()
|
||||
.map(Requirement::from)
|
||||
.collect()),
|
||||
}?;
|
||||
Ok::<(GroupName, Box<_>), MetadataError>((name, requirements))
|
||||
})
|
||||
|
@ -265,7 +237,7 @@ impl RequiresDist {
|
|||
|
||||
if let Some(group) = source.group() {
|
||||
// If the group doesn't exist at all, error.
|
||||
let Some(dependencies) = dependency_groups.get(group) else {
|
||||
let Some(flat_group) = dependency_groups.get(group) else {
|
||||
return Err(MetadataError::MissingSourceGroup(
|
||||
name.clone(),
|
||||
group.clone(),
|
||||
|
@ -273,7 +245,8 @@ impl RequiresDist {
|
|||
};
|
||||
|
||||
// If there is no such requirement with the group, error.
|
||||
if !dependencies
|
||||
if !flat_group
|
||||
.requirements
|
||||
.iter()
|
||||
.any(|requirement| requirement.name == *name)
|
||||
{
|
||||
|
|
|
@ -14,7 +14,6 @@ pub use options::{Flexibility, Options, OptionsBuilder};
|
|||
pub use preferences::{Preference, PreferenceError, Preferences};
|
||||
pub use prerelease::PrereleaseMode;
|
||||
pub use python_requirement::PythonRequirement;
|
||||
pub use requires_python::{RequiresPython, RequiresPythonRange};
|
||||
pub use resolution::{
|
||||
AnnotationStyle, ConflictingDistributionError, DisplayResolutionGraph, ResolverOutput,
|
||||
};
|
||||
|
@ -58,7 +57,6 @@ mod prerelease;
|
|||
mod pubgrub;
|
||||
mod python_requirement;
|
||||
mod redirect;
|
||||
mod requires_python;
|
||||
mod resolution;
|
||||
mod resolution_mode;
|
||||
mod resolver;
|
||||
|
|
|
@ -23,8 +23,8 @@ use uv_distribution_filename::{
|
|||
use uv_distribution_types::{
|
||||
BuiltDist, DirectUrlBuiltDist, DirectUrlSourceDist, DirectorySourceDist, Dist, Edge,
|
||||
FileLocation, GitSourceDist, IndexUrl, Name, Node, PathBuiltDist, PathSourceDist,
|
||||
RegistryBuiltDist, RegistryBuiltWheel, RegistrySourceDist, RemoteSource, Resolution,
|
||||
ResolvedDist, SourceDist, ToUrlError, UrlString,
|
||||
RegistryBuiltDist, RegistryBuiltWheel, RegistrySourceDist, RemoteSource, RequiresPython,
|
||||
Resolution, ResolvedDist, SourceDist, ToUrlError, UrlString,
|
||||
};
|
||||
use uv_fs::{PortablePathBuf, relative_to};
|
||||
use uv_git::{RepositoryReference, ResolvedRepositoryReference};
|
||||
|
@ -40,7 +40,7 @@ use uv_small_str::SmallString;
|
|||
use crate::lock::export::ExportableRequirements;
|
||||
use crate::lock::{Source, WheelTagHint, each_element_on_its_line_array};
|
||||
use crate::resolution::ResolutionGraphNode;
|
||||
use crate::{Installable, LockError, RequiresPython, ResolverOutput};
|
||||
use crate::{Installable, LockError, ResolverOutput};
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum PylockTomlErrorKind {
|
||||
|
|
|
@ -29,8 +29,8 @@ use uv_distribution_types::{
|
|||
BuiltDist, DependencyMetadata, DirectUrlBuiltDist, DirectUrlSourceDist, DirectorySourceDist,
|
||||
Dist, DistributionMetadata, FileLocation, GitSourceDist, IndexLocations, IndexMetadata,
|
||||
IndexUrl, Name, PathBuiltDist, PathSourceDist, RegistryBuiltDist, RegistryBuiltWheel,
|
||||
RegistrySourceDist, RemoteSource, Requirement, RequirementSource, ResolvedDist, StaticMetadata,
|
||||
ToUrlError, UrlString,
|
||||
RegistrySourceDist, RemoteSource, Requirement, RequirementSource, RequiresPython, ResolvedDist,
|
||||
SimplifiedMarkerTree, StaticMetadata, ToUrlError, UrlString,
|
||||
};
|
||||
use uv_fs::{PortablePath, PortablePathBuf, relative_to};
|
||||
use uv_git::{RepositoryReference, ResolvedRepositoryReference};
|
||||
|
@ -57,12 +57,10 @@ pub use crate::lock::export::{PylockToml, PylockTomlErrorKind};
|
|||
pub use crate::lock::installable::Installable;
|
||||
pub use crate::lock::map::PackageMap;
|
||||
pub use crate::lock::tree::TreeDisplay;
|
||||
use crate::requires_python::SimplifiedMarkerTree;
|
||||
use crate::resolution::{AnnotatedDist, ResolutionGraphNode};
|
||||
use crate::universal_marker::{ConflictMarker, UniversalMarker};
|
||||
use crate::{
|
||||
ExcludeNewer, InMemoryIndex, MetadataResponse, PrereleaseMode, RequiresPython, ResolutionMode,
|
||||
ResolverOutput,
|
||||
ExcludeNewer, InMemoryIndex, MetadataResponse, PrereleaseMode, ResolutionMode, ResolverOutput,
|
||||
};
|
||||
|
||||
mod export;
|
||||
|
|
|
@ -5,7 +5,7 @@ use std::ops::Bound;
|
|||
use uv_pep440::{LowerBound, UpperBound, Version};
|
||||
use uv_pep508::{CanonicalMarkerValueVersion, MarkerTree, MarkerTreeKind};
|
||||
|
||||
use crate::requires_python::RequiresPythonRange;
|
||||
use uv_distribution_types::RequiresPythonRange;
|
||||
|
||||
/// Returns the bounding Python versions that can satisfy the [`MarkerTree`], if it's constrained.
|
||||
pub(crate) fn requires_python(tree: MarkerTree) -> Option<RequiresPythonRange> {
|
||||
|
|
|
@ -11,7 +11,7 @@ use rustc_hash::FxHashMap;
|
|||
use uv_configuration::{IndexStrategy, NoBinary, NoBuild};
|
||||
use uv_distribution_types::{
|
||||
IncompatibleDist, IncompatibleSource, IncompatibleWheel, Index, IndexCapabilities,
|
||||
IndexLocations, IndexMetadata, IndexUrl,
|
||||
IndexLocations, IndexMetadata, IndexUrl, RequiresPython,
|
||||
};
|
||||
use uv_normalize::PackageName;
|
||||
use uv_pep440::{Version, VersionSpecifiers};
|
||||
|
@ -27,9 +27,7 @@ use crate::python_requirement::{PythonRequirement, PythonRequirementSource};
|
|||
use crate::resolver::{
|
||||
MetadataUnavailable, UnavailablePackage, UnavailableReason, UnavailableVersion,
|
||||
};
|
||||
use crate::{
|
||||
Flexibility, InMemoryIndex, Options, RequiresPython, ResolverEnvironment, VersionsResponse,
|
||||
};
|
||||
use crate::{Flexibility, InMemoryIndex, Options, ResolverEnvironment, VersionsResponse};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct PubGrubReportFormatter<'a> {
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
use std::collections::Bound;
|
||||
|
||||
use uv_distribution_types::{RequiresPython, RequiresPythonRange};
|
||||
use uv_pep440::Version;
|
||||
use uv_pep508::{MarkerEnvironment, MarkerTree};
|
||||
use uv_python::{Interpreter, PythonVersion};
|
||||
|
||||
use crate::{RequiresPython, RequiresPythonRange};
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub struct PythonRequirement {
|
||||
source: PythonRequirementSource,
|
||||
|
|
|
@ -12,8 +12,8 @@ use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet};
|
|||
use uv_configuration::{Constraints, Overrides};
|
||||
use uv_distribution::Metadata;
|
||||
use uv_distribution_types::{
|
||||
Dist, DistributionMetadata, Edge, IndexUrl, Name, Node, Requirement, ResolutionDiagnostic,
|
||||
ResolvedDist, VersionId, VersionOrUrlRef,
|
||||
Dist, DistributionMetadata, Edge, IndexUrl, Name, Node, Requirement, RequiresPython,
|
||||
ResolutionDiagnostic, ResolvedDist, VersionId, VersionOrUrlRef,
|
||||
};
|
||||
use uv_git::GitResolver;
|
||||
use uv_normalize::{ExtraName, GroupName, PackageName};
|
||||
|
@ -30,8 +30,7 @@ use crate::resolution_mode::ResolutionStrategy;
|
|||
use crate::resolver::{Resolution, ResolutionDependencyEdge, ResolutionPackage};
|
||||
use crate::universal_marker::{ConflictMarker, UniversalMarker};
|
||||
use crate::{
|
||||
InMemoryIndex, MetadataResponse, Options, PythonRequirement, RequiresPython, ResolveError,
|
||||
VersionsResponse,
|
||||
InMemoryIndex, MetadataResponse, Options, PythonRequirement, ResolveError, VersionsResponse,
|
||||
};
|
||||
|
||||
/// The output of a successful resolution.
|
||||
|
|
|
@ -4,16 +4,16 @@ use std::path::Path;
|
|||
|
||||
use itertools::Itertools;
|
||||
|
||||
use uv_distribution_types::{DistributionMetadata, Name, ResolvedDist, Verbatim, VersionOrUrlRef};
|
||||
use uv_distribution_types::{
|
||||
DistributionMetadata, Name, RequiresPython, ResolvedDist, SimplifiedMarkerTree, Verbatim,
|
||||
VersionOrUrlRef,
|
||||
};
|
||||
use uv_normalize::{ExtraName, PackageName};
|
||||
use uv_pep440::Version;
|
||||
use uv_pep508::{MarkerTree, Scheme, split_scheme};
|
||||
use uv_pypi_types::HashDigest;
|
||||
|
||||
use crate::{
|
||||
requires_python::{RequiresPython, SimplifiedMarkerTree},
|
||||
resolution::AnnotatedDist,
|
||||
};
|
||||
use crate::resolution::AnnotatedDist;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
/// A pinned package with its resolved distribution and all the extras that were pinned for it.
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
use std::sync::Arc;
|
||||
use tracing::trace;
|
||||
use uv_distribution_types::{RequiresPython, RequiresPythonRange};
|
||||
use uv_pep440::VersionSpecifiers;
|
||||
use uv_pep508::{MarkerEnvironment, MarkerTree};
|
||||
use uv_pypi_types::{ConflictItem, ConflictItemRef, ResolverMarkerEnvironment};
|
||||
|
||||
use crate::pubgrub::{PubGrubDependency, PubGrubPackage};
|
||||
use crate::requires_python::RequiresPythonRange;
|
||||
use crate::resolver::ForkState;
|
||||
use crate::universal_marker::{ConflictMarker, UniversalMarker};
|
||||
use crate::{PythonRequirement, RequiresPython, ResolveError};
|
||||
use crate::{PythonRequirement, ResolveError};
|
||||
|
||||
/// Represents one or more marker environments for a resolution.
|
||||
///
|
||||
|
@ -628,7 +628,7 @@ mod tests {
|
|||
use uv_pep440::{LowerBound, UpperBound, Version};
|
||||
use uv_pep508::{MarkerEnvironment, MarkerEnvironmentBuilder};
|
||||
|
||||
use crate::requires_python::{RequiresPython, RequiresPythonRange};
|
||||
use uv_distribution_types::{RequiresPython, RequiresPythonRange};
|
||||
|
||||
use super::*;
|
||||
|
||||
|
|
|
@ -5,16 +5,17 @@ use uv_configuration::BuildOptions;
|
|||
use uv_distribution::{ArchiveMetadata, DistributionDatabase, Reporter};
|
||||
use uv_distribution_types::{
|
||||
Dist, IndexCapabilities, IndexMetadata, IndexMetadataRef, InstalledDist, RequestedDist,
|
||||
RequiresPython,
|
||||
};
|
||||
use uv_normalize::PackageName;
|
||||
use uv_pep440::{Version, VersionSpecifiers};
|
||||
use uv_platform_tags::Tags;
|
||||
use uv_types::{BuildContext, HashStrategy};
|
||||
|
||||
use crate::ExcludeNewer;
|
||||
use crate::flat_index::FlatIndex;
|
||||
use crate::version_map::VersionMap;
|
||||
use crate::yanks::AllowedYanks;
|
||||
use crate::{ExcludeNewer, RequiresPython};
|
||||
|
||||
pub type PackageVersionsResult = Result<VersionsResponse, uv_client::Error>;
|
||||
pub type WheelMetadataResult = Result<MetadataResponse, uv_distribution::Error>;
|
||||
|
|
|
@ -11,7 +11,8 @@ use uv_configuration::BuildOptions;
|
|||
use uv_distribution_filename::{DistFilename, WheelFilename};
|
||||
use uv_distribution_types::{
|
||||
HashComparison, IncompatibleSource, IncompatibleWheel, IndexUrl, PrioritizedDist,
|
||||
RegistryBuiltWheel, RegistrySourceDist, SourceDistCompatibility, WheelCompatibility,
|
||||
RegistryBuiltWheel, RegistrySourceDist, RequiresPython, SourceDistCompatibility,
|
||||
WheelCompatibility,
|
||||
};
|
||||
use uv_normalize::PackageName;
|
||||
use uv_pep440::Version;
|
||||
|
@ -21,7 +22,7 @@ use uv_types::HashStrategy;
|
|||
use uv_warnings::warn_user_once;
|
||||
|
||||
use crate::flat_index::FlatDistributions;
|
||||
use crate::{ExcludeNewer, RequiresPython, yanks::AllowedYanks};
|
||||
use crate::{ExcludeNewer, yanks::AllowedYanks};
|
||||
|
||||
/// A map from versions to distributions.
|
||||
#[derive(Debug)]
|
||||
|
|
|
@ -140,6 +140,9 @@ pub struct Options {
|
|||
#[cfg_attr(feature = "schemars", schemars(skip))]
|
||||
pub default_groups: Option<serde::de::IgnoredAny>,
|
||||
|
||||
#[cfg_attr(feature = "schemars", schemars(skip))]
|
||||
pub dependency_groups: Option<serde::de::IgnoredAny>,
|
||||
|
||||
#[cfg_attr(feature = "schemars", schemars(skip))]
|
||||
pub managed: Option<serde::de::IgnoredAny>,
|
||||
|
||||
|
@ -1870,6 +1873,7 @@ pub struct OptionsWire {
|
|||
managed: Option<serde::de::IgnoredAny>,
|
||||
r#package: Option<serde::de::IgnoredAny>,
|
||||
default_groups: Option<serde::de::IgnoredAny>,
|
||||
dependency_groups: Option<serde::de::IgnoredAny>,
|
||||
dev_dependencies: Option<serde::de::IgnoredAny>,
|
||||
|
||||
// Build backend
|
||||
|
@ -1934,6 +1938,7 @@ impl From<OptionsWire> for Options {
|
|||
workspace,
|
||||
sources,
|
||||
default_groups,
|
||||
dependency_groups,
|
||||
dev_dependencies,
|
||||
managed,
|
||||
package,
|
||||
|
@ -2010,6 +2015,7 @@ impl From<OptionsWire> for Options {
|
|||
sources,
|
||||
dev_dependencies,
|
||||
default_groups,
|
||||
dependency_groups,
|
||||
managed,
|
||||
package,
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ workspace = true
|
|||
[dependencies]
|
||||
uv-build-backend = { workspace = true, features = ["schemars"] }
|
||||
uv-cache-key = { workspace = true }
|
||||
uv-configuration = { workspace = true }
|
||||
uv-distribution-types = { workspace = true }
|
||||
uv-fs = { workspace = true, features = ["tokio", "schemars"] }
|
||||
uv-git-types = { workspace = true }
|
||||
|
|
|
@ -1,32 +1,106 @@
|
|||
use std::collections::BTreeMap;
|
||||
use std::collections::btree_map::Entry;
|
||||
use std::str::FromStr;
|
||||
use std::{collections::BTreeMap, path::Path};
|
||||
|
||||
use thiserror::Error;
|
||||
use tracing::error;
|
||||
|
||||
use uv_distribution_types::RequiresPython;
|
||||
use uv_fs::Simplified;
|
||||
use uv_normalize::{DEV_DEPENDENCIES, GroupName};
|
||||
use uv_pep440::VersionSpecifiers;
|
||||
use uv_pep508::Pep508Error;
|
||||
use uv_pypi_types::{DependencyGroupSpecifier, VerbatimParsedUrl};
|
||||
|
||||
use crate::pyproject::{DependencyGroupSettings, PyProjectToml, ToolUvDependencyGroups};
|
||||
|
||||
/// PEP 735 dependency groups, with any `include-group` entries resolved.
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct FlatDependencyGroups(
|
||||
BTreeMap<GroupName, Vec<uv_pep508::Requirement<VerbatimParsedUrl>>>,
|
||||
);
|
||||
pub struct FlatDependencyGroups(BTreeMap<GroupName, FlatDependencyGroup>);
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct FlatDependencyGroup {
|
||||
pub requirements: Vec<uv_pep508::Requirement<VerbatimParsedUrl>>,
|
||||
pub requires_python: Option<VersionSpecifiers>,
|
||||
}
|
||||
|
||||
impl FlatDependencyGroups {
|
||||
/// Gather and flatten all the dependency-groups defined in the given pyproject.toml
|
||||
///
|
||||
/// The path is only used in diagnostics.
|
||||
pub fn from_pyproject_toml(
|
||||
path: &Path,
|
||||
pyproject_toml: &PyProjectToml,
|
||||
) -> Result<Self, DependencyGroupError> {
|
||||
// First, collect `tool.uv.dev_dependencies`
|
||||
let dev_dependencies = pyproject_toml
|
||||
.tool
|
||||
.as_ref()
|
||||
.and_then(|tool| tool.uv.as_ref())
|
||||
.and_then(|uv| uv.dev_dependencies.as_ref());
|
||||
|
||||
// Then, collect `dependency-groups`
|
||||
let dependency_groups = pyproject_toml
|
||||
.dependency_groups
|
||||
.iter()
|
||||
.flatten()
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
|
||||
// Get additional settings
|
||||
let empty_settings = ToolUvDependencyGroups::default();
|
||||
let group_settings = pyproject_toml
|
||||
.tool
|
||||
.as_ref()
|
||||
.and_then(|tool| tool.uv.as_ref())
|
||||
.and_then(|uv| uv.dependency_groups.as_ref())
|
||||
.unwrap_or(&empty_settings);
|
||||
|
||||
// Flatten the dependency groups.
|
||||
let mut dependency_groups = FlatDependencyGroups::from_dependency_groups(
|
||||
&dependency_groups,
|
||||
group_settings.inner(),
|
||||
)
|
||||
.map_err(|err| DependencyGroupError {
|
||||
package: pyproject_toml
|
||||
.project
|
||||
.as_ref()
|
||||
.map(|project| project.name.to_string())
|
||||
.unwrap_or_default(),
|
||||
path: path.user_display().to_string(),
|
||||
error: err.with_dev_dependencies(dev_dependencies),
|
||||
})?;
|
||||
|
||||
// Add the `dev` group, if the legacy `dev-dependencies` is defined.
|
||||
//
|
||||
// NOTE: the fact that we do this out here means that nothing can inherit from
|
||||
// the legacy dev-dependencies group (or define a group requires-python for it).
|
||||
// This is intentional, we want groups to be defined in a standard interoperable
|
||||
// way, and letting things include-group a group that isn't defined would be a
|
||||
// mess for other python tools.
|
||||
if let Some(dev_dependencies) = dev_dependencies {
|
||||
dependency_groups
|
||||
.entry(DEV_DEPENDENCIES.clone())
|
||||
.or_insert_with(FlatDependencyGroup::default)
|
||||
.requirements
|
||||
.extend(dev_dependencies.clone());
|
||||
}
|
||||
|
||||
Ok(dependency_groups)
|
||||
}
|
||||
|
||||
/// Resolve the dependency groups (which may contain references to other groups) into concrete
|
||||
/// lists of requirements.
|
||||
pub fn from_dependency_groups(
|
||||
fn from_dependency_groups(
|
||||
groups: &BTreeMap<&GroupName, &Vec<DependencyGroupSpecifier>>,
|
||||
) -> Result<Self, DependencyGroupError> {
|
||||
settings: &BTreeMap<GroupName, DependencyGroupSettings>,
|
||||
) -> Result<Self, DependencyGroupErrorInner> {
|
||||
fn resolve_group<'data>(
|
||||
resolved: &mut BTreeMap<GroupName, Vec<uv_pep508::Requirement<VerbatimParsedUrl>>>,
|
||||
resolved: &mut BTreeMap<GroupName, FlatDependencyGroup>,
|
||||
groups: &'data BTreeMap<&GroupName, &Vec<DependencyGroupSpecifier>>,
|
||||
settings: &BTreeMap<GroupName, DependencyGroupSettings>,
|
||||
name: &'data GroupName,
|
||||
parents: &mut Vec<&'data GroupName>,
|
||||
) -> Result<(), DependencyGroupError> {
|
||||
) -> Result<(), DependencyGroupErrorInner> {
|
||||
let Some(specifiers) = groups.get(name) else {
|
||||
// Missing group
|
||||
let parent_name = parents
|
||||
|
@ -34,7 +108,7 @@ impl FlatDependencyGroups {
|
|||
.last()
|
||||
.copied()
|
||||
.expect("parent when group is missing");
|
||||
return Err(DependencyGroupError::GroupNotFound(
|
||||
return Err(DependencyGroupErrorInner::GroupNotFound(
|
||||
name.clone(),
|
||||
parent_name.clone(),
|
||||
));
|
||||
|
@ -42,7 +116,7 @@ impl FlatDependencyGroups {
|
|||
|
||||
// "Dependency Group Includes MUST NOT include cycles, and tools SHOULD report an error if they detect a cycle."
|
||||
if parents.contains(&name) {
|
||||
return Err(DependencyGroupError::DependencyGroupCycle(Cycle(
|
||||
return Err(DependencyGroupErrorInner::DependencyGroupCycle(Cycle(
|
||||
parents.iter().copied().cloned().collect(),
|
||||
)));
|
||||
}
|
||||
|
@ -54,13 +128,14 @@ impl FlatDependencyGroups {
|
|||
|
||||
parents.push(name);
|
||||
let mut requirements = Vec::with_capacity(specifiers.len());
|
||||
let mut requires_python_intersection = VersionSpecifiers::empty();
|
||||
for specifier in *specifiers {
|
||||
match specifier {
|
||||
DependencyGroupSpecifier::Requirement(requirement) => {
|
||||
match uv_pep508::Requirement::<VerbatimParsedUrl>::from_str(requirement) {
|
||||
Ok(requirement) => requirements.push(requirement),
|
||||
Err(err) => {
|
||||
return Err(DependencyGroupError::GroupParseError(
|
||||
return Err(DependencyGroupErrorInner::GroupParseError(
|
||||
name.clone(),
|
||||
requirement.clone(),
|
||||
Box::new(err),
|
||||
|
@ -69,72 +144,107 @@ impl FlatDependencyGroups {
|
|||
}
|
||||
}
|
||||
DependencyGroupSpecifier::IncludeGroup { include_group } => {
|
||||
resolve_group(resolved, groups, include_group, parents)?;
|
||||
requirements
|
||||
.extend(resolved.get(include_group).into_iter().flatten().cloned());
|
||||
resolve_group(resolved, groups, settings, include_group, parents)?;
|
||||
if let Some(included) = resolved.get(include_group) {
|
||||
requirements.extend(included.requirements.iter().cloned());
|
||||
|
||||
// Intersect the requires-python for this group with the the included group's
|
||||
requires_python_intersection = requires_python_intersection
|
||||
.into_iter()
|
||||
.chain(included.requires_python.clone().into_iter().flatten())
|
||||
.collect();
|
||||
}
|
||||
}
|
||||
DependencyGroupSpecifier::Object(map) => {
|
||||
return Err(DependencyGroupError::DependencyObjectSpecifierNotSupported(
|
||||
name.clone(),
|
||||
map.clone(),
|
||||
));
|
||||
return Err(
|
||||
DependencyGroupErrorInner::DependencyObjectSpecifierNotSupported(
|
||||
name.clone(),
|
||||
map.clone(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let empty_settings = DependencyGroupSettings::default();
|
||||
let DependencyGroupSettings { requires_python } =
|
||||
settings.get(name).unwrap_or(&empty_settings);
|
||||
if let Some(requires_python) = requires_python {
|
||||
// Intersect the requires-python for this group to get the final requires-python
|
||||
// that will be used by interpreter discovery and checking.
|
||||
requires_python_intersection = requires_python_intersection
|
||||
.into_iter()
|
||||
.chain(requires_python.clone())
|
||||
.collect();
|
||||
|
||||
// Add the group requires-python as a marker to each requirement
|
||||
// We don't use `requires_python_intersection` because each `include-group`
|
||||
// should already have its markers applied to these.
|
||||
for requirement in &mut requirements {
|
||||
let extra_markers =
|
||||
RequiresPython::from_specifiers(requires_python).to_marker_tree();
|
||||
requirement.marker.and(extra_markers);
|
||||
}
|
||||
}
|
||||
|
||||
parents.pop();
|
||||
|
||||
resolved.insert(name.clone(), requirements);
|
||||
resolved.insert(
|
||||
name.clone(),
|
||||
FlatDependencyGroup {
|
||||
requirements,
|
||||
requires_python: if requires_python_intersection.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(requires_python_intersection)
|
||||
},
|
||||
},
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Validate the settings
|
||||
for (group_name, ..) in settings {
|
||||
if !groups.contains_key(group_name) {
|
||||
return Err(DependencyGroupErrorInner::SettingsGroupNotFound(
|
||||
group_name.clone(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let mut resolved = BTreeMap::new();
|
||||
for name in groups.keys() {
|
||||
let mut parents = Vec::new();
|
||||
resolve_group(&mut resolved, groups, name, &mut parents)?;
|
||||
resolve_group(&mut resolved, groups, settings, name, &mut parents)?;
|
||||
}
|
||||
Ok(Self(resolved))
|
||||
}
|
||||
|
||||
/// Return the requirements for a given group, if any.
|
||||
pub fn get(
|
||||
&self,
|
||||
group: &GroupName,
|
||||
) -> Option<&Vec<uv_pep508::Requirement<VerbatimParsedUrl>>> {
|
||||
pub fn get(&self, group: &GroupName) -> Option<&FlatDependencyGroup> {
|
||||
self.0.get(group)
|
||||
}
|
||||
|
||||
/// Return the entry for a given group, if any.
|
||||
pub fn entry(
|
||||
&mut self,
|
||||
group: GroupName,
|
||||
) -> Entry<GroupName, Vec<uv_pep508::Requirement<VerbatimParsedUrl>>> {
|
||||
pub fn entry(&mut self, group: GroupName) -> Entry<GroupName, FlatDependencyGroup> {
|
||||
self.0.entry(group)
|
||||
}
|
||||
|
||||
/// Consume the [`FlatDependencyGroups`] and return the inner map.
|
||||
pub fn into_inner(self) -> BTreeMap<GroupName, Vec<uv_pep508::Requirement<VerbatimParsedUrl>>> {
|
||||
pub fn into_inner(self) -> BTreeMap<GroupName, FlatDependencyGroup> {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl FromIterator<(GroupName, Vec<uv_pep508::Requirement<VerbatimParsedUrl>>)>
|
||||
for FlatDependencyGroups
|
||||
{
|
||||
fn from_iter<
|
||||
T: IntoIterator<Item = (GroupName, Vec<uv_pep508::Requirement<VerbatimParsedUrl>>)>,
|
||||
>(
|
||||
iter: T,
|
||||
) -> Self {
|
||||
impl FromIterator<(GroupName, FlatDependencyGroup)> for FlatDependencyGroups {
|
||||
fn from_iter<T: IntoIterator<Item = (GroupName, FlatDependencyGroup)>>(iter: T) -> Self {
|
||||
Self(iter.into_iter().collect())
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoIterator for FlatDependencyGroups {
|
||||
type Item = (GroupName, Vec<uv_pep508::Requirement<VerbatimParsedUrl>>);
|
||||
type IntoIter = std::collections::btree_map::IntoIter<
|
||||
GroupName,
|
||||
Vec<uv_pep508::Requirement<VerbatimParsedUrl>>,
|
||||
>;
|
||||
type Item = (GroupName, FlatDependencyGroup);
|
||||
type IntoIter = std::collections::btree_map::IntoIter<GroupName, FlatDependencyGroup>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.0.into_iter()
|
||||
|
@ -142,7 +252,24 @@ impl IntoIterator for FlatDependencyGroups {
|
|||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum DependencyGroupError {
|
||||
#[error("{} has malformed dependency groups", if path.is_empty() && package.is_empty() {
|
||||
"Project".to_string()
|
||||
} else if path.is_empty() {
|
||||
format!("Project `{package}`")
|
||||
} else if package.is_empty() {
|
||||
format!("`{path}`")
|
||||
} else {
|
||||
format!("Project `{package} @ {path}`")
|
||||
})]
|
||||
pub struct DependencyGroupError {
|
||||
package: String,
|
||||
path: String,
|
||||
#[source]
|
||||
error: DependencyGroupErrorInner,
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum DependencyGroupErrorInner {
|
||||
#[error("Failed to parse entry in group `{0}`: `{1}`")]
|
||||
GroupParseError(
|
||||
GroupName,
|
||||
|
@ -159,9 +286,15 @@ pub enum DependencyGroupError {
|
|||
DependencyGroupCycle(Cycle),
|
||||
#[error("Group `{0}` contains an unknown dependency object specifier: {1:?}")]
|
||||
DependencyObjectSpecifierNotSupported(GroupName, BTreeMap<String, String>),
|
||||
#[error("Failed to find group `{0}` specified in `[tool.uv.dependency-groups]`")]
|
||||
SettingsGroupNotFound(GroupName),
|
||||
#[error(
|
||||
"`[tool.uv.dependency-groups]` specifies the `dev` group, but only `tool.uv.dev-dependencies` was found. To reference the `dev` group, remove the `tool.uv.dev-dependencies` section and add any development dependencies to the `dev` entry in the `[dependency-groups]` table instead."
|
||||
)]
|
||||
SettingsDevGroupInclude,
|
||||
}
|
||||
|
||||
impl DependencyGroupError {
|
||||
impl DependencyGroupErrorInner {
|
||||
/// Enrich a [`DependencyGroupError`] with the `tool.uv.dev-dependencies` metadata, if applicable.
|
||||
#[must_use]
|
||||
pub fn with_dev_dependencies(
|
||||
|
@ -169,10 +302,15 @@ impl DependencyGroupError {
|
|||
dev_dependencies: Option<&Vec<uv_pep508::Requirement<VerbatimParsedUrl>>>,
|
||||
) -> Self {
|
||||
match self {
|
||||
DependencyGroupError::GroupNotFound(group, parent)
|
||||
Self::GroupNotFound(group, parent)
|
||||
if dev_dependencies.is_some() && group == *DEV_DEPENDENCIES =>
|
||||
{
|
||||
DependencyGroupError::DevGroupInclude(parent)
|
||||
Self::DevGroupInclude(parent)
|
||||
}
|
||||
Self::SettingsGroupNotFound(group)
|
||||
if dev_dependencies.is_some() && group == *DEV_DEPENDENCIES =>
|
||||
{
|
||||
Self::SettingsDevGroupInclude
|
||||
}
|
||||
_ => self,
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
pub use workspace::{
|
||||
DiscoveryOptions, MemberDiscovery, ProjectWorkspace, VirtualProject, Workspace, WorkspaceCache,
|
||||
WorkspaceError, WorkspaceMember,
|
||||
DiscoveryOptions, MemberDiscovery, ProjectWorkspace, RequiresPythonSources, VirtualProject,
|
||||
Workspace, WorkspaceCache, WorkspaceError, WorkspaceMember,
|
||||
};
|
||||
|
||||
pub mod dependency_groups;
|
||||
|
|
|
@ -353,6 +353,24 @@ pub struct ToolUv {
|
|||
)]
|
||||
pub default_groups: Option<DefaultGroups>,
|
||||
|
||||
/// Additional settings for `dependency-groups`.
|
||||
///
|
||||
/// Currently this can only be used to add `requires-python` constraints
|
||||
/// to dependency groups (typically to inform uv that your dev tooling
|
||||
/// has a higher python requirement than your actual project).
|
||||
///
|
||||
/// This cannot be used to define dependency groups, use the top-level
|
||||
/// `[dependency-groups]` table for that.
|
||||
#[option(
|
||||
default = "[]",
|
||||
value_type = "dict",
|
||||
example = r#"
|
||||
[tool.uv.dependency-groups]
|
||||
my-group = {requires-python = ">=3.12"}
|
||||
"#
|
||||
)]
|
||||
pub dependency_groups: Option<ToolUvDependencyGroups>,
|
||||
|
||||
/// The project's development dependencies.
|
||||
///
|
||||
/// Development dependencies will be installed by default in `uv run` and `uv sync`, but will
|
||||
|
@ -653,6 +671,77 @@ impl<'de> serde::de::Deserialize<'de> for ToolUvSources {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(test, derive(Serialize))]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct ToolUvDependencyGroups(BTreeMap<GroupName, DependencyGroupSettings>);
|
||||
|
||||
impl ToolUvDependencyGroups {
|
||||
/// Returns the underlying `BTreeMap` of group names to settings.
|
||||
pub fn inner(&self) -> &BTreeMap<GroupName, DependencyGroupSettings> {
|
||||
&self.0
|
||||
}
|
||||
|
||||
/// Convert the [`ToolUvDependencyGroups`] into its inner `BTreeMap`.
|
||||
#[must_use]
|
||||
pub fn into_inner(self) -> BTreeMap<GroupName, DependencyGroupSettings> {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Ensure that all keys in the TOML table are unique.
|
||||
impl<'de> serde::de::Deserialize<'de> for ToolUvDependencyGroups {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
struct SourcesVisitor;
|
||||
|
||||
impl<'de> serde::de::Visitor<'de> for SourcesVisitor {
|
||||
type Value = ToolUvDependencyGroups;
|
||||
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
formatter.write_str("a map with unique keys")
|
||||
}
|
||||
|
||||
fn visit_map<M>(self, mut access: M) -> Result<Self::Value, M::Error>
|
||||
where
|
||||
M: serde::de::MapAccess<'de>,
|
||||
{
|
||||
let mut groups = BTreeMap::new();
|
||||
while let Some((key, value)) =
|
||||
access.next_entry::<GroupName, DependencyGroupSettings>()?
|
||||
{
|
||||
match groups.entry(key) {
|
||||
std::collections::btree_map::Entry::Occupied(entry) => {
|
||||
return Err(serde::de::Error::custom(format!(
|
||||
"duplicate settings for dependency group `{}`",
|
||||
entry.key()
|
||||
)));
|
||||
}
|
||||
std::collections::btree_map::Entry::Vacant(entry) => {
|
||||
entry.insert(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(ToolUvDependencyGroups(groups))
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_map(SourcesVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default, Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(test, derive(Serialize))]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct DependencyGroupSettings {
|
||||
/// Version of python to require when installing this group
|
||||
#[cfg_attr(feature = "schemars", schemars(with = "Option<String>"))]
|
||||
pub requires_python: Option<VersionSpecifiers>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, OptionsMetadata, Default, Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(test, derive(Serialize))]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
|
|
|
@ -8,6 +8,7 @@ use glob::{GlobError, PatternError, glob};
|
|||
use rustc_hash::{FxHashMap, FxHashSet};
|
||||
use tracing::{debug, trace, warn};
|
||||
|
||||
use uv_configuration::DependencyGroupsWithDefaults;
|
||||
use uv_distribution_types::{Index, Requirement, RequirementSource};
|
||||
use uv_fs::{CWD, Simplified};
|
||||
use uv_normalize::{DEV_DEPENDENCIES, GroupName, PackageName};
|
||||
|
@ -17,7 +18,7 @@ use uv_pypi_types::{Conflicts, SupportedEnvironments, VerbatimParsedUrl};
|
|||
use uv_static::EnvVars;
|
||||
use uv_warnings::warn_user_once;
|
||||
|
||||
use crate::dependency_groups::{DependencyGroupError, FlatDependencyGroups};
|
||||
use crate::dependency_groups::{DependencyGroupError, FlatDependencyGroup, FlatDependencyGroups};
|
||||
use crate::pyproject::{
|
||||
Project, PyProjectToml, PyprojectTomlError, Sources, ToolUvSources, ToolUvWorkspace,
|
||||
};
|
||||
|
@ -95,6 +96,8 @@ pub struct DiscoveryOptions {
|
|||
pub members: MemberDiscovery,
|
||||
}
|
||||
|
||||
pub type RequiresPythonSources = BTreeMap<(PackageName, Option<GroupName>), VersionSpecifiers>;
|
||||
|
||||
/// A workspace, consisting of a root directory and members. See [`ProjectWorkspace`].
|
||||
#[derive(Debug, Clone)]
|
||||
#[cfg_attr(test, derive(serde::Serialize))]
|
||||
|
@ -413,15 +416,44 @@ impl Workspace {
|
|||
}
|
||||
|
||||
/// Returns an iterator over the `requires-python` values for each member of the workspace.
|
||||
pub fn requires_python(&self) -> impl Iterator<Item = (&PackageName, &VersionSpecifiers)> {
|
||||
self.packages().iter().filter_map(|(name, member)| {
|
||||
member
|
||||
pub fn requires_python(
|
||||
&self,
|
||||
groups: &DependencyGroupsWithDefaults,
|
||||
) -> Result<RequiresPythonSources, DependencyGroupError> {
|
||||
let mut requires = RequiresPythonSources::new();
|
||||
for (name, member) in self.packages() {
|
||||
// Get the top-level requires-python for this package, which is always active
|
||||
//
|
||||
// Arguably we could check groups.prod() to disable this, since, the requires-python
|
||||
// of the project is *technically* not relevant if you're doing `--only-group`, but,
|
||||
// that would be a big surprising change so let's *not* do that until someone asks!
|
||||
let top_requires = member
|
||||
.pyproject_toml()
|
||||
.project
|
||||
.as_ref()
|
||||
.and_then(|project| project.requires_python.as_ref())
|
||||
.map(|requires_python| (name, requires_python))
|
||||
})
|
||||
.map(|requires_python| ((name.to_owned(), None), requires_python.clone()));
|
||||
requires.extend(top_requires);
|
||||
|
||||
// Get the requires-python for each enabled group on this package
|
||||
// We need to do full flattening here because include-group can transfer requires-python
|
||||
let dependency_groups =
|
||||
FlatDependencyGroups::from_pyproject_toml(member.root(), &member.pyproject_toml)?;
|
||||
let group_requires =
|
||||
dependency_groups
|
||||
.into_iter()
|
||||
.filter_map(move |(group_name, flat_group)| {
|
||||
if groups.contains(&group_name) {
|
||||
flat_group.requires_python.map(|requires_python| {
|
||||
((name.to_owned(), Some(group_name)), requires_python)
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
requires.extend(group_requires);
|
||||
}
|
||||
Ok(requires)
|
||||
}
|
||||
|
||||
/// Returns any requirements that are exclusive to the workspace root, i.e., not included in
|
||||
|
@ -439,12 +471,9 @@ impl Workspace {
|
|||
/// corresponding `pyproject.toml`.
|
||||
///
|
||||
/// Otherwise, returns an empty list.
|
||||
pub fn dependency_groups(
|
||||
pub fn workspace_dependency_groups(
|
||||
&self,
|
||||
) -> Result<
|
||||
BTreeMap<GroupName, Vec<uv_pep508::Requirement<VerbatimParsedUrl>>>,
|
||||
DependencyGroupError,
|
||||
> {
|
||||
) -> Result<BTreeMap<GroupName, FlatDependencyGroup>, DependencyGroupError> {
|
||||
if self
|
||||
.packages
|
||||
.values()
|
||||
|
@ -455,35 +484,10 @@ impl Workspace {
|
|||
Ok(BTreeMap::default())
|
||||
} else {
|
||||
// Otherwise, return the dependency groups in the non-project workspace root.
|
||||
// First, collect `tool.uv.dev_dependencies`
|
||||
let dev_dependencies = self
|
||||
.pyproject_toml
|
||||
.tool
|
||||
.as_ref()
|
||||
.and_then(|tool| tool.uv.as_ref())
|
||||
.and_then(|uv| uv.dev_dependencies.as_ref());
|
||||
|
||||
// Then, collect `dependency-groups`
|
||||
let dependency_groups = self
|
||||
.pyproject_toml
|
||||
.dependency_groups
|
||||
.iter()
|
||||
.flatten()
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
|
||||
// Flatten the dependency groups.
|
||||
let mut dependency_groups =
|
||||
FlatDependencyGroups::from_dependency_groups(&dependency_groups)
|
||||
.map_err(|err| err.with_dev_dependencies(dev_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());
|
||||
}
|
||||
|
||||
let dependency_groups = FlatDependencyGroups::from_pyproject_toml(
|
||||
&self.install_path,
|
||||
&self.pyproject_toml,
|
||||
)?;
|
||||
Ok(dependency_groups.into_inner())
|
||||
}
|
||||
}
|
||||
|
@ -1818,6 +1822,7 @@ mod tests {
|
|||
"managed": null,
|
||||
"package": null,
|
||||
"default-groups": null,
|
||||
"dependency-groups": null,
|
||||
"dev-dependencies": null,
|
||||
"override-dependencies": null,
|
||||
"constraint-dependencies": null,
|
||||
|
@ -1913,6 +1918,7 @@ mod tests {
|
|||
"managed": null,
|
||||
"package": null,
|
||||
"default-groups": null,
|
||||
"dependency-groups": null,
|
||||
"dev-dependencies": null,
|
||||
"override-dependencies": null,
|
||||
"constraint-dependencies": null,
|
||||
|
@ -2123,6 +2129,7 @@ mod tests {
|
|||
"managed": null,
|
||||
"package": null,
|
||||
"default-groups": null,
|
||||
"dependency-groups": null,
|
||||
"dev-dependencies": null,
|
||||
"override-dependencies": null,
|
||||
"constraint-dependencies": null,
|
||||
|
@ -2230,6 +2237,7 @@ mod tests {
|
|||
"managed": null,
|
||||
"package": null,
|
||||
"default-groups": null,
|
||||
"dependency-groups": null,
|
||||
"dev-dependencies": null,
|
||||
"override-dependencies": null,
|
||||
"constraint-dependencies": null,
|
||||
|
@ -2350,6 +2358,7 @@ mod tests {
|
|||
"managed": null,
|
||||
"package": null,
|
||||
"default-groups": null,
|
||||
"dependency-groups": null,
|
||||
"dev-dependencies": null,
|
||||
"override-dependencies": null,
|
||||
"constraint-dependencies": null,
|
||||
|
@ -2444,6 +2453,7 @@ mod tests {
|
|||
"managed": null,
|
||||
"package": null,
|
||||
"default-groups": null,
|
||||
"dependency-groups": null,
|
||||
"dev-dependencies": null,
|
||||
"override-dependencies": null,
|
||||
"constraint-dependencies": null,
|
||||
|
|
|
@ -16,13 +16,16 @@ use uv_cache::{Cache, CacheBucket};
|
|||
use uv_client::{BaseClientBuilder, FlatIndexClient, RegistryClientBuilder};
|
||||
use uv_configuration::{
|
||||
BuildKind, BuildOptions, BuildOutput, Concurrency, ConfigSettings, Constraints,
|
||||
HashCheckingMode, IndexStrategy, KeyringProviderType, PreviewMode, SourceStrategy,
|
||||
DependencyGroupsWithDefaults, HashCheckingMode, IndexStrategy, KeyringProviderType,
|
||||
PreviewMode, SourceStrategy,
|
||||
};
|
||||
use uv_dispatch::{BuildDispatch, SharedState};
|
||||
use uv_distribution_filename::{
|
||||
DistFilename, SourceDistExtension, SourceDistFilename, WheelFilename,
|
||||
};
|
||||
use uv_distribution_types::{DependencyMetadata, Index, IndexLocations, SourceDist};
|
||||
use uv_distribution_types::{
|
||||
DependencyMetadata, Index, IndexLocations, RequiresPython, SourceDist,
|
||||
};
|
||||
use uv_fs::{Simplified, relative_to};
|
||||
use uv_install_wheel::LinkMode;
|
||||
use uv_normalize::PackageName;
|
||||
|
@ -33,7 +36,7 @@ use uv_python::{
|
|||
VersionRequest,
|
||||
};
|
||||
use uv_requirements::RequirementsSource;
|
||||
use uv_resolver::{ExcludeNewer, FlatIndex, RequiresPython};
|
||||
use uv_resolver::{ExcludeNewer, FlatIndex};
|
||||
use uv_settings::PythonInstallMirrors;
|
||||
use uv_types::{AnyErrorBuild, BuildContext, BuildIsolation, BuildStack, HashStrategy};
|
||||
use uv_workspace::{DiscoveryOptions, Workspace, WorkspaceCache, WorkspaceError};
|
||||
|
@ -471,7 +474,8 @@ async fn build_package(
|
|||
// (3) `Requires-Python` in `pyproject.toml`
|
||||
if interpreter_request.is_none() {
|
||||
if let Ok(workspace) = workspace {
|
||||
interpreter_request = find_requires_python(workspace)?
|
||||
let groups = DependencyGroupsWithDefaults::none();
|
||||
interpreter_request = find_requires_python(workspace, &groups)?
|
||||
.as_ref()
|
||||
.map(RequiresPython::specifiers)
|
||||
.map(|specifiers| {
|
||||
|
|
|
@ -21,7 +21,7 @@ use uv_configuration::{KeyringProviderType, TargetTriple};
|
|||
use uv_dispatch::{BuildDispatch, SharedState};
|
||||
use uv_distribution_types::{
|
||||
DependencyMetadata, HashGeneration, Index, IndexLocations, NameRequirementSpecification,
|
||||
Origin, Requirement, UnresolvedRequirementSpecification, Verbatim,
|
||||
Origin, Requirement, RequiresPython, UnresolvedRequirementSpecification, Verbatim,
|
||||
};
|
||||
use uv_fs::{CWD, Simplified};
|
||||
use uv_git::ResolvedRepositoryReference;
|
||||
|
@ -38,8 +38,8 @@ use uv_requirements::{
|
|||
};
|
||||
use uv_resolver::{
|
||||
AnnotationStyle, DependencyMode, DisplayResolutionGraph, ExcludeNewer, FlatIndex, ForkStrategy,
|
||||
InMemoryIndex, OptionsBuilder, PrereleaseMode, PylockToml, PythonRequirement, RequiresPython,
|
||||
ResolutionMode, ResolverEnvironment,
|
||||
InMemoryIndex, OptionsBuilder, PrereleaseMode, PylockToml, PythonRequirement, ResolutionMode,
|
||||
ResolverEnvironment,
|
||||
};
|
||||
use uv_torch::{TorchMode, TorchStrategy};
|
||||
use uv_types::{BuildIsolation, EmptyInstalledPackages, HashStrategy};
|
||||
|
|
|
@ -3,10 +3,10 @@ use tracing::debug;
|
|||
|
||||
use uv_client::{MetadataFormat, RegistryClient, VersionFiles};
|
||||
use uv_distribution_filename::DistFilename;
|
||||
use uv_distribution_types::{IndexCapabilities, IndexMetadataRef, IndexUrl};
|
||||
use uv_distribution_types::{IndexCapabilities, IndexMetadataRef, IndexUrl, RequiresPython};
|
||||
use uv_normalize::PackageName;
|
||||
use uv_platform_tags::Tags;
|
||||
use uv_resolver::{ExcludeNewer, PrereleaseMode, RequiresPython};
|
||||
use uv_resolver::{ExcludeNewer, PrereleaseMode};
|
||||
use uv_warnings::warn_user_once;
|
||||
|
||||
/// A client to fetch the latest version of a package from an index.
|
||||
|
|
|
@ -17,14 +17,16 @@ use uv_cli::ListFormat;
|
|||
use uv_client::{BaseClientBuilder, RegistryClientBuilder};
|
||||
use uv_configuration::{Concurrency, IndexStrategy, KeyringProviderType};
|
||||
use uv_distribution_filename::DistFilename;
|
||||
use uv_distribution_types::{Diagnostic, IndexCapabilities, IndexLocations, InstalledDist, Name};
|
||||
use uv_distribution_types::{
|
||||
Diagnostic, IndexCapabilities, IndexLocations, InstalledDist, Name, RequiresPython,
|
||||
};
|
||||
use uv_fs::Simplified;
|
||||
use uv_installer::SitePackages;
|
||||
use uv_normalize::PackageName;
|
||||
use uv_pep440::Version;
|
||||
use uv_python::PythonRequest;
|
||||
use uv_python::{EnvironmentPreference, PythonEnvironment};
|
||||
use uv_resolver::{ExcludeNewer, PrereleaseMode, RequiresPython};
|
||||
use uv_resolver::{ExcludeNewer, PrereleaseMode};
|
||||
|
||||
use crate::commands::ExitStatus;
|
||||
use crate::commands::pip::latest::LatestClient;
|
||||
|
|
|
@ -14,14 +14,14 @@ use uv_cache::{Cache, Refresh};
|
|||
use uv_cache_info::Timestamp;
|
||||
use uv_client::{BaseClientBuilder, RegistryClientBuilder};
|
||||
use uv_configuration::{Concurrency, IndexStrategy, KeyringProviderType};
|
||||
use uv_distribution_types::{Diagnostic, IndexCapabilities, IndexLocations, Name};
|
||||
use uv_distribution_types::{Diagnostic, IndexCapabilities, IndexLocations, Name, RequiresPython};
|
||||
use uv_installer::SitePackages;
|
||||
use uv_normalize::PackageName;
|
||||
use uv_pep440::Version;
|
||||
use uv_pep508::{Requirement, VersionOrUrl};
|
||||
use uv_pypi_types::{ResolutionMetadata, ResolverMarkerEnvironment, VerbatimParsedUrl};
|
||||
use uv_python::{EnvironmentPreference, PythonEnvironment, PythonRequest};
|
||||
use uv_resolver::{ExcludeNewer, PrereleaseMode, RequiresPython};
|
||||
use uv_resolver::{ExcludeNewer, PrereleaseMode};
|
||||
|
||||
use crate::commands::ExitStatus;
|
||||
use crate::commands::pip::latest::LatestClient;
|
||||
|
|
|
@ -17,8 +17,9 @@ use uv_cache::Cache;
|
|||
use uv_cache_key::RepositoryUrl;
|
||||
use uv_client::{BaseClientBuilder, FlatIndexClient, RegistryClientBuilder};
|
||||
use uv_configuration::{
|
||||
Concurrency, Constraints, DependencyGroups, DevMode, DryRun, EditableMode, ExtrasSpecification,
|
||||
InstallOptions, PreviewMode, SourceStrategy,
|
||||
Concurrency, Constraints, DependencyGroups, DependencyGroupsWithDefaults, DevMode, DryRun,
|
||||
EditableMode, ExtrasSpecification, ExtrasSpecificationWithDefaults, InstallOptions,
|
||||
PreviewMode, SourceStrategy,
|
||||
};
|
||||
use uv_dispatch::BuildDispatch;
|
||||
use uv_distribution::DistributionDatabase;
|
||||
|
@ -29,7 +30,7 @@ use uv_distribution_types::{
|
|||
use uv_fs::{LockedFile, Simplified};
|
||||
use uv_git::GIT_STORE;
|
||||
use uv_git_types::GitReference;
|
||||
use uv_normalize::{DEV_DEPENDENCIES, DefaultExtras, PackageName};
|
||||
use uv_normalize::{DEV_DEPENDENCIES, DefaultExtras, DefaultGroups, PackageName};
|
||||
use uv_pep508::{ExtraName, MarkerTree, UnnamedRequirement, VersionOrUrl};
|
||||
use uv_pypi_types::{ParsedUrl, VerbatimParsedUrl};
|
||||
use uv_python::{Interpreter, PythonDownloads, PythonEnvironment, PythonPreference, PythonRequest};
|
||||
|
@ -79,7 +80,7 @@ pub(crate) async fn add(
|
|||
rev: Option<String>,
|
||||
tag: Option<String>,
|
||||
branch: Option<String>,
|
||||
extras: Vec<ExtraName>,
|
||||
extras_of_dependency: Vec<ExtraName>,
|
||||
package: Option<PackageName>,
|
||||
python: Option<String>,
|
||||
install_mirrors: PythonInstallMirrors,
|
||||
|
@ -122,6 +123,34 @@ pub(crate) async fn add(
|
|||
|
||||
let reporter = PythonDownloadReporter::single(printer);
|
||||
|
||||
// Determine what defaults/extras we're explicitly enabling
|
||||
let (extras, groups) = match &dependency_type {
|
||||
DependencyType::Production => {
|
||||
let extras = ExtrasSpecification::from_extra(vec![]);
|
||||
let groups = DependencyGroups::from_dev_mode(DevMode::Exclude);
|
||||
(extras, groups)
|
||||
}
|
||||
DependencyType::Dev => {
|
||||
let extras = ExtrasSpecification::from_extra(vec![]);
|
||||
let groups = DependencyGroups::from_dev_mode(DevMode::Include);
|
||||
(extras, groups)
|
||||
}
|
||||
DependencyType::Optional(extra_name) => {
|
||||
let extras = ExtrasSpecification::from_extra(vec![extra_name.clone()]);
|
||||
let groups = DependencyGroups::from_dev_mode(DevMode::Exclude);
|
||||
(extras, groups)
|
||||
}
|
||||
DependencyType::Group(group_name) => {
|
||||
let extras = ExtrasSpecification::from_extra(vec![]);
|
||||
let groups = DependencyGroups::from_group(group_name.clone());
|
||||
(extras, groups)
|
||||
}
|
||||
};
|
||||
// Default extras currently always disabled
|
||||
let defaulted_extras = extras.with_defaults(DefaultExtras::default());
|
||||
// Default groups we need the actual project for, interpreter discovery will use this!
|
||||
let defaulted_groups;
|
||||
|
||||
let target = if let Some(script) = script {
|
||||
// If we found a PEP 723 script and the user provided a project-only setting, warn.
|
||||
if package.is_some() {
|
||||
|
@ -172,6 +201,9 @@ pub(crate) async fn add(
|
|||
}
|
||||
};
|
||||
|
||||
// Scripts don't actually have groups
|
||||
defaulted_groups = groups.with_defaults(DefaultGroups::default());
|
||||
|
||||
// Discover the interpreter.
|
||||
let interpreter = ScriptInterpreter::discover(
|
||||
Pep723ItemRef::Script(&script),
|
||||
|
@ -234,11 +266,16 @@ pub(crate) async fn add(
|
|||
}
|
||||
}
|
||||
|
||||
// Enable the default groups of the project
|
||||
defaulted_groups =
|
||||
groups.with_defaults(default_dependency_groups(project.pyproject_toml())?);
|
||||
|
||||
if frozen || no_sync {
|
||||
// Discover the interpreter.
|
||||
let interpreter = ProjectInterpreter::discover(
|
||||
project.workspace(),
|
||||
project_dir,
|
||||
&defaulted_groups,
|
||||
python.as_deref().map(PythonRequest::parse),
|
||||
&network_settings,
|
||||
python_preference,
|
||||
|
@ -258,6 +295,7 @@ pub(crate) async fn add(
|
|||
// Discover or create the virtual environment.
|
||||
let environment = ProjectEnvironment::get_or_init(
|
||||
project.workspace(),
|
||||
&defaulted_groups,
|
||||
python.as_deref().map(PythonRequest::parse),
|
||||
&install_mirrors,
|
||||
&network_settings,
|
||||
|
@ -468,7 +506,7 @@ pub(crate) async fn add(
|
|||
rev.as_deref(),
|
||||
tag.as_deref(),
|
||||
branch.as_deref(),
|
||||
&extras,
|
||||
&extras_of_dependency,
|
||||
index,
|
||||
&mut toml,
|
||||
)?;
|
||||
|
@ -551,7 +589,8 @@ pub(crate) async fn add(
|
|||
lock_state,
|
||||
sync_state,
|
||||
locked,
|
||||
&dependency_type,
|
||||
&defaulted_extras,
|
||||
&defaulted_groups,
|
||||
raw,
|
||||
bounds,
|
||||
constraints,
|
||||
|
@ -778,7 +817,8 @@ async fn lock_and_sync(
|
|||
lock_state: UniversalState,
|
||||
sync_state: PlatformState,
|
||||
locked: bool,
|
||||
dependency_type: &DependencyType,
|
||||
extras: &ExtrasSpecificationWithDefaults,
|
||||
groups: &DependencyGroupsWithDefaults,
|
||||
raw: bool,
|
||||
bound_kind: Option<AddBoundsKind>,
|
||||
constraints: Vec<NameRequirementSpecification>,
|
||||
|
@ -942,36 +982,6 @@ async fn lock_and_sync(
|
|||
return Ok(());
|
||||
};
|
||||
|
||||
// Sync the environment.
|
||||
let (extras, dev) = match dependency_type {
|
||||
DependencyType::Production => {
|
||||
let extras = ExtrasSpecification::from_extra(vec![]);
|
||||
let dev = DependencyGroups::from_dev_mode(DevMode::Exclude);
|
||||
(extras, dev)
|
||||
}
|
||||
DependencyType::Dev => {
|
||||
let extras = ExtrasSpecification::from_extra(vec![]);
|
||||
let dev = DependencyGroups::from_dev_mode(DevMode::Include);
|
||||
(extras, dev)
|
||||
}
|
||||
DependencyType::Optional(extra_name) => {
|
||||
let extras = ExtrasSpecification::from_extra(vec![extra_name.clone()]);
|
||||
let dev = DependencyGroups::from_dev_mode(DevMode::Exclude);
|
||||
(extras, dev)
|
||||
}
|
||||
DependencyType::Group(group_name) => {
|
||||
let extras = ExtrasSpecification::from_extra(vec![]);
|
||||
let dev = DependencyGroups::from_group(group_name.clone());
|
||||
(extras, dev)
|
||||
}
|
||||
};
|
||||
|
||||
// Determine the default groups to include.
|
||||
let default_groups = default_dependency_groups(project.pyproject_toml())?;
|
||||
|
||||
// Determine the default extras to include.
|
||||
let default_extras = DefaultExtras::default();
|
||||
|
||||
// Identify the installation target.
|
||||
let target = match &project {
|
||||
VirtualProject::Project(project) => InstallTarget::Project {
|
||||
|
@ -988,8 +998,8 @@ async fn lock_and_sync(
|
|||
project::sync::do_sync(
|
||||
target,
|
||||
venv,
|
||||
&extras.with_defaults(default_extras),
|
||||
&dev.with_defaults(default_groups),
|
||||
extras,
|
||||
groups,
|
||||
EditableMode::Editable,
|
||||
InstallOptions::default(),
|
||||
Modifications::Sufficient,
|
||||
|
|
|
@ -61,7 +61,7 @@ pub(crate) async fn export(
|
|||
install_options: InstallOptions,
|
||||
output_file: Option<PathBuf>,
|
||||
extras: ExtrasSpecification,
|
||||
dev: DependencyGroups,
|
||||
groups: DependencyGroups,
|
||||
editable: EditableMode,
|
||||
locked: bool,
|
||||
frozen: bool,
|
||||
|
@ -122,7 +122,7 @@ pub(crate) async fn export(
|
|||
ExportTarget::Script(_) => DefaultExtras::default(),
|
||||
};
|
||||
|
||||
let dev = dev.with_defaults(default_groups);
|
||||
let groups = groups.with_defaults(default_groups);
|
||||
let extras = extras.with_defaults(default_extras);
|
||||
|
||||
// Find an interpreter for the project, unless `--frozen` is set.
|
||||
|
@ -148,6 +148,7 @@ pub(crate) async fn export(
|
|||
ExportTarget::Project(project) => ProjectInterpreter::discover(
|
||||
project.workspace(),
|
||||
project_dir,
|
||||
&groups,
|
||||
python.as_deref().map(PythonRequest::parse),
|
||||
&network_settings,
|
||||
python_preference,
|
||||
|
@ -206,7 +207,7 @@ pub(crate) async fn export(
|
|||
};
|
||||
|
||||
// Validate that the set of requested extras and development groups are compatible.
|
||||
detect_conflicts(&lock, &extras, &dev)?;
|
||||
detect_conflicts(&lock, &extras, &groups)?;
|
||||
|
||||
// Identify the installation target.
|
||||
let target = match &target {
|
||||
|
@ -259,7 +260,7 @@ pub(crate) async fn export(
|
|||
|
||||
// Validate that the set of requested extras and development groups are defined in the lockfile.
|
||||
target.validate_extras(&extras)?;
|
||||
target.validate_groups(&dev)?;
|
||||
target.validate_groups(&groups)?;
|
||||
|
||||
// Write the resolved dependencies to the output channel.
|
||||
let mut writer = OutputWriter::new(!quiet || output_file.is_none(), output_file.as_deref());
|
||||
|
@ -306,7 +307,7 @@ pub(crate) async fn export(
|
|||
&target,
|
||||
&prune,
|
||||
&extras,
|
||||
&dev,
|
||||
&groups,
|
||||
include_annotations,
|
||||
editable,
|
||||
hashes,
|
||||
|
@ -328,7 +329,7 @@ pub(crate) async fn export(
|
|||
&target,
|
||||
&prune,
|
||||
&extras,
|
||||
&dev,
|
||||
&groups,
|
||||
include_annotations,
|
||||
editable,
|
||||
&install_options,
|
||||
|
|
|
@ -4,13 +4,15 @@ use std::fmt::Write;
|
|||
use std::path::{Path, PathBuf};
|
||||
use std::process::{Command, Stdio};
|
||||
use std::str::FromStr;
|
||||
use uv_distribution_types::RequiresPython;
|
||||
|
||||
use tracing::{debug, trace, warn};
|
||||
use uv_cache::Cache;
|
||||
use uv_cli::AuthorFrom;
|
||||
use uv_client::BaseClientBuilder;
|
||||
use uv_configuration::{
|
||||
PreviewMode, ProjectBuildBackend, VersionControlError, VersionControlSystem,
|
||||
DependencyGroupsWithDefaults, PreviewMode, ProjectBuildBackend, VersionControlError,
|
||||
VersionControlSystem,
|
||||
};
|
||||
use uv_fs::{CWD, Simplified};
|
||||
use uv_git::GIT;
|
||||
|
@ -21,7 +23,6 @@ use uv_python::{
|
|||
PythonPreference, PythonRequest, PythonVariant, PythonVersionFile, VersionFileDiscoveryOptions,
|
||||
VersionRequest,
|
||||
};
|
||||
use uv_resolver::RequiresPython;
|
||||
use uv_scripts::{Pep723Script, ScriptTag};
|
||||
use uv_settings::PythonInstallMirrors;
|
||||
use uv_static::EnvVars;
|
||||
|
@ -502,7 +503,7 @@ async fn init_project(
|
|||
(requires_python, python_request)
|
||||
} else if let Some(requires_python) = workspace
|
||||
.as_ref()
|
||||
.map(find_requires_python)
|
||||
.map(|workspace| find_requires_python(workspace, &DependencyGroupsWithDefaults::none()))
|
||||
.transpose()?
|
||||
.flatten()
|
||||
{
|
||||
|
|
|
@ -165,11 +165,18 @@ impl<'lock> InstallTarget<'lock> {
|
|||
.requirements()
|
||||
.into_iter()
|
||||
.map(Cow::Owned)
|
||||
.chain(workspace.dependency_groups().ok().into_iter().flat_map(
|
||||
|dependency_groups| {
|
||||
dependency_groups.into_values().flatten().map(Cow::Owned)
|
||||
},
|
||||
))
|
||||
.chain(
|
||||
workspace
|
||||
.workspace_dependency_groups()
|
||||
.ok()
|
||||
.into_iter()
|
||||
.flat_map(|dependency_groups| {
|
||||
dependency_groups
|
||||
.into_values()
|
||||
.flat_map(|group| group.requirements)
|
||||
.map(Cow::Owned)
|
||||
}),
|
||||
)
|
||||
.chain(workspace.packages().values().flat_map(|member| {
|
||||
// Iterate over all dependencies in each member.
|
||||
let dependencies = member
|
||||
|
@ -316,9 +323,15 @@ impl<'lock> InstallTarget<'lock> {
|
|||
let known_groups = member_packages
|
||||
.iter()
|
||||
.flat_map(|package| package.dependency_groups().keys().map(Cow::Borrowed))
|
||||
.chain(workspace.dependency_groups().ok().into_iter().flat_map(
|
||||
|dependency_groups| dependency_groups.into_keys().map(Cow::Owned),
|
||||
))
|
||||
.chain(
|
||||
workspace
|
||||
.workspace_dependency_groups()
|
||||
.ok()
|
||||
.into_iter()
|
||||
.flat_map(|dependency_groups| {
|
||||
dependency_groups.into_keys().map(Cow::Owned)
|
||||
}),
|
||||
)
|
||||
.collect::<FxHashSet<_>>();
|
||||
|
||||
for group in groups.explicit_names() {
|
||||
|
|
|
@ -12,13 +12,14 @@ use tracing::debug;
|
|||
use uv_cache::Cache;
|
||||
use uv_client::{BaseClientBuilder, FlatIndexClient, RegistryClientBuilder};
|
||||
use uv_configuration::{
|
||||
Concurrency, Constraints, DryRun, ExtrasSpecification, PreviewMode, Reinstall, Upgrade,
|
||||
Concurrency, Constraints, DependencyGroupsWithDefaults, DryRun, ExtrasSpecification,
|
||||
PreviewMode, Reinstall, Upgrade,
|
||||
};
|
||||
use uv_dispatch::BuildDispatch;
|
||||
use uv_distribution::DistributionDatabase;
|
||||
use uv_distribution_types::{
|
||||
DependencyMetadata, HashGeneration, Index, IndexLocations, NameRequirementSpecification,
|
||||
Requirement, UnresolvedRequirementSpecification,
|
||||
Requirement, RequiresPython, UnresolvedRequirementSpecification,
|
||||
};
|
||||
use uv_git::ResolvedRepositoryReference;
|
||||
use uv_normalize::{GroupName, PackageName};
|
||||
|
@ -28,7 +29,7 @@ use uv_python::{Interpreter, PythonDownloads, PythonEnvironment, PythonPreferenc
|
|||
use uv_requirements::ExtrasResolver;
|
||||
use uv_requirements::upgrade::{LockedRequirements, read_lock_requirements};
|
||||
use uv_resolver::{
|
||||
FlatIndex, InMemoryIndex, Lock, Options, OptionsBuilder, PythonRequirement, RequiresPython,
|
||||
FlatIndex, InMemoryIndex, Lock, Options, OptionsBuilder, PythonRequirement,
|
||||
ResolverEnvironment, ResolverManifest, SatisfiesResult, UniversalMarker,
|
||||
};
|
||||
use uv_scripts::{Pep723ItemRef, Pep723Script};
|
||||
|
@ -142,6 +143,8 @@ pub(crate) async fn lock(
|
|||
LockTarget::Workspace(workspace) => ProjectInterpreter::discover(
|
||||
workspace,
|
||||
project_dir,
|
||||
// Don't enable any groups' requires-python for interpreter discovery
|
||||
&DependencyGroupsWithDefaults::none(),
|
||||
python.as_deref().map(PythonRequest::parse),
|
||||
&network_settings,
|
||||
python_preference,
|
||||
|
@ -437,8 +440,8 @@ async fn do_lock(
|
|||
let build_constraints = target.lower(build_constraints, index_locations, *sources)?;
|
||||
let dependency_groups = dependency_groups
|
||||
.into_iter()
|
||||
.map(|(name, requirements)| {
|
||||
let requirements = target.lower(requirements, index_locations, *sources)?;
|
||||
.map(|(name, group)| {
|
||||
let requirements = target.lower(group.requirements, index_locations, *sources)?;
|
||||
Ok((name, requirements))
|
||||
})
|
||||
.collect::<Result<BTreeMap<_, _>, ProjectError>>()?;
|
||||
|
|
|
@ -3,15 +3,15 @@ use std::path::{Path, PathBuf};
|
|||
|
||||
use itertools::Either;
|
||||
|
||||
use uv_configuration::SourceStrategy;
|
||||
use uv_configuration::{DependencyGroupsWithDefaults, SourceStrategy};
|
||||
use uv_distribution::LoweredRequirement;
|
||||
use uv_distribution_types::{Index, IndexLocations, Requirement};
|
||||
use uv_distribution_types::{Index, IndexLocations, Requirement, RequiresPython};
|
||||
use uv_normalize::{GroupName, PackageName};
|
||||
use uv_pep508::RequirementOrigin;
|
||||
use uv_pypi_types::{Conflicts, SupportedEnvironments, VerbatimParsedUrl};
|
||||
use uv_resolver::{Lock, LockVersion, RequiresPython, VERSION};
|
||||
use uv_resolver::{Lock, LockVersion, VERSION};
|
||||
use uv_scripts::Pep723Script;
|
||||
use uv_workspace::dependency_groups::DependencyGroupError;
|
||||
use uv_workspace::dependency_groups::{DependencyGroupError, FlatDependencyGroup};
|
||||
use uv_workspace::{Workspace, WorkspaceMember};
|
||||
|
||||
use crate::commands::project::{ProjectError, find_requires_python};
|
||||
|
@ -100,12 +100,9 @@ impl<'lock> LockTarget<'lock> {
|
|||
/// attached to any members within the target.
|
||||
pub(crate) fn dependency_groups(
|
||||
self,
|
||||
) -> Result<
|
||||
BTreeMap<GroupName, Vec<uv_pep508::Requirement<VerbatimParsedUrl>>>,
|
||||
DependencyGroupError,
|
||||
> {
|
||||
) -> Result<BTreeMap<GroupName, FlatDependencyGroup>, DependencyGroupError> {
|
||||
match self {
|
||||
Self::Workspace(workspace) => workspace.dependency_groups(),
|
||||
Self::Workspace(workspace) => workspace.workspace_dependency_groups(),
|
||||
Self::Script(_) => Ok(BTreeMap::new()),
|
||||
}
|
||||
}
|
||||
|
@ -219,7 +216,11 @@ impl<'lock> LockTarget<'lock> {
|
|||
#[allow(clippy::result_large_err)]
|
||||
pub(crate) fn requires_python(self) -> Result<Option<RequiresPython>, ProjectError> {
|
||||
match self {
|
||||
Self::Workspace(workspace) => find_requires_python(workspace),
|
||||
Self::Workspace(workspace) => {
|
||||
// When locking, don't try to enforce requires-python bounds that appear on groups
|
||||
let groups = DependencyGroupsWithDefaults::none();
|
||||
find_requires_python(workspace, &groups)
|
||||
}
|
||||
Self::Script(script) => Ok(script
|
||||
.metadata
|
||||
.requires_python
|
||||
|
|
|
@ -18,7 +18,8 @@ use uv_configuration::{
|
|||
use uv_dispatch::{BuildDispatch, SharedState};
|
||||
use uv_distribution::{DistributionDatabase, LoweredRequirement};
|
||||
use uv_distribution_types::{
|
||||
Index, Requirement, Resolution, UnresolvedRequirement, UnresolvedRequirementSpecification,
|
||||
Index, Requirement, RequiresPython, Resolution, UnresolvedRequirement,
|
||||
UnresolvedRequirementSpecification,
|
||||
};
|
||||
use uv_fs::{CWD, LockedFile, Simplified};
|
||||
use uv_git::ResolvedRepositoryReference;
|
||||
|
@ -35,8 +36,8 @@ use uv_python::{
|
|||
use uv_requirements::upgrade::{LockedRequirements, read_lock_requirements};
|
||||
use uv_requirements::{NamedRequirementsResolver, RequirementsSpecification};
|
||||
use uv_resolver::{
|
||||
FlatIndex, Lock, OptionsBuilder, Preference, PythonRequirement, RequiresPython,
|
||||
ResolverEnvironment, ResolverOutput,
|
||||
FlatIndex, Lock, OptionsBuilder, Preference, PythonRequirement, ResolverEnvironment,
|
||||
ResolverOutput,
|
||||
};
|
||||
use uv_scripts::Pep723ItemRef;
|
||||
use uv_settings::PythonInstallMirrors;
|
||||
|
@ -45,7 +46,7 @@ use uv_types::{BuildIsolation, EmptyInstalledPackages, HashStrategy};
|
|||
use uv_warnings::{warn_user, warn_user_once};
|
||||
use uv_workspace::dependency_groups::DependencyGroupError;
|
||||
use uv_workspace::pyproject::PyProjectToml;
|
||||
use uv_workspace::{Workspace, WorkspaceCache};
|
||||
use uv_workspace::{RequiresPythonSources, Workspace, WorkspaceCache};
|
||||
|
||||
use crate::commands::pip::loggers::{InstallLogger, ResolveLogger};
|
||||
use crate::commands::pip::operations::{Changelog, Modifications};
|
||||
|
@ -108,19 +109,28 @@ pub(crate) enum ProjectError {
|
|||
Conflict(#[from] ConflictError),
|
||||
|
||||
#[error(
|
||||
"The requested interpreter resolved to Python {0}, which is incompatible with the project's Python requirement: `{1}`"
|
||||
"The requested interpreter resolved to Python {_0}, which is incompatible with the project's Python requirement: `{_1}`{}",
|
||||
format_optional_requires_python_sources(_2, *_3)
|
||||
)]
|
||||
RequestedPythonProjectIncompatibility(Version, RequiresPython),
|
||||
RequestedPythonProjectIncompatibility(Version, RequiresPython, RequiresPythonSources, bool),
|
||||
|
||||
#[error(
|
||||
"The Python request from `{0}` resolved to Python {1}, which is incompatible with the project's Python requirement: `{2}`. Use `uv python pin` to update the `.python-version` file to a compatible version."
|
||||
"The Python request from `{_0}` resolved to Python {_1}, which is incompatible with the project's Python requirement: `{_2}`{}\nUse `uv python pin` to update the `.python-version` file to a compatible version",
|
||||
format_optional_requires_python_sources(_3, *_4)
|
||||
)]
|
||||
DotPythonVersionProjectIncompatibility(String, Version, RequiresPython),
|
||||
DotPythonVersionProjectIncompatibility(
|
||||
String,
|
||||
Version,
|
||||
RequiresPython,
|
||||
RequiresPythonSources,
|
||||
bool,
|
||||
),
|
||||
|
||||
#[error(
|
||||
"The resolved Python interpreter (Python {0}) is incompatible with the project's Python requirement: `{1}`"
|
||||
"The resolved Python interpreter (Python {_0}) is incompatible with the project's Python requirement: `{_1}`{}",
|
||||
format_optional_requires_python_sources(_2, *_3)
|
||||
)]
|
||||
RequiresPythonProjectIncompatibility(Version, RequiresPython),
|
||||
RequiresPythonProjectIncompatibility(Version, RequiresPython, RequiresPythonSources, bool),
|
||||
|
||||
#[error(
|
||||
"The requested interpreter resolved to Python {0}, which is incompatible with the script's Python requirement: `{1}`"
|
||||
|
@ -137,34 +147,6 @@ pub(crate) enum ProjectError {
|
|||
)]
|
||||
RequiresPythonScriptIncompatibility(Version, RequiresPython),
|
||||
|
||||
#[error("The requested interpreter resolved to Python {0}, which is incompatible with the project's Python requirement: `{1}`. However, a workspace member (`{member}`) supports Python {3}. To install the workspace member on its own, navigate to `{path}`, then run `{venv}` followed by `{install}`.", member = _2.cyan(), venv = format!("uv venv --python {_0}").green(), install = "uv pip install -e .".green(), path = _4.user_display().cyan() )]
|
||||
RequestedMemberIncompatibility(
|
||||
Version,
|
||||
RequiresPython,
|
||||
PackageName,
|
||||
VersionSpecifiers,
|
||||
PathBuf,
|
||||
),
|
||||
|
||||
#[error("The Python request from `{0}` resolved to Python {1}, which is incompatible with the project's Python requirement: `{2}`. However, a workspace member (`{member}`) supports Python {4}. To install the workspace member on its own, navigate to `{path}`, then run `{venv}` followed by `{install}`.", member = _3.cyan(), venv = format!("uv venv --python {_1}").green(), install = "uv pip install -e .".green(), path = _5.user_display().cyan() )]
|
||||
DotPythonVersionMemberIncompatibility(
|
||||
String,
|
||||
Version,
|
||||
RequiresPython,
|
||||
PackageName,
|
||||
VersionSpecifiers,
|
||||
PathBuf,
|
||||
),
|
||||
|
||||
#[error("The resolved Python interpreter (Python {0}) is incompatible with the project's Python requirement: `{1}`. However, a workspace member (`{member}`) supports Python {3}. To install the workspace member on its own, navigate to `{path}`, then run `{venv}` followed by `{install}`.", member = _2.cyan(), venv = format!("uv venv --python {_0}").green(), install = "uv pip install -e .".green(), path = _4.user_display().cyan() )]
|
||||
RequiresPythonMemberIncompatibility(
|
||||
Version,
|
||||
RequiresPython,
|
||||
PackageName,
|
||||
VersionSpecifiers,
|
||||
PathBuf,
|
||||
),
|
||||
|
||||
#[error("Group `{0}` is not defined in the project's `dependency-groups` table")]
|
||||
MissingGroupProject(GroupName),
|
||||
|
||||
|
@ -194,8 +176,11 @@ pub(crate) enum ProjectError {
|
|||
#[error("Environment markers `{0}` don't overlap with Python requirement `{1}`")]
|
||||
DisjointEnvironment(MarkerTreeContents, VersionSpecifiers),
|
||||
|
||||
#[error("The workspace contains conflicting Python requirements:\n{}", _0.iter().map(|(name, specifiers)| format!("- `{name}`: `{specifiers}`")).join("\n"))]
|
||||
DisjointRequiresPython(BTreeMap<PackageName, VersionSpecifiers>),
|
||||
#[error(
|
||||
"Found conflicting Python requirements:\n{}",
|
||||
format_requires_python_sources(_0)
|
||||
)]
|
||||
DisjointRequiresPython(BTreeMap<(PackageName, Option<GroupName>), VersionSpecifiers>),
|
||||
|
||||
#[error("Environment marker is empty")]
|
||||
EmptyEnvironment,
|
||||
|
@ -286,7 +271,7 @@ pub(crate) struct ConflictError {
|
|||
/// The items from the set that were enabled, and thus create the conflict.
|
||||
pub(crate) conflicts: Vec<ConflictPackage>,
|
||||
/// Enabled dependency groups with defaults applied.
|
||||
pub(crate) dev: DependencyGroupsWithDefaults,
|
||||
pub(crate) groups: DependencyGroupsWithDefaults,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ConflictError {
|
||||
|
@ -338,7 +323,7 @@ impl std::fmt::Display for ConflictError {
|
|||
.iter()
|
||||
.map(|conflict| match conflict {
|
||||
ConflictPackage::Group(group)
|
||||
if self.dev.contains_because_default(group) =>
|
||||
if self.groups.contains_because_default(group) =>
|
||||
format!("`{group}` (enabled by default)"),
|
||||
ConflictPackage::Group(group) => format!("`{group}`"),
|
||||
ConflictPackage::Extra(..) => unreachable!(),
|
||||
|
@ -358,7 +343,7 @@ impl std::fmt::Display for ConflictError {
|
|||
let conflict = match conflict {
|
||||
ConflictPackage::Extra(extra) => format!("extra `{extra}`"),
|
||||
ConflictPackage::Group(group)
|
||||
if self.dev.contains_because_default(group) =>
|
||||
if self.groups.contains_because_default(group) =>
|
||||
{
|
||||
format!("group `{group}` (enabled by default)")
|
||||
}
|
||||
|
@ -429,23 +414,16 @@ impl PlatformState {
|
|||
#[allow(clippy::result_large_err)]
|
||||
pub(crate) fn find_requires_python(
|
||||
workspace: &Workspace,
|
||||
groups: &DependencyGroupsWithDefaults,
|
||||
) -> Result<Option<RequiresPython>, ProjectError> {
|
||||
let requires_python = workspace.requires_python(groups)?;
|
||||
// If there are no `Requires-Python` specifiers in the workspace, return `None`.
|
||||
if workspace.requires_python().next().is_none() {
|
||||
if requires_python.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
match RequiresPython::intersection(
|
||||
workspace
|
||||
.requires_python()
|
||||
.map(|(.., specifiers)| specifiers),
|
||||
) {
|
||||
match RequiresPython::intersection(requires_python.iter().map(|(.., specifiers)| specifiers)) {
|
||||
Some(requires_python) => Ok(Some(requires_python)),
|
||||
None => Err(ProjectError::DisjointRequiresPython(
|
||||
workspace
|
||||
.requires_python()
|
||||
.map(|(name, specifiers)| (name.clone(), specifiers.clone()))
|
||||
.collect(),
|
||||
)),
|
||||
None => Err(ProjectError::DisjointRequiresPython(requires_python)),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -457,6 +435,7 @@ pub(crate) fn find_requires_python(
|
|||
pub(crate) fn validate_project_requires_python(
|
||||
interpreter: &Interpreter,
|
||||
workspace: Option<&Workspace>,
|
||||
groups: &DependencyGroupsWithDefaults,
|
||||
requires_python: &RequiresPython,
|
||||
source: &PythonRequestSource,
|
||||
) -> Result<(), ProjectError> {
|
||||
|
@ -464,57 +443,24 @@ pub(crate) fn validate_project_requires_python(
|
|||
return Ok(());
|
||||
}
|
||||
|
||||
// If the Python version is compatible with one of the workspace _members_, raise
|
||||
// a dedicated error. For example, if the workspace root requires Python >=3.12, but
|
||||
// a library in the workspace is compatible with Python >=3.8, the user may attempt
|
||||
// to sync on Python 3.8. This will fail, but we should provide a more helpful error
|
||||
// message.
|
||||
for (name, member) in workspace.into_iter().flat_map(Workspace::packages) {
|
||||
let Some(project) = member.pyproject_toml().project.as_ref() else {
|
||||
continue;
|
||||
};
|
||||
let Some(specifiers) = project.requires_python.as_ref() else {
|
||||
continue;
|
||||
};
|
||||
if specifiers.contains(interpreter.python_version()) {
|
||||
return match source {
|
||||
PythonRequestSource::UserRequest => {
|
||||
Err(ProjectError::RequestedMemberIncompatibility(
|
||||
interpreter.python_version().clone(),
|
||||
requires_python.clone(),
|
||||
name.clone(),
|
||||
specifiers.clone(),
|
||||
member.root().clone(),
|
||||
))
|
||||
}
|
||||
PythonRequestSource::DotPythonVersion(file) => {
|
||||
Err(ProjectError::DotPythonVersionMemberIncompatibility(
|
||||
file.path().user_display().to_string(),
|
||||
interpreter.python_version().clone(),
|
||||
requires_python.clone(),
|
||||
name.clone(),
|
||||
specifiers.clone(),
|
||||
member.root().clone(),
|
||||
))
|
||||
}
|
||||
PythonRequestSource::RequiresPython => {
|
||||
Err(ProjectError::RequiresPythonMemberIncompatibility(
|
||||
interpreter.python_version().clone(),
|
||||
requires_python.clone(),
|
||||
name.clone(),
|
||||
specifiers.clone(),
|
||||
member.root().clone(),
|
||||
))
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
// Find all the individual requires_python constraints that conflict
|
||||
let conflicting_requires = workspace
|
||||
.and_then(|workspace| workspace.requires_python(groups).ok())
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.filter(|(.., requires)| !requires.contains(interpreter.python_version()))
|
||||
.collect::<RequiresPythonSources>();
|
||||
let workspace_non_trivial = workspace
|
||||
.map(|workspace| workspace.packages().len() > 1)
|
||||
.unwrap_or(false);
|
||||
|
||||
match source {
|
||||
PythonRequestSource::UserRequest => {
|
||||
Err(ProjectError::RequestedPythonProjectIncompatibility(
|
||||
interpreter.python_version().clone(),
|
||||
requires_python.clone(),
|
||||
conflicting_requires,
|
||||
workspace_non_trivial,
|
||||
))
|
||||
}
|
||||
PythonRequestSource::DotPythonVersion(file) => {
|
||||
|
@ -522,12 +468,16 @@ pub(crate) fn validate_project_requires_python(
|
|||
file.path().user_display().to_string(),
|
||||
interpreter.python_version().clone(),
|
||||
requires_python.clone(),
|
||||
conflicting_requires,
|
||||
workspace_non_trivial,
|
||||
))
|
||||
}
|
||||
PythonRequestSource::RequiresPython => {
|
||||
Err(ProjectError::RequiresPythonProjectIncompatibility(
|
||||
interpreter.python_version().clone(),
|
||||
requires_python.clone(),
|
||||
conflicting_requires,
|
||||
workspace_non_trivial,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
@ -738,7 +688,13 @@ impl ScriptInterpreter {
|
|||
|
||||
if let Err(err) = match requires_python {
|
||||
Some((requires_python, RequiresPythonSource::Project)) => {
|
||||
validate_project_requires_python(&interpreter, workspace, &requires_python, &source)
|
||||
validate_project_requires_python(
|
||||
&interpreter,
|
||||
workspace,
|
||||
&DependencyGroupsWithDefaults::none(),
|
||||
&requires_python,
|
||||
&source,
|
||||
)
|
||||
}
|
||||
Some((requires_python, RequiresPythonSource::Script)) => {
|
||||
validate_script_requires_python(&interpreter, &requires_python, &source)
|
||||
|
@ -874,6 +830,7 @@ impl ProjectInterpreter {
|
|||
pub(crate) async fn discover(
|
||||
workspace: &Workspace,
|
||||
project_dir: &Path,
|
||||
groups: &DependencyGroupsWithDefaults,
|
||||
python_request: Option<PythonRequest>,
|
||||
network_settings: &NetworkSettings,
|
||||
python_preference: PythonPreference,
|
||||
|
@ -890,8 +847,14 @@ impl ProjectInterpreter {
|
|||
source,
|
||||
python_request,
|
||||
requires_python,
|
||||
} = WorkspacePython::from_request(python_request, Some(workspace), project_dir, no_config)
|
||||
.await?;
|
||||
} = WorkspacePython::from_request(
|
||||
python_request,
|
||||
Some(workspace),
|
||||
groups,
|
||||
project_dir,
|
||||
no_config,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Read from the virtual environment first.
|
||||
let root = workspace.venv(active);
|
||||
|
@ -1002,6 +965,7 @@ impl ProjectInterpreter {
|
|||
validate_project_requires_python(
|
||||
&interpreter,
|
||||
Some(workspace),
|
||||
groups,
|
||||
requires_python,
|
||||
&source,
|
||||
)?;
|
||||
|
@ -1081,10 +1045,14 @@ impl WorkspacePython {
|
|||
pub(crate) async fn from_request(
|
||||
python_request: Option<PythonRequest>,
|
||||
workspace: Option<&Workspace>,
|
||||
groups: &DependencyGroupsWithDefaults,
|
||||
project_dir: &Path,
|
||||
no_config: bool,
|
||||
) -> Result<Self, ProjectError> {
|
||||
let requires_python = workspace.map(find_requires_python).transpose()?.flatten();
|
||||
let requires_python = workspace
|
||||
.map(|workspace| find_requires_python(workspace, groups))
|
||||
.transpose()?
|
||||
.flatten();
|
||||
|
||||
let workspace_root = workspace.map(Workspace::install_path);
|
||||
|
||||
|
@ -1165,6 +1133,8 @@ impl ScriptPython {
|
|||
} = WorkspacePython::from_request(
|
||||
python_request,
|
||||
workspace,
|
||||
// Scripts have no groups to hang requires-python settings off of
|
||||
&DependencyGroupsWithDefaults::none(),
|
||||
script.path().and_then(Path::parent).unwrap_or(&**CWD),
|
||||
no_config,
|
||||
)
|
||||
|
@ -1231,6 +1201,7 @@ impl ProjectEnvironment {
|
|||
/// Initialize a virtual environment for the current project.
|
||||
pub(crate) async fn get_or_init(
|
||||
workspace: &Workspace,
|
||||
groups: &DependencyGroupsWithDefaults,
|
||||
python: Option<PythonRequest>,
|
||||
install_mirrors: &PythonInstallMirrors,
|
||||
network_settings: &NetworkSettings,
|
||||
|
@ -1249,6 +1220,7 @@ impl ProjectEnvironment {
|
|||
match ProjectInterpreter::discover(
|
||||
workspace,
|
||||
workspace.install_path().as_ref(),
|
||||
groups,
|
||||
python,
|
||||
network_settings,
|
||||
python_preference,
|
||||
|
@ -2434,7 +2406,7 @@ pub(crate) fn default_dependency_groups(
|
|||
pub(crate) fn detect_conflicts(
|
||||
lock: &Lock,
|
||||
extras: &ExtrasSpecification,
|
||||
dev: &DependencyGroupsWithDefaults,
|
||||
groups: &DependencyGroupsWithDefaults,
|
||||
) -> Result<(), ProjectError> {
|
||||
// Note that we need to collect all extras and groups that match in
|
||||
// a particular set, since extras can be declared as conflicting with
|
||||
|
@ -2453,7 +2425,7 @@ pub(crate) fn detect_conflicts(
|
|||
}
|
||||
if item
|
||||
.group()
|
||||
.map(|group| dev.contains(group))
|
||||
.map(|group| groups.contains(group))
|
||||
.unwrap_or(false)
|
||||
{
|
||||
conflicts.push(item.conflict().clone());
|
||||
|
@ -2463,7 +2435,7 @@ pub(crate) fn detect_conflicts(
|
|||
return Err(ProjectError::Conflict(ConflictError {
|
||||
set: set.clone(),
|
||||
conflicts,
|
||||
dev: dev.clone(),
|
||||
groups: groups.clone(),
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
@ -2677,6 +2649,50 @@ fn cache_name(name: &str) -> Option<Cow<'_, str>> {
|
|||
}
|
||||
}
|
||||
|
||||
fn format_requires_python_sources(conflicts: &RequiresPythonSources) -> String {
|
||||
conflicts
|
||||
.iter()
|
||||
.map(|((package, group), specifiers)| {
|
||||
if let Some(group) = group {
|
||||
format!("- {package}:{group}: {specifiers}")
|
||||
} else {
|
||||
format!("- {package}: {specifiers}")
|
||||
}
|
||||
})
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
fn format_optional_requires_python_sources(
|
||||
conflicts: &RequiresPythonSources,
|
||||
workspace_non_trivial: bool,
|
||||
) -> String {
|
||||
// If there's lots of conflicts, print a list
|
||||
if conflicts.len() > 1 {
|
||||
return format!(
|
||||
".\nThe following `requires-python` declarations do not permit this version:\n{}",
|
||||
format_requires_python_sources(conflicts)
|
||||
);
|
||||
}
|
||||
// If there's one conflict, give a clean message
|
||||
if conflicts.len() == 1 {
|
||||
let ((package, group), _) = conflicts.iter().next().unwrap();
|
||||
if let Some(group) = group {
|
||||
if workspace_non_trivial {
|
||||
return format!(
|
||||
" (from workspace member `{package}`'s `tool.uv.dependency-groups.{group}.requires-python`)."
|
||||
);
|
||||
}
|
||||
return format!(" (from `tool.uv.dependency-groups.{group}.requires-python`).");
|
||||
}
|
||||
if workspace_non_trivial {
|
||||
return format!(" (from workspace member `{package}`'s `project.requires-python`).");
|
||||
}
|
||||
return " (from `project.requires-python`)".to_owned();
|
||||
}
|
||||
// Otherwise don't elaborate
|
||||
String::new()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
|
@ -13,7 +13,7 @@ use uv_configuration::{
|
|||
PreviewMode,
|
||||
};
|
||||
use uv_fs::Simplified;
|
||||
use uv_normalize::{DEV_DEPENDENCIES, DefaultExtras};
|
||||
use uv_normalize::{DEV_DEPENDENCIES, DefaultExtras, DefaultGroups};
|
||||
use uv_pep508::PackageName;
|
||||
use uv_python::{PythonDownloads, PythonPreference, PythonRequest};
|
||||
use uv_scripts::{Pep723ItemRef, Pep723Metadata, Pep723Script};
|
||||
|
@ -202,6 +202,14 @@ pub(crate) async fn remove(
|
|||
// Update the `pypackage.toml` in-memory.
|
||||
let target = target.update(&content)?;
|
||||
|
||||
// Determine enabled groups and extras
|
||||
let default_groups = match &target {
|
||||
RemoveTarget::Project(project) => default_dependency_groups(project.pyproject_toml())?,
|
||||
RemoveTarget::Script(_) => DefaultGroups::default(),
|
||||
};
|
||||
let groups = DependencyGroups::default().with_defaults(default_groups);
|
||||
let extras = ExtrasSpecification::default().with_defaults(DefaultExtras::default());
|
||||
|
||||
// Convert to an `AddTarget` by attaching the appropriate interpreter or environment.
|
||||
let target = match target {
|
||||
RemoveTarget::Project(project) => {
|
||||
|
@ -210,6 +218,7 @@ pub(crate) async fn remove(
|
|||
let interpreter = ProjectInterpreter::discover(
|
||||
project.workspace(),
|
||||
project_dir,
|
||||
&groups,
|
||||
python.as_deref().map(PythonRequest::parse),
|
||||
&network_settings,
|
||||
python_preference,
|
||||
|
@ -229,6 +238,7 @@ pub(crate) async fn remove(
|
|||
// Discover or create the virtual environment.
|
||||
let environment = ProjectEnvironment::get_or_init(
|
||||
project.workspace(),
|
||||
&groups,
|
||||
python.as_deref().map(PythonRequest::parse),
|
||||
&install_mirrors,
|
||||
&network_settings,
|
||||
|
@ -314,12 +324,6 @@ pub(crate) async fn remove(
|
|||
return Ok(ExitStatus::Success);
|
||||
};
|
||||
|
||||
// Determine the default groups to include.
|
||||
let default_groups = default_dependency_groups(project.pyproject_toml())?;
|
||||
|
||||
// Determine the default extras to include.
|
||||
let default_extras = DefaultExtras::default();
|
||||
|
||||
// Identify the installation target.
|
||||
let target = match &project {
|
||||
VirtualProject::Project(project) => InstallTarget::Project {
|
||||
|
@ -338,8 +342,8 @@ pub(crate) async fn remove(
|
|||
match project::sync::do_sync(
|
||||
target,
|
||||
venv,
|
||||
&ExtrasSpecification::default().with_defaults(default_extras),
|
||||
&DependencyGroups::default().with_defaults(default_groups),
|
||||
&extras,
|
||||
&groups,
|
||||
EditableMode::Editable,
|
||||
InstallOptions::default(),
|
||||
Modifications::Exact,
|
||||
|
|
|
@ -78,7 +78,7 @@ pub(crate) async fn run(
|
|||
no_project: bool,
|
||||
no_config: bool,
|
||||
extras: ExtrasSpecification,
|
||||
dev: DependencyGroups,
|
||||
groups: DependencyGroups,
|
||||
editable: EditableMode,
|
||||
modifications: Modifications,
|
||||
python: Option<String>,
|
||||
|
@ -291,7 +291,7 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl
|
|||
target,
|
||||
&environment,
|
||||
&extras.with_defaults(DefaultExtras::default()),
|
||||
&dev.with_defaults(DefaultGroups::default()),
|
||||
&groups.with_defaults(DefaultGroups::default()),
|
||||
editable,
|
||||
install_options,
|
||||
modifications,
|
||||
|
@ -468,7 +468,7 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl
|
|||
if !extras.is_empty() {
|
||||
warn_user!("Extras are not supported for Python scripts with inline metadata");
|
||||
}
|
||||
for flag in dev.history().as_flags_pretty() {
|
||||
for flag in groups.history().as_flags_pretty() {
|
||||
warn_user!("`{flag}` is not supported for Python scripts with inline metadata");
|
||||
}
|
||||
if all_packages {
|
||||
|
@ -543,7 +543,7 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl
|
|||
for flag in extras.history().as_flags_pretty() {
|
||||
warn_user!("`{flag}` has no effect when used alongside `--no-project`");
|
||||
}
|
||||
for flag in dev.history().as_flags_pretty() {
|
||||
for flag in groups.history().as_flags_pretty() {
|
||||
warn_user!("`{flag}` has no effect when used alongside `--no-project`");
|
||||
}
|
||||
if locked {
|
||||
|
@ -560,7 +560,7 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl
|
|||
for flag in extras.history().as_flags_pretty() {
|
||||
warn_user!("`{flag}` has no effect when used outside of a project");
|
||||
}
|
||||
for flag in dev.history().as_flags_pretty() {
|
||||
for flag in groups.history().as_flags_pretty() {
|
||||
warn_user!("`{flag}` has no effect when used outside of a project");
|
||||
}
|
||||
if locked {
|
||||
|
@ -583,6 +583,11 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl
|
|||
project.workspace().install_path().display()
|
||||
);
|
||||
}
|
||||
// Determine the groups and extras to include.
|
||||
let default_groups = default_dependency_groups(project.pyproject_toml())?;
|
||||
let default_extras = DefaultExtras::default();
|
||||
let groups = groups.with_defaults(default_groups);
|
||||
let extras = extras.with_defaults(default_extras);
|
||||
|
||||
let venv = if isolated {
|
||||
debug!("Creating isolated virtual environment");
|
||||
|
@ -602,6 +607,7 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl
|
|||
} = WorkspacePython::from_request(
|
||||
python.as_deref().map(PythonRequest::parse),
|
||||
Some(project.workspace()),
|
||||
&groups,
|
||||
project_dir,
|
||||
no_config,
|
||||
)
|
||||
|
@ -626,6 +632,7 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl
|
|||
validate_project_requires_python(
|
||||
&interpreter,
|
||||
Some(project.workspace()),
|
||||
&groups,
|
||||
requires_python,
|
||||
&source,
|
||||
)?;
|
||||
|
@ -647,6 +654,7 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl
|
|||
// project.
|
||||
ProjectEnvironment::get_or_init(
|
||||
project.workspace(),
|
||||
&groups,
|
||||
python.as_deref().map(PythonRequest::parse),
|
||||
&install_mirrors,
|
||||
&network_settings,
|
||||
|
@ -677,14 +685,6 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl
|
|||
.map(|lock| (lock, project.workspace().install_path().to_owned()));
|
||||
}
|
||||
} else {
|
||||
// Validate that any referenced dependency groups are defined in the workspace.
|
||||
|
||||
// Determine the default groups to include.
|
||||
let default_groups = default_dependency_groups(project.pyproject_toml())?;
|
||||
|
||||
// Determine the default extras to include.
|
||||
let default_extras = DefaultExtras::default();
|
||||
|
||||
// Determine the lock mode.
|
||||
let mode = if frozen {
|
||||
LockMode::Frozen
|
||||
|
@ -769,18 +769,15 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl
|
|||
};
|
||||
|
||||
let install_options = InstallOptions::default();
|
||||
let dev = dev.with_defaults(default_groups);
|
||||
let extras = extras.with_defaults(default_extras);
|
||||
|
||||
// Validate that the set of requested extras and development groups are defined in the lockfile.
|
||||
target.validate_extras(&extras)?;
|
||||
target.validate_groups(&dev)?;
|
||||
target.validate_groups(&groups)?;
|
||||
|
||||
match project::sync::do_sync(
|
||||
target,
|
||||
&venv,
|
||||
&extras,
|
||||
&dev,
|
||||
&groups,
|
||||
editable,
|
||||
install_options,
|
||||
modifications,
|
||||
|
|
|
@ -57,7 +57,7 @@ pub(crate) async fn sync(
|
|||
all_packages: bool,
|
||||
package: Option<PackageName>,
|
||||
extras: ExtrasSpecification,
|
||||
dev: DependencyGroups,
|
||||
groups: DependencyGroups,
|
||||
editable: EditableMode,
|
||||
install_options: InstallOptions,
|
||||
modifications: Modifications,
|
||||
|
@ -116,23 +116,24 @@ pub(crate) async fn sync(
|
|||
SyncTarget::Project(project)
|
||||
};
|
||||
|
||||
// Determine the default groups to include.
|
||||
// Determine the groups and extras to include.
|
||||
let default_groups = match &target {
|
||||
SyncTarget::Project(project) => default_dependency_groups(project.pyproject_toml())?,
|
||||
SyncTarget::Script(..) => DefaultGroups::default(),
|
||||
};
|
||||
|
||||
// Determine the default extras to include.
|
||||
let default_extras = match &target {
|
||||
SyncTarget::Project(_project) => DefaultExtras::default(),
|
||||
SyncTarget::Script(..) => DefaultExtras::default(),
|
||||
};
|
||||
let groups = groups.with_defaults(default_groups);
|
||||
let extras = extras.with_defaults(default_extras);
|
||||
|
||||
// Discover or create the virtual environment.
|
||||
let environment = match &target {
|
||||
SyncTarget::Project(project) => SyncEnvironment::Project(
|
||||
ProjectEnvironment::get_or_init(
|
||||
project.workspace(),
|
||||
&groups,
|
||||
python.as_deref().map(PythonRequest::parse),
|
||||
&install_mirrors,
|
||||
&network_settings,
|
||||
|
@ -437,8 +438,8 @@ pub(crate) async fn sync(
|
|||
match do_sync(
|
||||
sync_target,
|
||||
&environment,
|
||||
&extras.with_defaults(default_extras),
|
||||
&dev.with_defaults(default_groups),
|
||||
&extras,
|
||||
&groups,
|
||||
editable,
|
||||
install_options,
|
||||
modifications,
|
||||
|
@ -573,7 +574,7 @@ pub(super) async fn do_sync(
|
|||
target: InstallTarget<'_>,
|
||||
venv: &PythonEnvironment,
|
||||
extras: &ExtrasSpecificationWithDefaults,
|
||||
dev: &DependencyGroupsWithDefaults,
|
||||
groups: &DependencyGroupsWithDefaults,
|
||||
editable: EditableMode,
|
||||
install_options: InstallOptions,
|
||||
modifications: Modifications,
|
||||
|
@ -624,11 +625,11 @@ pub(super) async fn do_sync(
|
|||
}
|
||||
|
||||
// Validate that the set of requested extras and development groups are compatible.
|
||||
detect_conflicts(target.lock(), extras, dev)?;
|
||||
detect_conflicts(target.lock(), extras, groups)?;
|
||||
|
||||
// Validate that the set of requested extras and development groups are defined in the lockfile.
|
||||
target.validate_extras(extras)?;
|
||||
target.validate_groups(dev)?;
|
||||
target.validate_groups(groups)?;
|
||||
|
||||
// Determine the markers to use for resolution.
|
||||
let marker_env = venv.interpreter().resolver_marker_environment();
|
||||
|
@ -665,7 +666,7 @@ pub(super) async fn do_sync(
|
|||
&marker_env,
|
||||
tags,
|
||||
extras,
|
||||
dev,
|
||||
groups,
|
||||
build_options,
|
||||
&install_options,
|
||||
)?;
|
||||
|
|
|
@ -34,7 +34,7 @@ use crate::settings::{NetworkSettings, ResolverSettings};
|
|||
#[allow(clippy::fn_params_excessive_bools)]
|
||||
pub(crate) async fn tree(
|
||||
project_dir: &Path,
|
||||
dev: DependencyGroups,
|
||||
groups: DependencyGroups,
|
||||
locked: bool,
|
||||
frozen: bool,
|
||||
universal: bool,
|
||||
|
@ -71,11 +71,12 @@ pub(crate) async fn tree(
|
|||
LockTarget::Workspace(&workspace)
|
||||
};
|
||||
|
||||
// Determine the default groups to include.
|
||||
let defaults = match target {
|
||||
// Determine the groups to include.
|
||||
let default_groups = match target {
|
||||
LockTarget::Workspace(workspace) => default_dependency_groups(workspace.pyproject_toml())?,
|
||||
LockTarget::Script(_) => DefaultGroups::default(),
|
||||
};
|
||||
let groups = groups.with_defaults(default_groups);
|
||||
|
||||
let native_tls = network_settings.native_tls;
|
||||
|
||||
|
@ -102,6 +103,7 @@ pub(crate) async fn tree(
|
|||
LockTarget::Workspace(workspace) => ProjectInterpreter::discover(
|
||||
workspace,
|
||||
project_dir,
|
||||
&groups,
|
||||
python.as_deref().map(PythonRequest::parse),
|
||||
network_settings,
|
||||
python_preference,
|
||||
|
@ -271,7 +273,7 @@ pub(crate) async fn tree(
|
|||
depth.into(),
|
||||
&prune,
|
||||
&package,
|
||||
&dev.with_defaults(defaults),
|
||||
&groups,
|
||||
no_dedupe,
|
||||
invert,
|
||||
);
|
||||
|
|
|
@ -10,8 +10,8 @@ use uv_cache::Cache;
|
|||
use uv_cli::version::VersionInfo;
|
||||
use uv_cli::{VersionBump, VersionFormat};
|
||||
use uv_configuration::{
|
||||
Concurrency, DependencyGroups, DryRun, EditableMode, ExtrasSpecification, InstallOptions,
|
||||
PreviewMode,
|
||||
Concurrency, DependencyGroups, DependencyGroupsWithDefaults, DryRun, EditableMode,
|
||||
ExtrasSpecification, InstallOptions, PreviewMode,
|
||||
};
|
||||
use uv_fs::Simplified;
|
||||
use uv_normalize::DefaultExtras;
|
||||
|
@ -285,6 +285,7 @@ async fn print_frozen_version(
|
|||
let interpreter = ProjectInterpreter::discover(
|
||||
project.workspace(),
|
||||
project_dir,
|
||||
&DependencyGroupsWithDefaults::none(),
|
||||
python.as_deref().map(PythonRequest::parse),
|
||||
&network_settings,
|
||||
python_preference,
|
||||
|
@ -378,12 +379,20 @@ async fn lock_and_sync(
|
|||
return Ok(ExitStatus::Success);
|
||||
}
|
||||
|
||||
// Determine the groups and extras that should be enabled.
|
||||
let default_groups = default_dependency_groups(project.pyproject_toml())?;
|
||||
let default_extras = DefaultExtras::default();
|
||||
let groups = DependencyGroups::default().with_defaults(default_groups);
|
||||
let extras = ExtrasSpecification::from_all_extras().with_defaults(default_extras);
|
||||
let install_options = InstallOptions::default();
|
||||
|
||||
// Convert to an `AddTarget` by attaching the appropriate interpreter or environment.
|
||||
let target = if no_sync {
|
||||
// Discover the interpreter.
|
||||
let interpreter = ProjectInterpreter::discover(
|
||||
project.workspace(),
|
||||
project_dir,
|
||||
&groups,
|
||||
python.as_deref().map(PythonRequest::parse),
|
||||
&network_settings,
|
||||
python_preference,
|
||||
|
@ -403,6 +412,7 @@ async fn lock_and_sync(
|
|||
// Discover or create the virtual environment.
|
||||
let environment = ProjectEnvironment::get_or_init(
|
||||
project.workspace(),
|
||||
&groups,
|
||||
python.as_deref().map(PythonRequest::parse),
|
||||
&install_mirrors,
|
||||
&network_settings,
|
||||
|
@ -466,15 +476,6 @@ async fn lock_and_sync(
|
|||
};
|
||||
|
||||
// Perform a full sync, because we don't know what exactly is affected by the version.
|
||||
// TODO(ibraheem): Should we accept CLI overrides for this? Should we even sync here?
|
||||
let extras = ExtrasSpecification::from_all_extras();
|
||||
let install_options = InstallOptions::default();
|
||||
|
||||
// Determine the default groups to include.
|
||||
let default_groups = default_dependency_groups(project.pyproject_toml())?;
|
||||
|
||||
// Determine the default extras to include.
|
||||
let default_extras = DefaultExtras::default();
|
||||
|
||||
// Identify the installation target.
|
||||
let target = match &project {
|
||||
|
@ -494,8 +495,8 @@ async fn lock_and_sync(
|
|||
match project::sync::do_sync(
|
||||
target,
|
||||
venv,
|
||||
&extras.with_defaults(default_extras),
|
||||
&DependencyGroups::default().with_defaults(default_groups),
|
||||
&extras,
|
||||
&groups,
|
||||
EditableMode::Editable,
|
||||
install_options,
|
||||
Modifications::Sufficient,
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
use anyhow::Result;
|
||||
use std::fmt::Write;
|
||||
use std::path::Path;
|
||||
use uv_configuration::DependencyGroupsWithDefaults;
|
||||
|
||||
use uv_cache::Cache;
|
||||
use uv_fs::Simplified;
|
||||
|
@ -56,6 +57,8 @@ pub(crate) async fn find(
|
|||
}
|
||||
};
|
||||
|
||||
// Don't enable the requires-python settings on groups
|
||||
let groups = DependencyGroupsWithDefaults::none();
|
||||
let WorkspacePython {
|
||||
source,
|
||||
python_request,
|
||||
|
@ -63,6 +66,7 @@ pub(crate) async fn find(
|
|||
} = WorkspacePython::from_request(
|
||||
request.map(|request| PythonRequest::parse(&request)),
|
||||
project.as_ref().map(VirtualProject::workspace),
|
||||
&groups,
|
||||
project_dir,
|
||||
no_config,
|
||||
)
|
||||
|
@ -80,6 +84,7 @@ pub(crate) async fn find(
|
|||
match validate_project_requires_python(
|
||||
python.interpreter(),
|
||||
project.as_ref().map(VirtualProject::workspace),
|
||||
&groups,
|
||||
&requires_python,
|
||||
&source,
|
||||
) {
|
||||
|
|
|
@ -8,6 +8,7 @@ use tracing::debug;
|
|||
|
||||
use uv_cache::Cache;
|
||||
use uv_client::BaseClientBuilder;
|
||||
use uv_configuration::DependencyGroupsWithDefaults;
|
||||
use uv_dirs::user_uv_config_dir;
|
||||
use uv_fs::Simplified;
|
||||
use uv_python::{
|
||||
|
@ -322,6 +323,9 @@ struct Pin<'a> {
|
|||
|
||||
/// Checks if the pinned Python version is compatible with the workspace/project's `Requires-Python`.
|
||||
fn assert_pin_compatible_with_project(pin: &Pin, virtual_project: &VirtualProject) -> Result<()> {
|
||||
// Don't factor in requires-python settings on dependency-groups
|
||||
let groups = DependencyGroupsWithDefaults::none();
|
||||
|
||||
let (requires_python, project_type) = match virtual_project {
|
||||
VirtualProject::Project(project_workspace) => {
|
||||
debug!(
|
||||
|
@ -329,7 +333,8 @@ fn assert_pin_compatible_with_project(pin: &Pin, virtual_project: &VirtualProjec
|
|||
project_workspace.project_name(),
|
||||
project_workspace.workspace().install_path().display()
|
||||
);
|
||||
let requires_python = find_requires_python(project_workspace.workspace())?;
|
||||
|
||||
let requires_python = find_requires_python(project_workspace.workspace(), &groups)?;
|
||||
(requires_python, "project")
|
||||
}
|
||||
VirtualProject::NonProject(workspace) => {
|
||||
|
@ -337,7 +342,7 @@ fn assert_pin_compatible_with_project(pin: &Pin, virtual_project: &VirtualProjec
|
|||
"Discovered virtual workspace at: {}",
|
||||
workspace.install_path().display()
|
||||
);
|
||||
let requires_python = find_requires_python(workspace)?;
|
||||
let requires_python = find_requires_python(workspace, &groups)?;
|
||||
(requires_python, "workspace")
|
||||
}
|
||||
};
|
||||
|
|
|
@ -13,14 +13,15 @@ use thiserror::Error;
|
|||
use uv_cache::Cache;
|
||||
use uv_client::{BaseClientBuilder, FlatIndexClient, RegistryClientBuilder};
|
||||
use uv_configuration::{
|
||||
BuildOptions, Concurrency, ConfigSettings, Constraints, IndexStrategy, KeyringProviderType,
|
||||
NoBinary, NoBuild, PreviewMode, SourceStrategy,
|
||||
BuildOptions, Concurrency, ConfigSettings, Constraints, DependencyGroups, IndexStrategy,
|
||||
KeyringProviderType, NoBinary, NoBuild, PreviewMode, SourceStrategy,
|
||||
};
|
||||
use uv_dispatch::{BuildDispatch, SharedState};
|
||||
use uv_distribution_types::Requirement;
|
||||
use uv_distribution_types::{DependencyMetadata, Index, IndexLocations};
|
||||
use uv_fs::Simplified;
|
||||
use uv_install_wheel::LinkMode;
|
||||
use uv_normalize::DefaultGroups;
|
||||
use uv_python::{
|
||||
EnvironmentPreference, PythonDownloads, PythonInstallation, PythonPreference, PythonRequest,
|
||||
};
|
||||
|
@ -39,6 +40,8 @@ use crate::commands::reporters::PythonDownloadReporter;
|
|||
use crate::printer::Printer;
|
||||
use crate::settings::NetworkSettings;
|
||||
|
||||
use super::project::default_dependency_groups;
|
||||
|
||||
/// Create a virtual environment.
|
||||
#[allow(clippy::unnecessary_wraps, clippy::fn_params_excessive_bools)]
|
||||
pub(crate) async fn venv(
|
||||
|
@ -197,6 +200,13 @@ async fn venv_impl(
|
|||
|
||||
let reporter = PythonDownloadReporter::single(printer);
|
||||
|
||||
// If the default dependency-groups demand a higher requires-python
|
||||
// we should bias an empty venv to that to avoid churn.
|
||||
let default_groups = match &project {
|
||||
Some(project) => default_dependency_groups(project.pyproject_toml()).into_diagnostic()?,
|
||||
None => DefaultGroups::default(),
|
||||
};
|
||||
let groups = DependencyGroups::default().with_defaults(default_groups);
|
||||
let WorkspacePython {
|
||||
source,
|
||||
python_request,
|
||||
|
@ -204,6 +214,7 @@ async fn venv_impl(
|
|||
} = WorkspacePython::from_request(
|
||||
python_request.map(PythonRequest::parse),
|
||||
project.as_ref().map(VirtualProject::workspace),
|
||||
&groups,
|
||||
project_dir,
|
||||
no_config,
|
||||
)
|
||||
|
@ -246,6 +257,7 @@ async fn venv_impl(
|
|||
match validate_project_requires_python(
|
||||
&interpreter,
|
||||
project.as_ref().map(VirtualProject::workspace),
|
||||
&groups,
|
||||
&requires_python,
|
||||
&source,
|
||||
) {
|
||||
|
|
|
@ -1704,7 +1704,7 @@ async fn run_project(
|
|||
args.no_project,
|
||||
no_config,
|
||||
args.extras,
|
||||
args.dev,
|
||||
args.groups,
|
||||
args.editable,
|
||||
args.modifications,
|
||||
args.python,
|
||||
|
@ -1752,7 +1752,7 @@ async fn run_project(
|
|||
args.all_packages,
|
||||
args.package,
|
||||
args.extras,
|
||||
args.dev,
|
||||
args.groups,
|
||||
args.editable,
|
||||
args.install_options,
|
||||
args.modifications,
|
||||
|
@ -2043,7 +2043,7 @@ async fn run_project(
|
|||
|
||||
Box::pin(commands::tree(
|
||||
project_dir,
|
||||
args.dev,
|
||||
args.groups,
|
||||
args.locked,
|
||||
args.frozen,
|
||||
args.universal,
|
||||
|
@ -2095,7 +2095,7 @@ async fn run_project(
|
|||
args.install_options,
|
||||
args.output_file,
|
||||
args.extras,
|
||||
args.dev,
|
||||
args.groups,
|
||||
args.editable,
|
||||
args.locked,
|
||||
args.frozen,
|
||||
|
|
|
@ -313,7 +313,7 @@ pub(crate) struct RunSettings {
|
|||
pub(crate) locked: bool,
|
||||
pub(crate) frozen: bool,
|
||||
pub(crate) extras: ExtrasSpecification,
|
||||
pub(crate) dev: DependencyGroups,
|
||||
pub(crate) groups: DependencyGroups,
|
||||
pub(crate) editable: EditableMode,
|
||||
pub(crate) modifications: Modifications,
|
||||
pub(crate) with: Vec<String>,
|
||||
|
@ -404,7 +404,7 @@ impl RunSettings {
|
|||
vec![],
|
||||
flag(all_extras, no_all_extras).unwrap_or_default(),
|
||||
),
|
||||
dev: DependencyGroups::from_args(
|
||||
groups: DependencyGroups::from_args(
|
||||
dev,
|
||||
no_dev,
|
||||
only_dev,
|
||||
|
@ -1098,7 +1098,7 @@ pub(crate) struct SyncSettings {
|
|||
pub(crate) script: Option<PathBuf>,
|
||||
pub(crate) active: Option<bool>,
|
||||
pub(crate) extras: ExtrasSpecification,
|
||||
pub(crate) dev: DependencyGroups,
|
||||
pub(crate) groups: DependencyGroups,
|
||||
pub(crate) editable: EditableMode,
|
||||
pub(crate) install_options: InstallOptions,
|
||||
pub(crate) modifications: Modifications,
|
||||
|
@ -1180,7 +1180,7 @@ impl SyncSettings {
|
|||
vec![],
|
||||
flag(all_extras, no_all_extras).unwrap_or_default(),
|
||||
),
|
||||
dev: DependencyGroups::from_args(
|
||||
groups: DependencyGroups::from_args(
|
||||
dev,
|
||||
no_dev,
|
||||
only_dev,
|
||||
|
@ -1578,7 +1578,7 @@ impl VersionSettings {
|
|||
#[allow(clippy::struct_excessive_bools)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct TreeSettings {
|
||||
pub(crate) dev: DependencyGroups,
|
||||
pub(crate) groups: DependencyGroups,
|
||||
pub(crate) locked: bool,
|
||||
pub(crate) frozen: bool,
|
||||
pub(crate) universal: bool,
|
||||
|
@ -1626,7 +1626,7 @@ impl TreeSettings {
|
|||
.unwrap_or_default();
|
||||
|
||||
Self {
|
||||
dev: DependencyGroups::from_args(
|
||||
groups: DependencyGroups::from_args(
|
||||
dev,
|
||||
no_dev,
|
||||
only_dev,
|
||||
|
@ -1664,7 +1664,7 @@ pub(crate) struct ExportSettings {
|
|||
pub(crate) package: Option<PackageName>,
|
||||
pub(crate) prune: Vec<PackageName>,
|
||||
pub(crate) extras: ExtrasSpecification,
|
||||
pub(crate) dev: DependencyGroups,
|
||||
pub(crate) groups: DependencyGroups,
|
||||
pub(crate) editable: EditableMode,
|
||||
pub(crate) hashes: bool,
|
||||
pub(crate) install_options: InstallOptions,
|
||||
|
@ -1739,7 +1739,7 @@ impl ExportSettings {
|
|||
vec![],
|
||||
flag(all_extras, no_all_extras).unwrap_or_default(),
|
||||
),
|
||||
dev: DependencyGroups::from_args(
|
||||
groups: DependencyGroups::from_args(
|
||||
dev,
|
||||
no_dev,
|
||||
only_dev,
|
||||
|
|
|
@ -5259,16 +5259,16 @@ fn lock_requires_python_disjoint() -> Result<()> {
|
|||
"#,
|
||||
)?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.lock(), @r###"
|
||||
uv_snapshot!(context.filters(), context.lock(), @r"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
error: The workspace contains conflicting Python requirements:
|
||||
- `child`: `==3.10`
|
||||
- `project`: `>=3.12`
|
||||
"###);
|
||||
error: Found conflicting Python requirements:
|
||||
- child: ==3.10
|
||||
- project: >=3.12
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -18298,29 +18298,30 @@ fn lock_request_requires_python() -> Result<()> {
|
|||
)?;
|
||||
|
||||
// Request a version that conflicts with `--requires-python`.
|
||||
uv_snapshot!(context.filters(), context.lock().arg("--python").arg("3.12"), @r###"
|
||||
uv_snapshot!(context.filters(), context.lock().arg("--python").arg("3.12"), @r"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
|
||||
error: The requested interpreter resolved to Python 3.12.[X], which is incompatible with the project's Python requirement: `>=3.8, <=3.10`
|
||||
"###);
|
||||
error: The requested interpreter resolved to Python 3.12.[X], which is incompatible with the project's Python requirement: `>=3.8, <=3.10` (from `project.requires-python`)
|
||||
");
|
||||
|
||||
// Add a `.python-version` file that conflicts.
|
||||
let python_version = context.temp_dir.child(".python-version");
|
||||
python_version.write_str("3.12")?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.lock(), @r###"
|
||||
uv_snapshot!(context.filters(), context.lock(), @r"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
|
||||
error: The Python request from `.python-version` resolved to Python 3.12.[X], which is incompatible with the project's Python requirement: `>=3.8, <=3.10`. Use `uv python pin` to update the `.python-version` file to a compatible version.
|
||||
"###);
|
||||
error: The Python request from `.python-version` resolved to Python 3.12.[X], which is incompatible with the project's Python requirement: `>=3.8, <=3.10` (from `project.requires-python`)
|
||||
Use `uv python pin` to update the `.python-version` file to a compatible version
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -20660,6 +20661,465 @@ fn lock_group_include() -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lock_group_requires_python() -> 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 = ["typing-extensions"]
|
||||
|
||||
[dependency-groups]
|
||||
foo = ["idna"]
|
||||
bar = ["sortedcontainers", "sniffio"]
|
||||
|
||||
[tool.uv.dependency-groups]
|
||||
bar = { requires-python = ">=3.13" }
|
||||
"#,
|
||||
)?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.lock(), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 5 packages in [TIME]
|
||||
");
|
||||
|
||||
let lock = context.read("uv.lock");
|
||||
|
||||
insta::with_settings!({
|
||||
filters => context.filters(),
|
||||
}, {
|
||||
assert_snapshot!(
|
||||
lock, @r#"
|
||||
version = 1
|
||||
revision = 2
|
||||
requires-python = ">=3.12"
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.13'",
|
||||
"python_full_version < '3.13'",
|
||||
]
|
||||
|
||||
[options]
|
||||
exclude-newer = "2024-03-25T00:00:00Z"
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/bf/3f/ea4b9117521a1e9c50344b909be7886dd00a519552724809bb1f486986c2/idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", size = 175426, upload-time = "2023-11-25T15:40:54.902Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/e7/a82b05cf63a603df6e68d59ae6a68bf5064484a0718ea5033660af4b54a9/idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f", size = 61567, upload-time = "2023-11-25T15:40:52.604Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
|
||||
[package.dev-dependencies]
|
||||
bar = [
|
||||
{ name = "sniffio", marker = "python_full_version >= '3.13'" },
|
||||
{ name = "sortedcontainers", marker = "python_full_version >= '3.13'" },
|
||||
]
|
||||
foo = [
|
||||
{ name = "idna" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [{ name = "typing-extensions" }]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
bar = [
|
||||
{ name = "sniffio", marker = "python_full_version >= '3.13'" },
|
||||
{ name = "sortedcontainers", marker = "python_full_version >= '3.13'" },
|
||||
]
|
||||
foo = [{ name = "idna" }]
|
||||
|
||||
[[package]]
|
||||
name = "sniffio"
|
||||
version = "1.3.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sortedcontainers"
|
||||
version = "2.4.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.10.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/16/3a/0d26ce356c7465a19c9ea8814b960f8a36c3b0d07c323176620b7b483e44/typing_extensions-4.10.0.tar.gz", hash = "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb", size = 77558, upload-time = "2024-02-25T22:12:49.693Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/de/dc04a3ea60b22624b51c703a84bbe0184abcd1d0b9bc8074b5d6b7ab90bb/typing_extensions-4.10.0-py3-none-any.whl", hash = "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475", size = 33926, upload-time = "2024-02-25T22:12:47.72Z" },
|
||||
]
|
||||
"#
|
||||
);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lock_group_includes_requires_python() -> 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 = ["typing-extensions"]
|
||||
|
||||
[dependency-groups]
|
||||
foo = ["idna", {include-group = "bar"}]
|
||||
bar = ["sortedcontainers", "sniffio"]
|
||||
baz = ["idna", {include-group = "bar"}]
|
||||
blargh = ["idna", {include-group = "bar"}]
|
||||
|
||||
[tool.uv.dependency-groups]
|
||||
bar = { requires-python = ">=3.13" }
|
||||
baz = { requires-python = ">=3.13.1" }
|
||||
blargh = { requires-python = ">=3.12.1" }
|
||||
"#,
|
||||
)?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.lock(), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 5 packages in [TIME]
|
||||
");
|
||||
|
||||
let lock = context.read("uv.lock");
|
||||
|
||||
insta::with_settings!({
|
||||
filters => context.filters(),
|
||||
}, {
|
||||
assert_snapshot!(
|
||||
lock, @r#"
|
||||
version = 1
|
||||
revision = 2
|
||||
requires-python = ">=3.12"
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.13.1'",
|
||||
"python_full_version >= '3.13' and python_full_version < '3.13.1'",
|
||||
"python_full_version >= '3.12.[X]' and python_full_version < '3.13'",
|
||||
"python_full_version < '3.12.[X]'",
|
||||
]
|
||||
|
||||
[options]
|
||||
exclude-newer = "2024-03-25T00:00:00Z"
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/bf/3f/ea4b9117521a1e9c50344b909be7886dd00a519552724809bb1f486986c2/idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", size = 175426, upload-time = "2023-11-25T15:40:54.902Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/e7/a82b05cf63a603df6e68d59ae6a68bf5064484a0718ea5033660af4b54a9/idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f", size = 61567, upload-time = "2023-11-25T15:40:52.604Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
|
||||
[package.dev-dependencies]
|
||||
bar = [
|
||||
{ name = "sniffio", marker = "python_full_version >= '3.13'" },
|
||||
{ name = "sortedcontainers", marker = "python_full_version >= '3.13'" },
|
||||
]
|
||||
baz = [
|
||||
{ name = "idna", marker = "python_full_version >= '3.13.1'" },
|
||||
{ name = "sniffio", marker = "python_full_version >= '3.13.1'" },
|
||||
{ name = "sortedcontainers", marker = "python_full_version >= '3.13.1'" },
|
||||
]
|
||||
blargh = [
|
||||
{ name = "idna", marker = "python_full_version >= '3.12.[X]'" },
|
||||
{ name = "sniffio", marker = "python_full_version >= '3.13'" },
|
||||
{ name = "sortedcontainers", marker = "python_full_version >= '3.13'" },
|
||||
]
|
||||
foo = [
|
||||
{ name = "idna" },
|
||||
{ name = "sniffio", marker = "python_full_version >= '3.13'" },
|
||||
{ name = "sortedcontainers", marker = "python_full_version >= '3.13'" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [{ name = "typing-extensions" }]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
bar = [
|
||||
{ name = "sniffio", marker = "python_full_version >= '3.13'" },
|
||||
{ name = "sortedcontainers", marker = "python_full_version >= '3.13'" },
|
||||
]
|
||||
baz = [
|
||||
{ name = "idna", marker = "python_full_version >= '3.13.1'" },
|
||||
{ name = "sniffio", marker = "python_full_version >= '3.13.1'" },
|
||||
{ name = "sortedcontainers", marker = "python_full_version >= '3.13.1'" },
|
||||
]
|
||||
blargh = [
|
||||
{ name = "idna", marker = "python_full_version >= '3.12.[X]'" },
|
||||
{ name = "sniffio", marker = "python_full_version >= '3.13'" },
|
||||
{ name = "sortedcontainers", marker = "python_full_version >= '3.13'" },
|
||||
]
|
||||
foo = [
|
||||
{ name = "idna" },
|
||||
{ name = "sniffio", marker = "python_full_version >= '3.13'" },
|
||||
{ name = "sortedcontainers", marker = "python_full_version >= '3.13'" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sniffio"
|
||||
version = "1.3.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sortedcontainers"
|
||||
version = "2.4.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.10.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/16/3a/0d26ce356c7465a19c9ea8814b960f8a36c3b0d07c323176620b7b483e44/typing_extensions-4.10.0.tar.gz", hash = "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb", size = 77558, upload-time = "2024-02-25T22:12:49.693Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/de/dc04a3ea60b22624b51c703a84bbe0184abcd1d0b9bc8074b5d6b7ab90bb/typing_extensions-4.10.0-py3-none-any.whl", hash = "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475", size = 33926, upload-time = "2024-02-25T22:12:47.72Z" },
|
||||
]
|
||||
"#
|
||||
);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Referring to a dependency-group with group-requires-python that does not exist
|
||||
#[test]
|
||||
fn lock_group_requires_undefined_group() -> Result<()> {
|
||||
let context = TestContext::new("3.12");
|
||||
|
||||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||||
pyproject_toml.write_str(
|
||||
r#"
|
||||
[project]
|
||||
name = "myproject"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = ["typing-extensions"]
|
||||
|
||||
[dependency-groups]
|
||||
bar = ["sortedcontainers"]
|
||||
|
||||
[tool.uv.dependency-groups]
|
||||
foo = { requires-python = ">=3.13" }
|
||||
"#,
|
||||
)?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.lock(), @r"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
error: Project `myproject` has malformed dependency groups
|
||||
Caused by: Failed to find group `foo` specified in `[tool.uv.dependency-groups]`
|
||||
");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The legacy dev-dependencies cannot be referred to by group-requires-python
|
||||
#[test]
|
||||
fn lock_group_requires_dev_dep() -> Result<()> {
|
||||
let context = TestContext::new("3.12");
|
||||
|
||||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||||
pyproject_toml.write_str(
|
||||
r#"
|
||||
[project]
|
||||
name = "myproject"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = ["typing-extensions"]
|
||||
|
||||
[tool.uv]
|
||||
dev-dependencies = ["sortedcontainers"]
|
||||
|
||||
[tool.uv.dependency-groups]
|
||||
dev = { requires-python = ">=3.13" }
|
||||
"#,
|
||||
)?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.lock(), @r"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
error: Project `myproject` has malformed dependency groups
|
||||
Caused by: `[tool.uv.dependency-groups]` specifies the `dev` group, but only `tool.uv.dev-dependencies` was found. To reference the `dev` group, remove the `tool.uv.dev-dependencies` section and add any development dependencies to the `dev` entry in the `[dependency-groups]` table instead.
|
||||
");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lock_group_includes_requires_python_contradiction() -> 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 = ["typing-extensions"]
|
||||
|
||||
[dependency-groups]
|
||||
foo = ["idna", {include-group = "bar"}]
|
||||
bar = ["sortedcontainers", "sniffio"]
|
||||
|
||||
[tool.uv.dependency-groups]
|
||||
bar = { requires-python = ">=3.13" }
|
||||
foo = { requires-python = "<3.13" }
|
||||
"#,
|
||||
)?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.lock(), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 5 packages in [TIME]
|
||||
");
|
||||
|
||||
let lock = context.read("uv.lock");
|
||||
|
||||
insta::with_settings!({
|
||||
filters => context.filters(),
|
||||
}, {
|
||||
assert_snapshot!(
|
||||
lock, @r#"
|
||||
version = 1
|
||||
revision = 2
|
||||
requires-python = ">=3.12"
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.13'",
|
||||
"python_full_version < '3.13'",
|
||||
]
|
||||
|
||||
[options]
|
||||
exclude-newer = "2024-03-25T00:00:00Z"
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/bf/3f/ea4b9117521a1e9c50344b909be7886dd00a519552724809bb1f486986c2/idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", size = 175426, upload-time = "2023-11-25T15:40:54.902Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/e7/a82b05cf63a603df6e68d59ae6a68bf5064484a0718ea5033660af4b54a9/idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f", size = 61567, upload-time = "2023-11-25T15:40:52.604Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
|
||||
[package.dev-dependencies]
|
||||
bar = [
|
||||
{ name = "sniffio", marker = "python_full_version >= '3.13'" },
|
||||
{ name = "sortedcontainers", marker = "python_full_version >= '3.13'" },
|
||||
]
|
||||
foo = [
|
||||
{ name = "idna", marker = "python_full_version < '3.13'" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [{ name = "typing-extensions" }]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
bar = [
|
||||
{ name = "sniffio", marker = "python_full_version >= '3.13'" },
|
||||
{ name = "sortedcontainers", marker = "python_full_version >= '3.13'" },
|
||||
]
|
||||
foo = [
|
||||
{ name = "idna", marker = "python_full_version < '3.13'" },
|
||||
{ name = "sniffio", marker = "python_version < '0'" },
|
||||
{ name = "sortedcontainers", marker = "python_version < '0'" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sniffio"
|
||||
version = "1.3.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sortedcontainers"
|
||||
version = "2.4.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.10.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/16/3a/0d26ce356c7465a19c9ea8814b960f8a36c3b0d07c323176620b7b483e44/typing_extensions-4.10.0.tar.gz", hash = "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb", size = 77558, upload-time = "2024-02-25T22:12:49.693Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/de/dc04a3ea60b22624b51c703a84bbe0184abcd1d0b9bc8074b5d6b7ab90bb/typing_extensions-4.10.0-py3-none-any.whl", hash = "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475", size = 33926, upload-time = "2024-02-25T22:12:47.72Z" },
|
||||
]
|
||||
"#
|
||||
);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lock_group_include_cycle() -> Result<()> {
|
||||
let context = TestContext::new("3.12");
|
||||
|
@ -20680,15 +21140,15 @@ fn lock_group_include_cycle() -> Result<()> {
|
|||
"#,
|
||||
)?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.lock(), @r###"
|
||||
uv_snapshot!(context.filters(), context.lock(), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
× Failed to build `project @ file://[TEMP_DIR]/`
|
||||
╰─▶ Detected a cycle in `dependency-groups`: `bar` -> `foobar` -> `foo` -> `bar`
|
||||
"###);
|
||||
error: Project `project` has malformed dependency groups
|
||||
Caused by: Detected a cycle in `dependency-groups`: `bar` -> `foobar` -> `foo` -> `bar`
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -20714,15 +21174,15 @@ fn lock_group_include_dev() -> Result<()> {
|
|||
"#,
|
||||
)?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.lock(), @r###"
|
||||
uv_snapshot!(context.filters(), context.lock(), @r#"
|
||||
success: false
|
||||
exit_code: 1
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
× Failed to build `project @ file://[TEMP_DIR]/`
|
||||
╰─▶ Group `foo` includes the `dev` group (`include = "dev"`), but only `tool.uv.dev-dependencies` was found. To reference the `dev` group via an `include`, remove the `tool.uv.dev-dependencies` section and add any development dependencies to the `dev` entry in the `[dependency-groups]` table instead.
|
||||
"###);
|
||||
error: Project `project` has malformed dependency groups
|
||||
Caused by: Group `foo` includes the `dev` group (`include = "dev"`), but only `tool.uv.dev-dependencies` was found. To reference the `dev` group via an `include`, remove the `tool.uv.dev-dependencies` section and add any development dependencies to the `dev` entry in the `[dependency-groups]` table instead.
|
||||
"#);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -20745,15 +21205,15 @@ fn lock_group_include_missing() -> Result<()> {
|
|||
"#,
|
||||
)?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.lock(), @r###"
|
||||
uv_snapshot!(context.filters(), context.lock(), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
× Failed to build `project @ file://[TEMP_DIR]/`
|
||||
╰─▶ Failed to find group `bar` included by `foo`
|
||||
"###);
|
||||
error: Project `project` has malformed dependency groups
|
||||
Caused by: Failed to find group `bar` included by `foo`
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -20776,31 +21236,31 @@ fn lock_group_invalid_entry_package() -> Result<()> {
|
|||
"#,
|
||||
)?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.lock(), @r###"
|
||||
uv_snapshot!(context.filters(), context.lock(), @r#"
|
||||
success: false
|
||||
exit_code: 1
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
× Failed to build `project @ file://[TEMP_DIR]/`
|
||||
├─▶ Failed to parse entry in group `foo`: `invalid!`
|
||||
╰─▶ no such comparison operator "!", must be one of ~= == != <= >= < > ===
|
||||
invalid!
|
||||
^
|
||||
"###);
|
||||
error: Project `project` has malformed dependency groups
|
||||
Caused by: Failed to parse entry in group `foo`: `invalid!`
|
||||
Caused by: no such comparison operator "!", must be one of ~= == != <= >= < > ===
|
||||
invalid!
|
||||
^
|
||||
"#);
|
||||
|
||||
uv_snapshot!(context.filters(), context.sync().arg("--group").arg("foo"), @r###"
|
||||
uv_snapshot!(context.filters(), context.sync().arg("--group").arg("foo"), @r#"
|
||||
success: false
|
||||
exit_code: 1
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
× Failed to build `project @ file://[TEMP_DIR]/`
|
||||
├─▶ Failed to parse entry in group `foo`: `invalid!`
|
||||
╰─▶ no such comparison operator "!", must be one of ~= == != <= >= < > ===
|
||||
invalid!
|
||||
^
|
||||
"###);
|
||||
error: Project `project` has malformed dependency groups
|
||||
Caused by: Failed to parse entry in group `foo`: `invalid!`
|
||||
Caused by: no such comparison operator "!", must be one of ~= == != <= >= < > ===
|
||||
invalid!
|
||||
^
|
||||
"#);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -20897,12 +21357,12 @@ fn lock_group_invalid_entry_table() -> Result<()> {
|
|||
|
||||
uv_snapshot!(context.filters(), context.lock(), @r#"
|
||||
success: false
|
||||
exit_code: 1
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
× Failed to build `project @ file://[TEMP_DIR]/`
|
||||
╰─▶ Group `foo` contains an unknown dependency object specifier: {"bar": "unknown"}
|
||||
error: Project `project` has malformed dependency groups
|
||||
Caused by: Group `foo` contains an unknown dependency object specifier: {"bar": "unknown"}
|
||||
"#);
|
||||
|
||||
Ok(())
|
||||
|
|
|
@ -318,15 +318,15 @@ fn python_find_project() {
|
|||
"###);
|
||||
|
||||
// Unless explicitly requested
|
||||
uv_snapshot!(context.filters(), context.python_find().arg("3.10"), @r###"
|
||||
uv_snapshot!(context.filters(), context.python_find().arg("3.10"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
[PYTHON-3.10]
|
||||
|
||||
----- stderr -----
|
||||
warning: The requested interpreter resolved to Python 3.10.[X], which is incompatible with the project's Python requirement: `>=3.11`
|
||||
"###);
|
||||
warning: The requested interpreter resolved to Python 3.10.[X], which is incompatible with the project's Python requirement: `>=3.11` (from `project.requires-python`)
|
||||
");
|
||||
|
||||
// Or `--no-project` is used
|
||||
uv_snapshot!(context.filters(), context.python_find().arg("--no-project"), @r###"
|
||||
|
@ -367,15 +367,16 @@ fn python_find_project() {
|
|||
"###);
|
||||
|
||||
// We should warn on subsequent uses, but respect the pinned version?
|
||||
uv_snapshot!(context.filters(), context.python_find(), @r###"
|
||||
uv_snapshot!(context.filters(), context.python_find(), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
[PYTHON-3.10]
|
||||
|
||||
----- stderr -----
|
||||
warning: The Python request from `.python-version` resolved to Python 3.10.[X], which is incompatible with the project's Python requirement: `>=3.11`. Use `uv python pin` to update the `.python-version` file to a compatible version.
|
||||
"###);
|
||||
warning: The Python request from `.python-version` resolved to Python 3.10.[X], which is incompatible with the project's Python requirement: `>=3.11` (from `project.requires-python`)
|
||||
Use `uv python pin` to update the `.python-version` file to a compatible version
|
||||
");
|
||||
|
||||
// Unless the pin file is outside the project, in which case we should just ignore it
|
||||
let child_dir = context.temp_dir.child("child");
|
||||
|
|
|
@ -133,7 +133,7 @@ fn run_with_python_version() -> Result<()> {
|
|||
|
||||
----- stderr -----
|
||||
Using CPython 3.9.[X] interpreter at: [PYTHON-3.9]
|
||||
error: The requested interpreter resolved to Python 3.9.[X], which is incompatible with the project's Python requirement: `>=3.11, <4`
|
||||
error: The requested interpreter resolved to Python 3.9.[X], which is incompatible with the project's Python requirement: `>=3.11, <4` (from `project.requires-python`)
|
||||
");
|
||||
|
||||
Ok(())
|
||||
|
@ -3136,25 +3136,27 @@ fn run_isolated_incompatible_python() -> Result<()> {
|
|||
})?;
|
||||
|
||||
// We should reject Python 3.9...
|
||||
uv_snapshot!(context.filters(), context.run().arg("main.py"), @r###"
|
||||
uv_snapshot!(context.filters(), context.run().arg("main.py"), @r"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Using CPython 3.9.[X] interpreter at: [PYTHON-3.9]
|
||||
error: The Python request from `.python-version` resolved to Python 3.9.[X], which is incompatible with the project's Python requirement: `>=3.12`. Use `uv python pin` to update the `.python-version` file to a compatible version.
|
||||
"###);
|
||||
error: The Python request from `.python-version` resolved to Python 3.9.[X], which is incompatible with the project's Python requirement: `>=3.12` (from `project.requires-python`)
|
||||
Use `uv python pin` to update the `.python-version` file to a compatible version
|
||||
");
|
||||
|
||||
// ...even if `--isolated` is provided.
|
||||
uv_snapshot!(context.filters(), context.run().arg("--isolated").arg("main.py"), @r###"
|
||||
uv_snapshot!(context.filters(), context.run().arg("--isolated").arg("main.py"), @r"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
error: The Python request from `.python-version` resolved to Python 3.9.[X], which is incompatible with the project's Python requirement: `>=3.12`. Use `uv python pin` to update the `.python-version` file to a compatible version.
|
||||
"###);
|
||||
error: The Python request from `.python-version` resolved to Python 3.9.[X], which is incompatible with the project's Python requirement: `>=3.12` (from `project.requires-python`)
|
||||
Use `uv python pin` to update the `.python-version` file to a compatible version
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -4598,6 +4600,249 @@ fn run_default_groups() -> Result<()> {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_groups_requires_python() -> Result<()> {
|
||||
let context =
|
||||
TestContext::new_with_versions(&["3.11", "3.12", "3.13"]).with_filtered_python_sources();
|
||||
|
||||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||||
pyproject_toml.write_str(
|
||||
r#"
|
||||
[project]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = ["typing-extensions"]
|
||||
|
||||
[dependency-groups]
|
||||
foo = ["anyio"]
|
||||
bar = ["iniconfig"]
|
||||
dev = ["sniffio"]
|
||||
|
||||
[tool.uv.dependency-groups]
|
||||
foo = {requires-python=">=3.14"}
|
||||
bar = {requires-python=">=3.13"}
|
||||
dev = {requires-python=">=3.12"}
|
||||
"#,
|
||||
)?;
|
||||
|
||||
context.lock().assert().success();
|
||||
|
||||
// With --no-default-groups only the main requires-python should be consulted
|
||||
uv_snapshot!(context.filters(), context.run()
|
||||
.arg("--no-default-groups")
|
||||
.arg("python").arg("-c").arg("import typing_extensions"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Using CPython 3.11.[X] interpreter at: [PYTHON-3.11]
|
||||
Creating virtual environment at: .venv
|
||||
Resolved 6 packages in [TIME]
|
||||
Prepared 1 package in [TIME]
|
||||
Installed 1 package in [TIME]
|
||||
+ typing-extensions==4.10.0
|
||||
");
|
||||
|
||||
// The main requires-python and the default group's requires-python should be consulted
|
||||
// (This should trigger a version bump)
|
||||
uv_snapshot!(context.filters(), context.run()
|
||||
.arg("python").arg("-c").arg("import typing_extensions"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
|
||||
Removed virtual environment at: .venv
|
||||
Creating virtual environment at: .venv
|
||||
Resolved 6 packages in [TIME]
|
||||
Prepared 1 package in [TIME]
|
||||
Installed 2 packages in [TIME]
|
||||
+ sniffio==1.3.1
|
||||
+ typing-extensions==4.10.0
|
||||
");
|
||||
|
||||
// The main requires-python and "dev" and "bar" requires-python should be consulted
|
||||
// (This should trigger a version bump)
|
||||
uv_snapshot!(context.filters(), context.run()
|
||||
.arg("--group").arg("bar")
|
||||
.arg("python").arg("-c").arg("import typing_extensions"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Using CPython 3.13.[X] interpreter at: [PYTHON-3.13]
|
||||
Removed virtual environment at: .venv
|
||||
Creating virtual environment at: .venv
|
||||
Resolved 6 packages in [TIME]
|
||||
Prepared 1 package in [TIME]
|
||||
Installed 3 packages in [TIME]
|
||||
+ iniconfig==2.0.0
|
||||
+ sniffio==1.3.1
|
||||
+ typing-extensions==4.10.0
|
||||
");
|
||||
|
||||
// Going back to just "dev" we shouldn't churn the venv needlessly
|
||||
uv_snapshot!(context.filters(), context.run()
|
||||
.arg("python").arg("-c").arg("import typing_extensions"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 6 packages in [TIME]
|
||||
Audited 2 packages in [TIME]
|
||||
");
|
||||
|
||||
// Explicitly requesting an in-range python can downgrade
|
||||
uv_snapshot!(context.filters(), context.run()
|
||||
.arg("-p").arg("3.12")
|
||||
.arg("python").arg("-c").arg("import typing_extensions"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
|
||||
Removed virtual environment at: .venv
|
||||
Creating virtual environment at: .venv
|
||||
Resolved 6 packages in [TIME]
|
||||
Installed 2 packages in [TIME]
|
||||
+ sniffio==1.3.1
|
||||
+ typing-extensions==4.10.0
|
||||
");
|
||||
|
||||
// Explicitly requesting an out-of-range python fails
|
||||
uv_snapshot!(context.filters(), context.run()
|
||||
.arg("-p").arg("3.11")
|
||||
.arg("python").arg("-c").arg("import typing_extensions"), @r"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Using CPython 3.11.[X] interpreter at: [PYTHON-3.11]
|
||||
error: The requested interpreter resolved to Python 3.11.[X], which is incompatible with the project's Python requirement: `>=3.12` (from `tool.uv.dependency-groups.dev.requires-python`).
|
||||
");
|
||||
|
||||
// Enabling foo we can't find an interpreter
|
||||
uv_snapshot!(context.filters(), context.run()
|
||||
.arg("--group").arg("foo")
|
||||
.arg("python").arg("-c").arg("import typing_extensions"), @r"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
error: No interpreter found for Python >=3.14 in [PYTHON SOURCES]
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_groups_include_requires_python() -> Result<()> {
|
||||
let context = TestContext::new_with_versions(&["3.11", "3.12", "3.13"]);
|
||||
|
||||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||||
pyproject_toml.write_str(
|
||||
r#"
|
||||
[project]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = ["typing-extensions"]
|
||||
|
||||
[dependency-groups]
|
||||
foo = ["anyio"]
|
||||
bar = ["iniconfig"]
|
||||
baz = ["iniconfig"]
|
||||
dev = ["sniffio", {include-group = "foo"}, {include-group = "baz"}]
|
||||
|
||||
|
||||
[tool.uv.dependency-groups]
|
||||
foo = {requires-python="<3.13"}
|
||||
bar = {requires-python=">=3.13"}
|
||||
baz = {requires-python=">=3.12"}
|
||||
"#,
|
||||
)?;
|
||||
|
||||
context.lock().assert().success();
|
||||
|
||||
// With --no-default-groups only the main requires-python should be consulted
|
||||
uv_snapshot!(context.filters(), context.run()
|
||||
.arg("--no-default-groups")
|
||||
.arg("python").arg("-c").arg("import typing_extensions"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Using CPython 3.11.[X] interpreter at: [PYTHON-3.11]
|
||||
Creating virtual environment at: .venv
|
||||
Resolved 6 packages in [TIME]
|
||||
Prepared 1 package in [TIME]
|
||||
Installed 1 package in [TIME]
|
||||
+ typing-extensions==4.10.0
|
||||
");
|
||||
|
||||
// The main requires-python and the default group's requires-python should be consulted
|
||||
// (This should trigger a version bump)
|
||||
uv_snapshot!(context.filters(), context.run()
|
||||
.arg("python").arg("-c").arg("import typing_extensions"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
|
||||
Removed virtual environment at: .venv
|
||||
Creating virtual environment at: .venv
|
||||
Resolved 6 packages in [TIME]
|
||||
Prepared 4 packages in [TIME]
|
||||
Installed 5 packages in [TIME]
|
||||
+ anyio==4.3.0
|
||||
+ idna==3.6
|
||||
+ iniconfig==2.0.0
|
||||
+ sniffio==1.3.1
|
||||
+ typing-extensions==4.10.0
|
||||
");
|
||||
|
||||
// The main requires-python and "dev" and "bar" requires-python should be consulted
|
||||
// (This should trigger a conflict)
|
||||
uv_snapshot!(context.filters(), context.run()
|
||||
.arg("--group").arg("bar")
|
||||
.arg("python").arg("-c").arg("import typing_extensions"), @r"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
error: Found conflicting Python requirements:
|
||||
- project: >=3.11
|
||||
- project:bar: >=3.13
|
||||
- project:dev: >=3.12, <3.13
|
||||
");
|
||||
|
||||
// Explicitly requesting an out-of-range python fails
|
||||
uv_snapshot!(context.filters(), context.run()
|
||||
.arg("-p").arg("3.13")
|
||||
.arg("python").arg("-c").arg("import typing_extensions"), @r"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Using CPython 3.13.[X] interpreter at: [PYTHON-3.13]
|
||||
error: The requested interpreter resolved to Python 3.13.[X], which is incompatible with the project's Python requirement: `==3.12.*` (from `tool.uv.dependency-groups.dev.requires-python`).
|
||||
");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test that a signal n makes the process exit with code 128+n.
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
|
|
|
@ -3987,7 +3987,7 @@ fn resolve_config_file() -> anyhow::Result<()> {
|
|||
|
|
||||
1 | [project]
|
||||
| ^^^^^^^
|
||||
unknown field `project`, expected one of `required-version`, `native-tls`, `offline`, `no-cache`, `cache-dir`, `preview`, `python-preference`, `python-downloads`, `concurrent-downloads`, `concurrent-builds`, `concurrent-installs`, `index`, `index-url`, `extra-index-url`, `no-index`, `find-links`, `index-strategy`, `keyring-provider`, `allow-insecure-host`, `resolution`, `prerelease`, `fork-strategy`, `dependency-metadata`, `config-settings`, `no-build-isolation`, `no-build-isolation-package`, `exclude-newer`, `link-mode`, `compile-bytecode`, `no-sources`, `upgrade`, `upgrade-package`, `reinstall`, `reinstall-package`, `no-build`, `no-build-package`, `no-binary`, `no-binary-package`, `python-install-mirror`, `pypy-install-mirror`, `python-downloads-json-url`, `publish-url`, `trusted-publishing`, `check-url`, `add-bounds`, `pip`, `cache-keys`, `override-dependencies`, `constraint-dependencies`, `build-constraint-dependencies`, `environments`, `required-environments`, `conflicts`, `workspace`, `sources`, `managed`, `package`, `default-groups`, `dev-dependencies`, `build-backend`
|
||||
unknown field `project`, expected one of `required-version`, `native-tls`, `offline`, `no-cache`, `cache-dir`, `preview`, `python-preference`, `python-downloads`, `concurrent-downloads`, `concurrent-builds`, `concurrent-installs`, `index`, `index-url`, `extra-index-url`, `no-index`, `find-links`, `index-strategy`, `keyring-provider`, `allow-insecure-host`, `resolution`, `prerelease`, `fork-strategy`, `dependency-metadata`, `config-settings`, `no-build-isolation`, `no-build-isolation-package`, `exclude-newer`, `link-mode`, `compile-bytecode`, `no-sources`, `upgrade`, `upgrade-package`, `reinstall`, `reinstall-package`, `no-build`, `no-build-package`, `no-binary`, `no-binary-package`, `python-install-mirror`, `pypy-install-mirror`, `python-downloads-json-url`, `publish-url`, `trusted-publishing`, `check-url`, `add-bounds`, `pip`, `cache-keys`, `override-dependencies`, `constraint-dependencies`, `build-constraint-dependencies`, `environments`, `required-environments`, `conflicts`, `workspace`, `sources`, `managed`, `package`, `default-groups`, `dependency-groups`, `dev-dependencies`, `build-backend`
|
||||
"
|
||||
);
|
||||
|
||||
|
|
|
@ -346,7 +346,296 @@ fn mixed_requires_python() -> Result<()> {
|
|||
|
||||
----- stderr -----
|
||||
Using CPython 3.9.[X] interpreter at: [PYTHON-3.9]
|
||||
error: The requested interpreter resolved to Python 3.9.[X], which is incompatible with the project's Python requirement: `>=3.12`. However, a workspace member (`bird-feeder`) supports Python >=3.9. To install the workspace member on its own, navigate to `packages/bird-feeder`, then run `uv venv --python 3.9.[X]` followed by `uv pip install -e .`.
|
||||
error: The requested interpreter resolved to Python 3.9.[X], which is incompatible with the project's Python requirement: `>=3.12` (from workspace member `albatross`'s `project.requires-python`).
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Ensure that group requires-python solves an actual problem
|
||||
#[test]
|
||||
#[cfg(not(windows))]
|
||||
fn group_requires_python_useful_defaults() -> Result<()> {
|
||||
let context = TestContext::new_with_versions(&["3.8", "3.9"]);
|
||||
|
||||
// Require 3.8 for our project, but have a dev-dependency on a version of sphinx that needs 3.9
|
||||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||||
pyproject_toml.write_str(
|
||||
r#"
|
||||
[project]
|
||||
name = "pharaohs-tomp"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.8"
|
||||
dependencies = ["anyio"]
|
||||
|
||||
[dependency-groups]
|
||||
dev = ["sphinx>=7.2.6"]
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let src = context.temp_dir.child("src").child("albatross");
|
||||
src.create_dir_all()?;
|
||||
|
||||
let init = src.child("__init__.py");
|
||||
init.touch()?;
|
||||
|
||||
// Running `uv sync --no-dev` should ideally succeed, locking for Python 3.8.
|
||||
// ...but once we pick the 3.8 interpreter the lock freaks out because it sees
|
||||
// that the dependency-group containing sphinx will never successfully install,
|
||||
// even though it's not enabled!
|
||||
uv_snapshot!(context.filters(), context.sync()
|
||||
.arg("--no-dev"), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Using CPython 3.8.[X] interpreter at: [PYTHON-3.8]
|
||||
Creating virtual environment at: .venv
|
||||
× No solution found when resolving dependencies for split (python_full_version == '3.8.*'):
|
||||
╰─▶ Because the requested Python version (>=3.8) does not satisfy Python>=3.9 and sphinx==7.2.6 depends on Python>=3.9, we can conclude that sphinx==7.2.6 cannot be used.
|
||||
And because only sphinx<=7.2.6 is available, we can conclude that sphinx>=7.2.6 cannot be used.
|
||||
And because pharaohs-tomp:dev depends on sphinx>=7.2.6 and your project requires pharaohs-tomp:dev, we can conclude that your project's requirements are unsatisfiable.
|
||||
|
||||
hint: The `requires-python` value (>=3.8) includes Python versions that are not supported by your dependencies (e.g., sphinx==7.2.6 only supports >=3.9). Consider using a more restrictive `requires-python` value (like >=3.9).
|
||||
");
|
||||
|
||||
// Running `uv sync` should always fail, as now sphinx is involved
|
||||
uv_snapshot!(context.filters(), context.sync(), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
× No solution found when resolving dependencies for split (python_full_version == '3.8.*'):
|
||||
╰─▶ Because the requested Python version (>=3.8) does not satisfy Python>=3.9 and sphinx==7.2.6 depends on Python>=3.9, we can conclude that sphinx==7.2.6 cannot be used.
|
||||
And because only sphinx<=7.2.6 is available, we can conclude that sphinx>=7.2.6 cannot be used.
|
||||
And because pharaohs-tomp:dev depends on sphinx>=7.2.6 and your project requires pharaohs-tomp:dev, we can conclude that your project's requirements are unsatisfiable.
|
||||
|
||||
hint: The `requires-python` value (>=3.8) includes Python versions that are not supported by your dependencies (e.g., sphinx==7.2.6 only supports >=3.9). Consider using a more restrictive `requires-python` value (like >=3.9).
|
||||
");
|
||||
|
||||
// Adding group requires python should fix it
|
||||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||||
pyproject_toml.write_str(
|
||||
r#"
|
||||
[project]
|
||||
name = "pharaohs-tomp"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.8"
|
||||
dependencies = ["anyio"]
|
||||
|
||||
[dependency-groups]
|
||||
dev = ["sphinx>=7.2.6"]
|
||||
|
||||
[tool.uv.dependency-groups]
|
||||
dev = {requires-python = ">=3.9"}
|
||||
"#,
|
||||
)?;
|
||||
|
||||
// Running `uv sync --no-dev` should succeed, still using the Python 3.8.
|
||||
uv_snapshot!(context.filters(), context.sync()
|
||||
.arg("--no-dev"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 29 packages in [TIME]
|
||||
Prepared 5 packages in [TIME]
|
||||
Installed 5 packages in [TIME]
|
||||
+ anyio==4.3.0
|
||||
+ exceptiongroup==1.2.0
|
||||
+ idna==3.6
|
||||
+ sniffio==1.3.1
|
||||
+ typing-extensions==4.10.0
|
||||
");
|
||||
|
||||
// Running `uv sync` should succeed, bumping to Python 3.9 as sphinx is now involved.
|
||||
uv_snapshot!(context.filters(), context.sync(), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Using CPython 3.9.[X] interpreter at: [PYTHON-3.9]
|
||||
Removed virtual environment at: .venv
|
||||
Creating virtual environment at: .venv
|
||||
Resolved 29 packages in [TIME]
|
||||
Prepared 22 packages in [TIME]
|
||||
Installed 27 packages in [TIME]
|
||||
+ alabaster==0.7.16
|
||||
+ anyio==4.3.0
|
||||
+ babel==2.14.0
|
||||
+ certifi==2024.2.2
|
||||
+ charset-normalizer==3.3.2
|
||||
+ docutils==0.20.1
|
||||
+ exceptiongroup==1.2.0
|
||||
+ idna==3.6
|
||||
+ imagesize==1.4.1
|
||||
+ importlib-metadata==7.1.0
|
||||
+ jinja2==3.1.3
|
||||
+ markupsafe==2.1.5
|
||||
+ packaging==24.0
|
||||
+ pygments==2.17.2
|
||||
+ requests==2.31.0
|
||||
+ sniffio==1.3.1
|
||||
+ snowballstemmer==2.2.0
|
||||
+ sphinx==7.2.6
|
||||
+ sphinxcontrib-applehelp==1.0.8
|
||||
+ sphinxcontrib-devhelp==1.0.6
|
||||
+ sphinxcontrib-htmlhelp==2.0.5
|
||||
+ sphinxcontrib-jsmath==1.0.1
|
||||
+ sphinxcontrib-qthelp==1.0.7
|
||||
+ sphinxcontrib-serializinghtml==1.1.10
|
||||
+ typing-extensions==4.10.0
|
||||
+ urllib3==2.2.1
|
||||
+ zipp==3.18.1
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Ensure that group requires-python solves an actual problem
|
||||
#[test]
|
||||
#[cfg(not(windows))]
|
||||
fn group_requires_python_useful_non_defaults() -> Result<()> {
|
||||
let context = TestContext::new_with_versions(&["3.8", "3.9"]);
|
||||
|
||||
// Require 3.8 for our project, but have a dev-dependency on a version of sphinx that needs 3.9
|
||||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||||
pyproject_toml.write_str(
|
||||
r#"
|
||||
[project]
|
||||
name = "pharaohs-tomp"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.8"
|
||||
dependencies = ["anyio"]
|
||||
|
||||
[dependency-groups]
|
||||
mygroup = ["sphinx>=7.2.6"]
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let src = context.temp_dir.child("src").child("albatross");
|
||||
src.create_dir_all()?;
|
||||
|
||||
let init = src.child("__init__.py");
|
||||
init.touch()?;
|
||||
|
||||
// Running `uv sync` should ideally succeed, locking for Python 3.8.
|
||||
// ...but once we pick the 3.8 interpreter the lock freaks out because it sees
|
||||
// that the dependency-group containing sphinx will never successfully install,
|
||||
// even though it's not enabled, or even a default!
|
||||
uv_snapshot!(context.filters(), context.sync(), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Using CPython 3.8.[X] interpreter at: [PYTHON-3.8]
|
||||
Creating virtual environment at: .venv
|
||||
× No solution found when resolving dependencies for split (python_full_version == '3.8.*'):
|
||||
╰─▶ Because the requested Python version (>=3.8) does not satisfy Python>=3.9 and sphinx==7.2.6 depends on Python>=3.9, we can conclude that sphinx==7.2.6 cannot be used.
|
||||
And because only sphinx<=7.2.6 is available, we can conclude that sphinx>=7.2.6 cannot be used.
|
||||
And because pharaohs-tomp:mygroup depends on sphinx>=7.2.6 and your project requires pharaohs-tomp:mygroup, we can conclude that your project's requirements are unsatisfiable.
|
||||
|
||||
hint: The `requires-python` value (>=3.8) includes Python versions that are not supported by your dependencies (e.g., sphinx==7.2.6 only supports >=3.9). Consider using a more restrictive `requires-python` value (like >=3.9).
|
||||
");
|
||||
|
||||
// Running `uv sync --group mygroup` should definitely fail, as now sphinx is involved
|
||||
uv_snapshot!(context.filters(), context.sync()
|
||||
.arg("--group").arg("mygroup"), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
× No solution found when resolving dependencies for split (python_full_version == '3.8.*'):
|
||||
╰─▶ Because the requested Python version (>=3.8) does not satisfy Python>=3.9 and sphinx==7.2.6 depends on Python>=3.9, we can conclude that sphinx==7.2.6 cannot be used.
|
||||
And because only sphinx<=7.2.6 is available, we can conclude that sphinx>=7.2.6 cannot be used.
|
||||
And because pharaohs-tomp:mygroup depends on sphinx>=7.2.6 and your project requires pharaohs-tomp:mygroup, we can conclude that your project's requirements are unsatisfiable.
|
||||
|
||||
hint: The `requires-python` value (>=3.8) includes Python versions that are not supported by your dependencies (e.g., sphinx==7.2.6 only supports >=3.9). Consider using a more restrictive `requires-python` value (like >=3.9).
|
||||
");
|
||||
|
||||
// Adding group requires python should fix it
|
||||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||||
pyproject_toml.write_str(
|
||||
r#"
|
||||
[project]
|
||||
name = "pharaohs-tomp"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.8"
|
||||
dependencies = ["anyio"]
|
||||
|
||||
[dependency-groups]
|
||||
mygroup = ["sphinx>=7.2.6"]
|
||||
|
||||
[tool.uv.dependency-groups]
|
||||
mygroup = {requires-python = ">=3.9"}
|
||||
"#,
|
||||
)?;
|
||||
|
||||
// Running `uv sync` should succeed, locking for the previous picked Python 3.8.
|
||||
uv_snapshot!(context.filters(), context.sync(), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 29 packages in [TIME]
|
||||
Prepared 5 packages in [TIME]
|
||||
Installed 5 packages in [TIME]
|
||||
+ anyio==4.3.0
|
||||
+ exceptiongroup==1.2.0
|
||||
+ idna==3.6
|
||||
+ sniffio==1.3.1
|
||||
+ typing-extensions==4.10.0
|
||||
");
|
||||
|
||||
// Running `uv sync --group mygroup` should pass, bumping the interpreter to 3.9,
|
||||
// as the group requires-python saves us
|
||||
uv_snapshot!(context.filters(), context.sync()
|
||||
.arg("--group").arg("mygroup"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Using CPython 3.9.[X] interpreter at: [PYTHON-3.9]
|
||||
Removed virtual environment at: .venv
|
||||
Creating virtual environment at: .venv
|
||||
Resolved 29 packages in [TIME]
|
||||
Prepared 22 packages in [TIME]
|
||||
Installed 27 packages in [TIME]
|
||||
+ alabaster==0.7.16
|
||||
+ anyio==4.3.0
|
||||
+ babel==2.14.0
|
||||
+ certifi==2024.2.2
|
||||
+ charset-normalizer==3.3.2
|
||||
+ docutils==0.20.1
|
||||
+ exceptiongroup==1.2.0
|
||||
+ idna==3.6
|
||||
+ imagesize==1.4.1
|
||||
+ importlib-metadata==7.1.0
|
||||
+ jinja2==3.1.3
|
||||
+ markupsafe==2.1.5
|
||||
+ packaging==24.0
|
||||
+ pygments==2.17.2
|
||||
+ requests==2.31.0
|
||||
+ sniffio==1.3.1
|
||||
+ snowballstemmer==2.2.0
|
||||
+ sphinx==7.2.6
|
||||
+ sphinxcontrib-applehelp==1.0.8
|
||||
+ sphinxcontrib-devhelp==1.0.6
|
||||
+ sphinxcontrib-htmlhelp==2.0.5
|
||||
+ sphinxcontrib-jsmath==1.0.1
|
||||
+ sphinxcontrib-qthelp==1.0.7
|
||||
+ sphinxcontrib-serializinghtml==1.1.10
|
||||
+ typing-extensions==4.10.0
|
||||
+ urllib3==2.2.1
|
||||
+ zipp==3.18.1
|
||||
");
|
||||
|
||||
Ok(())
|
||||
|
@ -4080,17 +4369,17 @@ fn sync_custom_environment_path() -> Result<()> {
|
|||
|
||||
// But if it's just an incompatible virtual environment...
|
||||
fs_err::remove_dir_all(context.temp_dir.join("foo"))?;
|
||||
uv_snapshot!(context.filters(), context.venv().arg("foo").arg("--python").arg("3.11"), @r###"
|
||||
uv_snapshot!(context.filters(), context.venv().arg("foo").arg("--python").arg("3.11"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Using CPython 3.11.[X] interpreter at: [PYTHON-3.11]
|
||||
warning: The requested interpreter resolved to Python 3.11.[X], which is incompatible with the project's Python requirement: `>=3.12`
|
||||
warning: The requested interpreter resolved to Python 3.11.[X], which is incompatible with the project's Python requirement: `>=3.12` (from `project.requires-python`)
|
||||
Creating virtual environment at: foo
|
||||
Activate with: source foo/[BIN]/activate
|
||||
"###);
|
||||
");
|
||||
|
||||
// Even with some extraneous content...
|
||||
fs_err::write(context.temp_dir.join("foo").join("file"), b"")?;
|
||||
|
@ -5817,17 +6106,17 @@ fn sync_invalid_environment() -> Result<()> {
|
|||
|
||||
// But if it's just an incompatible virtual environment...
|
||||
fs_err::remove_dir_all(context.temp_dir.join(".venv"))?;
|
||||
uv_snapshot!(context.filters(), context.venv().arg("--python").arg("3.11"), @r###"
|
||||
uv_snapshot!(context.filters(), context.venv().arg("--python").arg("3.11"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Using CPython 3.11.[X] interpreter at: [PYTHON-3.11]
|
||||
warning: The requested interpreter resolved to Python 3.11.[X], which is incompatible with the project's Python requirement: `>=3.12`
|
||||
warning: The requested interpreter resolved to Python 3.11.[X], which is incompatible with the project's Python requirement: `>=3.12` (from `project.requires-python`)
|
||||
Creating virtual environment at: .venv
|
||||
Activate with: source .venv/[BIN]/activate
|
||||
"###);
|
||||
");
|
||||
|
||||
// Even with some extraneous content...
|
||||
fs_err::write(context.temp_dir.join(".venv").join("file"), b"")?;
|
||||
|
@ -5884,17 +6173,17 @@ fn sync_invalid_environment() -> Result<()> {
|
|||
|
||||
// But if it's not a virtual environment...
|
||||
fs_err::remove_dir_all(context.temp_dir.join(".venv"))?;
|
||||
uv_snapshot!(context.filters(), context.venv().arg("--python").arg("3.11"), @r###"
|
||||
uv_snapshot!(context.filters(), context.venv().arg("--python").arg("3.11"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Using CPython 3.11.[X] interpreter at: [PYTHON-3.11]
|
||||
warning: The requested interpreter resolved to Python 3.11.[X], which is incompatible with the project's Python requirement: `>=3.12`
|
||||
warning: The requested interpreter resolved to Python 3.11.[X], which is incompatible with the project's Python requirement: `>=3.12` (from `project.requires-python`)
|
||||
Creating virtual environment at: .venv
|
||||
Activate with: source .venv/[BIN]/activate
|
||||
"###);
|
||||
");
|
||||
|
||||
// Which we detect by the presence of a `pyvenv.cfg` file
|
||||
fs_err::remove_file(context.temp_dir.join(".venv").join("pyvenv.cfg"))?;
|
||||
|
@ -6004,15 +6293,15 @@ fn sync_python_version() -> Result<()> {
|
|||
"###);
|
||||
|
||||
// Unless explicitly requested...
|
||||
uv_snapshot!(context.filters(), context.sync().arg("--python").arg("3.10"), @r###"
|
||||
uv_snapshot!(context.filters(), context.sync().arg("--python").arg("3.10"), @r"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Using CPython 3.10.[X] interpreter at: [PYTHON-3.10]
|
||||
error: The requested interpreter resolved to Python 3.10.[X], which is incompatible with the project's Python requirement: `>=3.11`
|
||||
"###);
|
||||
error: The requested interpreter resolved to Python 3.10.[X], which is incompatible with the project's Python requirement: `>=3.11` (from `project.requires-python`)
|
||||
");
|
||||
|
||||
// But a pin should take precedence
|
||||
uv_snapshot!(context.filters(), context.python_pin().arg("3.12"), @r###"
|
||||
|
@ -6051,15 +6340,16 @@ fn sync_python_version() -> Result<()> {
|
|||
"###);
|
||||
|
||||
// We should warn on subsequent uses, but respect the pinned version?
|
||||
uv_snapshot!(context.filters(), context.sync(), @r###"
|
||||
uv_snapshot!(context.filters(), context.sync(), @r"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Using CPython 3.10.[X] interpreter at: [PYTHON-3.10]
|
||||
error: The Python request from `.python-version` resolved to Python 3.10.[X], which is incompatible with the project's Python requirement: `>=3.11`. Use `uv python pin` to update the `.python-version` file to a compatible version.
|
||||
"###);
|
||||
error: The Python request from `.python-version` resolved to Python 3.10.[X], which is incompatible with the project's Python requirement: `>=3.11` (from `project.requires-python`)
|
||||
Use `uv python pin` to update the `.python-version` file to a compatible version
|
||||
");
|
||||
|
||||
// Unless the pin file is outside the project, in which case we should just ignore it entirely
|
||||
let child_dir = context.temp_dir.child("child");
|
||||
|
@ -8935,52 +9225,52 @@ fn transitive_group_conflicts_cycle() -> Result<()> {
|
|||
|
||||
uv_snapshot!(context.filters(), context.sync(), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
× Failed to build `example @ file://[TEMP_DIR]/`
|
||||
╰─▶ Detected a cycle in `dependency-groups`: `dev` -> `test` -> `dev`
|
||||
error: Project `example` has malformed dependency groups
|
||||
Caused by: Detected a cycle in `dependency-groups`: `dev` -> `test` -> `dev`
|
||||
");
|
||||
|
||||
uv_snapshot!(context.filters(), context.sync().arg("--group").arg("dev"), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
× Failed to build `example @ file://[TEMP_DIR]/`
|
||||
╰─▶ Detected a cycle in `dependency-groups`: `dev` -> `test` -> `dev`
|
||||
error: Project `example` has malformed dependency groups
|
||||
Caused by: Detected a cycle in `dependency-groups`: `dev` -> `test` -> `dev`
|
||||
");
|
||||
|
||||
uv_snapshot!(context.filters(), context.sync().arg("--group").arg("dev").arg("--group").arg("test"), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
× Failed to build `example @ file://[TEMP_DIR]/`
|
||||
╰─▶ Detected a cycle in `dependency-groups`: `dev` -> `test` -> `dev`
|
||||
error: Project `example` has malformed dependency groups
|
||||
Caused by: Detected a cycle in `dependency-groups`: `dev` -> `test` -> `dev`
|
||||
");
|
||||
|
||||
uv_snapshot!(context.filters(), context.sync().arg("--group").arg("test").arg("--group").arg("magic"), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
× Failed to build `example @ file://[TEMP_DIR]/`
|
||||
╰─▶ Detected a cycle in `dependency-groups`: `dev` -> `test` -> `dev`
|
||||
error: Project `example` has malformed dependency groups
|
||||
Caused by: Detected a cycle in `dependency-groups`: `dev` -> `test` -> `dev`
|
||||
");
|
||||
|
||||
uv_snapshot!(context.filters(), context.sync().arg("--group").arg("dev").arg("--group").arg("magic"), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
× Failed to build `example @ file://[TEMP_DIR]/`
|
||||
╰─▶ Detected a cycle in `dependency-groups`: `dev` -> `test` -> `dev`
|
||||
error: Project `example` has malformed dependency groups
|
||||
Caused by: Detected a cycle in `dependency-groups`: `dev` -> `test` -> `dev`
|
||||
");
|
||||
|
||||
Ok(())
|
||||
|
|
|
@ -475,17 +475,195 @@ fn create_venv_respects_pyproject_requires_python() -> Result<()> {
|
|||
context.venv.assert(predicates::path::is_dir());
|
||||
|
||||
// We warn if we receive an incompatible version
|
||||
uv_snapshot!(context.filters(), context.venv().arg("--python").arg("3.11"), @r###"
|
||||
uv_snapshot!(context.filters(), context.venv().arg("--python").arg("3.11"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Using CPython 3.11.[X] interpreter at: [PYTHON-3.11]
|
||||
warning: The requested interpreter resolved to Python 3.11.[X], which is incompatible with the project's Python requirement: `>=3.12`
|
||||
warning: The requested interpreter resolved to Python 3.11.[X], which is incompatible with the project's Python requirement: `>=3.12` (from `project.requires-python`)
|
||||
Creating virtual environment at: .venv
|
||||
Activate with: source .venv/[BIN]/activate
|
||||
"###
|
||||
"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_venv_respects_group_requires_python() -> Result<()> {
|
||||
let context = TestContext::new_with_versions(&["3.9", "3.10", "3.11", "3.12"]);
|
||||
|
||||
// Without a Python requirement, we use the first on the PATH
|
||||
uv_snapshot!(context.filters(), context.venv(), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Using CPython 3.9.[X] interpreter at: [PYTHON-3.9]
|
||||
Creating virtual environment at: .venv
|
||||
Activate with: source .venv/[BIN]/activate
|
||||
"
|
||||
);
|
||||
|
||||
// With `requires-python = ">=3.10"` on the default group, we pick 3.10
|
||||
// However non-default groups should not be consulted!
|
||||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||||
pyproject_toml.write_str(indoc! { r#"
|
||||
[project]
|
||||
name = "foo"
|
||||
version = "1.0.0"
|
||||
dependencies = []
|
||||
|
||||
[dependency-groups]
|
||||
dev = ["sortedcontainers"]
|
||||
other = ["sniffio"]
|
||||
|
||||
[tool.uv.dependency-groups]
|
||||
dev = {requires-python = ">=3.10"}
|
||||
other = {requires-python = ">=3.12"}
|
||||
"#
|
||||
})?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.venv(), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Using CPython 3.10.[X] interpreter at: [PYTHON-3.10]
|
||||
Creating virtual environment at: .venv
|
||||
Activate with: source .venv/[BIN]/activate
|
||||
"
|
||||
);
|
||||
|
||||
// When the top-level requires-python and default group requires-python
|
||||
// both apply, their intersection is used. However non-default groups
|
||||
// should not be consulted! (here the top-level wins)
|
||||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||||
pyproject_toml.write_str(indoc! { r#"
|
||||
[project]
|
||||
name = "foo"
|
||||
version = "1.0.0"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = []
|
||||
|
||||
[dependency-groups]
|
||||
dev = ["sortedcontainers"]
|
||||
other = ["sniffio"]
|
||||
|
||||
[tool.uv.dependency-groups]
|
||||
dev = {requires-python = ">=3.10"}
|
||||
other = {requires-python = ">=3.12"}
|
||||
"#
|
||||
})?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.venv(), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Using CPython 3.11.[X] interpreter at: [PYTHON-3.11]
|
||||
Creating virtual environment at: .venv
|
||||
Activate with: source .venv/[BIN]/activate
|
||||
"
|
||||
);
|
||||
|
||||
// When the top-level requires-python and default group requires-python
|
||||
// both apply, their intersection is used. However non-default groups
|
||||
// should not be consulted! (here the group wins)
|
||||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||||
pyproject_toml.write_str(indoc! { r#"
|
||||
[project]
|
||||
name = "foo"
|
||||
version = "1.0.0"
|
||||
requires-python = ">=3.10"
|
||||
dependencies = []
|
||||
|
||||
[dependency-groups]
|
||||
dev = ["sortedcontainers"]
|
||||
other = ["sniffio"]
|
||||
|
||||
[tool.uv.dependency-groups]
|
||||
dev = {requires-python = ">=3.11"}
|
||||
other = {requires-python = ">=3.12"}
|
||||
"#
|
||||
})?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.venv(), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Using CPython 3.11.[X] interpreter at: [PYTHON-3.11]
|
||||
Creating virtual environment at: .venv
|
||||
Activate with: source .venv/[BIN]/activate
|
||||
"
|
||||
);
|
||||
|
||||
// We warn if we receive an incompatible version
|
||||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||||
pyproject_toml.write_str(indoc! { r#"
|
||||
[project]
|
||||
name = "foo"
|
||||
version = "1.0.0"
|
||||
dependencies = []
|
||||
|
||||
[dependency-groups]
|
||||
dev = ["sortedcontainers"]
|
||||
|
||||
[tool.uv.dependency-groups]
|
||||
dev = {requires-python = ">=3.12"}
|
||||
"#
|
||||
})?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.venv().arg("--python").arg("3.11"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Using CPython 3.11.[X] interpreter at: [PYTHON-3.11]
|
||||
warning: The requested interpreter resolved to Python 3.11.[X], which is incompatible with the project's Python requirement: `>=3.12` (from `tool.uv.dependency-groups.dev.requires-python`).
|
||||
Creating virtual environment at: .venv
|
||||
Activate with: source .venv/[BIN]/activate
|
||||
"
|
||||
);
|
||||
|
||||
// We error if there's no compatible version
|
||||
// non-default groups are not consulted here!
|
||||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||||
pyproject_toml.write_str(indoc! { r#"
|
||||
[project]
|
||||
name = "foo"
|
||||
version = "1.0.0"
|
||||
requires-python = "<3.12"
|
||||
dependencies = []
|
||||
|
||||
[dependency-groups]
|
||||
dev = ["sortedcontainers"]
|
||||
other = ["sniffio"]
|
||||
|
||||
[tool.uv.dependency-groups]
|
||||
dev = {requires-python = ">=3.12"}
|
||||
other = {requires-python = ">=3.11"}
|
||||
"#
|
||||
})?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.venv().arg("--python").arg("3.11"), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
× Found conflicting Python requirements:
|
||||
│ - foo: <3.12
|
||||
│ - foo:dev: >=3.12
|
||||
"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
|
|
|
@ -127,6 +127,31 @@ default-groups = ["docs"]
|
|||
|
||||
---
|
||||
|
||||
### [`dependency-groups`](#dependency-groups) {: #dependency-groups }
|
||||
|
||||
Additional settings for `dependency-groups`.
|
||||
|
||||
Currently this can only be used to add `requires-python` constraints
|
||||
to dependency groups (typically to inform uv that your dev tooling
|
||||
has a higher python requirement than your actual project).
|
||||
|
||||
This cannot be used to define dependency groups, use the top-level
|
||||
`[dependency-groups]` table for that.
|
||||
|
||||
**Default value**: `[]`
|
||||
|
||||
**Type**: `dict`
|
||||
|
||||
**Example usage**:
|
||||
|
||||
```toml title="pyproject.toml"
|
||||
|
||||
[tool.uv.dependency-groups]
|
||||
my-group = {requires-python = ">=3.12"}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### [`dev-dependencies`](#dev-dependencies) {: #dev-dependencies }
|
||||
|
||||
The project's development dependencies.
|
||||
|
|
29
uv.schema.json
generated
29
uv.schema.json
generated
|
@ -151,6 +151,17 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"dependency-groups": {
|
||||
"description": "Additional settings for `dependency-groups`.\n\nCurrently this can only be used to add `requires-python` constraints to dependency groups (typically to inform uv that your dev tooling has a higher python requirement than your actual project).\n\nThis cannot be used to define dependency groups, use the top-level `[dependency-groups]` table for that.",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ToolUvDependencyGroups"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"dependency-metadata": {
|
||||
"description": "Pre-defined static metadata for dependencies of the project (direct or transitive). When provided, enables the resolver to use the specified metadata instead of querying the registry or building the relevant package from source.\n\nMetadata should be provided in adherence with the [Metadata 2.3](https://packaging.python.org/en/latest/specifications/core-metadata/) standard, though only the following fields are respected:\n\n- `name`: The name of the package. - (Optional) `version`: The version of the package. If omitted, the metadata will be applied to all versions of the package. - (Optional) `requires-dist`: The dependencies of the package (e.g., `werkzeug>=0.14`). - (Optional) `requires-python`: The Python version required by the package (e.g., `>=3.10`). - (Optional) `provides-extras`: The extras provided by the package.",
|
||||
"type": [
|
||||
|
@ -824,6 +835,18 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"DependencyGroupSettings": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"requires-python": {
|
||||
"description": "Version of python to require when installing this group",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"ExcludeNewer": {
|
||||
"description": "Exclude distributions uploaded after the given timestamp.\n\nAccepts both RFC 3339 timestamps (e.g., `2006-12-02T02:07:43Z`) and local dates in the same format (e.g., `2006-12-02`).",
|
||||
"type": "string",
|
||||
|
@ -2344,6 +2367,12 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"ToolUvDependencyGroups": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/DependencyGroupSettings"
|
||||
}
|
||||
},
|
||||
"ToolUvSources": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue