mirror of
https://github.com/astral-sh/uv.git
synced 2025-08-04 19:08:04 +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
|
@ -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(())
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue