From 7408f53a335f30fde5b817c36ae398b94ebd203f Mon Sep 17 00:00:00 2001 From: Liam Date: Tue, 9 Dec 2025 17:15:48 -0500 Subject: [PATCH 1/9] Allow disabling `exclude-newer` per package --- crates/uv-resolver/src/exclude_newer.rs | 268 ++++++++++++++++++++---- crates/uv-resolver/src/lib.rs | 3 +- crates/uv-resolver/src/lock/mod.rs | 39 ++-- crates/uv-settings/src/combine.rs | 8 +- crates/uv/tests/it/lock.rs | 142 +++++++++++++ crates/uv/tests/it/pip_compile.rs | 2 +- docs/concepts/resolution.md | 8 +- uv.schema.json | 25 ++- 8 files changed, 431 insertions(+), 64 deletions(-) diff --git a/crates/uv-resolver/src/exclude_newer.rs b/crates/uv-resolver/src/exclude_newer.rs index ea28332e5..90818eda2 100644 --- a/crates/uv-resolver/src/exclude_newer.rs +++ b/crates/uv-resolver/src/exclude_newer.rs @@ -95,9 +95,9 @@ impl std::fmt::Display for ExcludeNewerChange { #[derive(Debug, Clone, PartialEq, Eq)] pub enum ExcludeNewerPackageChange { - PackageAdded(PackageName, ExcludeNewerValue), + PackageAdded(PackageName, PackageExcludeNewer), PackageRemoved(PackageName), - PackageChanged(PackageName, ExcludeNewerValueChange), + PackageChanged(PackageName, Box), } impl ExcludeNewerPackageChange { @@ -112,18 +112,20 @@ impl ExcludeNewerPackageChange { impl std::fmt::Display for ExcludeNewerPackageChange { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Self::PackageAdded(name, value) => { + Self::PackageAdded(name, PackageExcludeNewer::Timestamp(value)) => { write!( f, - "addition of exclude newer `{value}` for package `{name}`" + "addition of exclude newer `{}` for package `{name}`", + value.as_ref() ) } + Self::PackageAdded(name, PackageExcludeNewer::Disabled) => { + write!(f, "addition of exclude newer disable for package `{name}`") + } Self::PackageRemoved(name) => { write!(f, "removal of exclude newer for package `{name}`") } - Self::PackageChanged(name, change) => { - write!(f, "{change} for package `{name}`") - } + Self::PackageChanged(name, change) => write!(f, "{change} for package `{name}`"), } } } @@ -425,47 +427,175 @@ impl std::fmt::Display for ExcludeNewerValue { } } +/// Backwards-compatible alias for the exclude-newer timestamp type. +pub type ExcludeNewerTimestamp = ExcludeNewerValue; + +/// Per-package exclude-newer setting. +/// +/// This enum represents whether exclude-newer should be disabled for a package, +/// or if a specific timestamp cutoff should be used. +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +pub enum PackageExcludeNewer { + /// Disable exclude-newer for this package (allow all versions regardless of upload date). + Disabled, + /// Use this specific timestamp cutoff for this package. + Timestamp(Box), +} + /// A package-specific exclude-newer entry. #[derive(Debug, Clone, PartialEq, Eq)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub struct ExcludeNewerPackageEntry { pub package: PackageName, - pub timestamp: ExcludeNewerValue, + pub setting: PackageExcludeNewer, } impl FromStr for ExcludeNewerPackageEntry { type Err = String; - /// Parses a [`ExcludeNewerPackageEntry`] from a string in the format `PACKAGE=DATE`. + /// Parses a [`ExcludeNewerPackageEntry`] from a string in the format `PACKAGE=DATE` or `PACKAGE=false`. fn from_str(s: &str) -> Result { - let Some((package, date)) = s.split_once('=') else { + let Some((package, value)) = s.split_once('=') else { return Err(format!( - "Invalid `exclude-newer-package` value `{s}`: expected format `PACKAGE=DATE`" + "Invalid `exclude-newer-package` value `{s}`: expected format `PACKAGE=DATE` or `PACKAGE=false`" )); }; let package = PackageName::from_str(package).map_err(|err| { format!("Invalid `exclude-newer-package` package name `{package}`: {err}") })?; - let timestamp = ExcludeNewerValue::from_str(date) - .map_err(|err| format!("Invalid `exclude-newer-package` timestamp `{date}`: {err}"))?; - Ok(Self { package, timestamp }) + let setting = if value == "false" { + PackageExcludeNewer::Disabled + } else { + PackageExcludeNewer::Timestamp(Box::new(ExcludeNewerValue::from_str(value).map_err( + |err| format!("Invalid `exclude-newer-package` timestamp `{value}`: {err}"), + )?)) + }; + + Ok(Self { package, setting }) + } +} + +impl From<(PackageName, PackageExcludeNewer)> for ExcludeNewerPackageEntry { + fn from((package, setting): (PackageName, PackageExcludeNewer)) -> Self { + Self { package, setting } } } impl From<(PackageName, ExcludeNewerValue)> for ExcludeNewerPackageEntry { fn from((package, timestamp): (PackageName, ExcludeNewerValue)) -> Self { - Self { package, timestamp } + Self { + package, + setting: PackageExcludeNewer::Timestamp(Box::new(timestamp)), + } + } +} + +impl<'de> serde::Deserialize<'de> for PackageExcludeNewer { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct Visitor; + + impl serde::de::Visitor<'_> for Visitor { + type Value = PackageExcludeNewer; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a timestamp string or false/null to disable exclude-newer") + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + ExcludeNewerValue::from_str(v) + .map(|ts| PackageExcludeNewer::Timestamp(Box::new(ts))) + .map_err(|e| E::custom(format!("failed to parse timestamp: {e}"))) + } + + fn visit_bool(self, v: bool) -> Result + where + E: serde::de::Error, + { + if v { + Err(E::custom( + "expected false to disable exclude-newer, got true", + )) + } else { + Ok(PackageExcludeNewer::Disabled) + } + } + + fn visit_none(self) -> Result + where + E: serde::de::Error, + { + Ok(PackageExcludeNewer::Disabled) + } + + fn visit_unit(self) -> Result + where + E: serde::de::Error, + { + Ok(PackageExcludeNewer::Disabled) + } + } + + deserializer.deserialize_any(Visitor) + } +} + +impl serde::Serialize for PackageExcludeNewer { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + match self { + Self::Timestamp(timestamp) => timestamp.to_string().serialize(serializer), + Self::Disabled => serializer.serialize_bool(false), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum PackageExcludeNewerChange { + Disabled { was: ExcludeNewerValue }, + Enabled { now: ExcludeNewerValue }, + TimestampChanged(ExcludeNewerValueChange), +} + +impl PackageExcludeNewerChange { + pub fn is_relative_timestamp_change(&self) -> bool { + match self { + Self::Disabled { .. } | Self::Enabled { .. } => false, + Self::TimestampChanged(change) => change.is_relative_timestamp_change(), + } + } +} + +impl std::fmt::Display for PackageExcludeNewerChange { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Disabled { was } => { + write!(f, "disable exclude newer (was `{was}`)") + } + Self::Enabled { now } => { + write!(f, "enable exclude newer `{now}`") + } + Self::TimestampChanged(change) => write!(f, "{change}"), + } } } #[derive(Debug, Clone, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct ExcludeNewerPackage(FxHashMap); +pub struct ExcludeNewerPackage(FxHashMap); impl Deref for ExcludeNewerPackage { - type Target = FxHashMap; + type Target = FxHashMap; fn deref(&self) -> &Self::Target { &self.0 @@ -482,15 +612,15 @@ impl FromIterator for ExcludeNewerPackage { fn from_iter>(iter: T) -> Self { Self( iter.into_iter() - .map(|entry| (entry.package, entry.timestamp)) + .map(|entry| (entry.package, entry.setting)) .collect(), ) } } impl IntoIterator for ExcludeNewerPackage { - type Item = (PackageName, ExcludeNewerValue); - type IntoIter = std::collections::hash_map::IntoIter; + type Item = (PackageName, PackageExcludeNewer); + type IntoIter = std::collections::hash_map::IntoIter; fn into_iter(self) -> Self::IntoIter { self.0.into_iter() @@ -498,8 +628,8 @@ impl IntoIterator for ExcludeNewerPackage { } impl<'a> IntoIterator for &'a ExcludeNewerPackage { - type Item = (&'a PackageName, &'a ExcludeNewerValue); - type IntoIter = std::collections::hash_map::Iter<'a, PackageName, ExcludeNewerValue>; + type Item = (&'a PackageName, &'a PackageExcludeNewer); + type IntoIter = std::collections::hash_map::Iter<'a, PackageName, PackageExcludeNewer>; fn into_iter(self) -> Self::IntoIter { self.0.iter() @@ -508,20 +638,55 @@ impl<'a> IntoIterator for &'a ExcludeNewerPackage { impl ExcludeNewerPackage { /// Convert to the inner `HashMap`. - pub fn into_inner(self) -> FxHashMap { + pub fn into_inner(self) -> FxHashMap { self.0 } + /// Returns true if this map is empty (no package-specific settings). + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + pub fn compare(&self, other: &Self) -> Option { - for (package, timestamp) in self { - let Some(other_timestamp) = other.get(package) else { - return Some(ExcludeNewerPackageChange::PackageRemoved(package.clone())); - }; - if let Some(change) = timestamp.compare(other_timestamp) { - return Some(ExcludeNewerPackageChange::PackageChanged( - package.clone(), - change, - )); + for (package, setting) in self { + match (setting, other.get(package)) { + ( + PackageExcludeNewer::Timestamp(self_timestamp), + Some(PackageExcludeNewer::Timestamp(other_timestamp)), + ) => { + if let Some(change) = self_timestamp.compare(other_timestamp) { + return Some(ExcludeNewerPackageChange::PackageChanged( + package.clone(), + Box::new(PackageExcludeNewerChange::TimestampChanged(change)), + )); + } + } + ( + PackageExcludeNewer::Timestamp(self_timestamp), + Some(PackageExcludeNewer::Disabled), + ) => { + return Some(ExcludeNewerPackageChange::PackageChanged( + package.clone(), + Box::new(PackageExcludeNewerChange::Disabled { + was: self_timestamp.as_ref().clone(), + }), + )); + } + ( + PackageExcludeNewer::Disabled, + Some(PackageExcludeNewer::Timestamp(other_timestamp)), + ) => { + return Some(ExcludeNewerPackageChange::PackageChanged( + package.clone(), + Box::new(PackageExcludeNewerChange::Enabled { + now: other_timestamp.as_ref().clone(), + }), + )); + } + (PackageExcludeNewer::Disabled, Some(PackageExcludeNewer::Disabled)) => {} + (_, None) => { + return Some(ExcludeNewerPackageChange::PackageRemoved(package.clone())); + } } } @@ -574,12 +739,16 @@ impl ExcludeNewer { Self { global, package } } - /// Returns the timestamp for a specific package, falling back to the global timestamp if set. + /// Returns the exclude-newer timestamp for a specific package, returning `Some(timestamp)` + /// if the package has a package-specific timestamp or falls back to the global timestamp if + /// set, or `None` if exclude-newer is explicitly disabled for the package (set to `false`) or + /// if no exclude-newer is configured. pub fn exclude_newer_package(&self, package_name: &PackageName) -> Option { - self.package - .get(package_name) - .cloned() - .or(self.global.clone()) + match self.package.get(package_name) { + Some(PackageExcludeNewer::Timestamp(timestamp)) => Some(timestamp.as_ref().clone()), + Some(PackageExcludeNewer::Disabled) => None, + None => self.global.clone(), + } } /// Returns true if this has any configuration (global or per-package). @@ -615,11 +784,18 @@ impl std::fmt::Display for ExcludeNewer { } } let mut first = true; - for (name, timestamp) in &self.package { + for (name, setting) in &self.package { if !first { write!(f, ", ")?; } - write!(f, "{name}: {timestamp}")?; + match setting { + PackageExcludeNewer::Timestamp(timestamp) => { + write!(f, "{name}: {}", timestamp.as_ref())?; + } + PackageExcludeNewer::Disabled => { + write!(f, "{name}: disabled")?; + } + } first = false; } Ok(()) @@ -690,11 +866,21 @@ mod tests { fn test_exclude_newer_package_entry() { let entry = ExcludeNewerPackageEntry::from_str("numpy=2023-01-01T00:00:00Z").unwrap(); assert_eq!(entry.package.as_ref(), "numpy"); - assert!(entry.timestamp.to_string().contains("2023-01-01")); + match entry.setting { + PackageExcludeNewer::Timestamp(ref timestamp) => { + assert!(timestamp.to_string().contains("2023-01-01")); + } + PackageExcludeNewer::Disabled => panic!("expected timestamp"), + } // Test with relative timestamp let entry = ExcludeNewerPackageEntry::from_str("requests=7 days").unwrap(); assert_eq!(entry.package.as_ref(), "requests"); - // Just verify it parsed without error - the timestamp will be relative to now + assert!(matches!(entry.setting, PackageExcludeNewer::Timestamp(_))); + + // Test disabling exclude-newer per package + let entry = ExcludeNewerPackageEntry::from_str("torch=false").unwrap(); + assert_eq!(entry.package.as_ref(), "torch"); + assert!(matches!(entry.setting, PackageExcludeNewer::Disabled)); } } diff --git a/crates/uv-resolver/src/lib.rs b/crates/uv-resolver/src/lib.rs index d57f8f097..5d1ff4916 100644 --- a/crates/uv-resolver/src/lib.rs +++ b/crates/uv-resolver/src/lib.rs @@ -2,7 +2,8 @@ pub use dependency_mode::DependencyMode; pub use error::{ErrorTree, NoSolutionError, NoSolutionHeader, ResolveError, SentinelRange}; pub use exclude_newer::{ ExcludeNewer, ExcludeNewerChange, ExcludeNewerPackage, ExcludeNewerPackageChange, - ExcludeNewerPackageEntry, ExcludeNewerValue, ExcludeNewerValueChange, + ExcludeNewerPackageEntry, ExcludeNewerTimestamp, ExcludeNewerValue, ExcludeNewerValueChange, + PackageExcludeNewer, PackageExcludeNewerChange, }; pub use exclusions::Exclusions; pub use flat_index::{FlatDistributions, FlatIndex}; diff --git a/crates/uv-resolver/src/lock/mod.rs b/crates/uv-resolver/src/lock/mod.rs index 3ef583155..d2cfa03d5 100644 --- a/crates/uv-resolver/src/lock/mod.rs +++ b/crates/uv-resolver/src/lock/mod.rs @@ -61,7 +61,7 @@ use crate::resolution::{AnnotatedDist, ResolutionGraphNode}; use crate::universal_marker::{ConflictMarker, UniversalMarker}; use crate::{ ExcludeNewer, ExcludeNewerPackage, ExcludeNewerValue, InMemoryIndex, MetadataResponse, - PrereleaseMode, ResolutionMode, ResolverOutput, + PackageExcludeNewer, PrereleaseMode, ResolutionMode, ResolverOutput, }; mod export; @@ -1071,20 +1071,29 @@ impl Lock { // Serialize package-specific exclusions as a separate field if !exclude_newer.package.is_empty() { let mut package_table = toml_edit::Table::new(); - for (name, exclude_newer_value) in &exclude_newer.package { - if let Some(span) = exclude_newer_value.span() { - // Serialize as inline table with timestamp and span - let mut inline = toml_edit::InlineTable::new(); - inline.insert( - "timestamp", - exclude_newer_value.timestamp().to_string().into(), - ); - inline.insert("span", span.to_string().into()); - package_table.insert(name.as_ref(), Item::Value(inline.into())); - } else { - // Serialize as simple string - package_table - .insert(name.as_ref(), value(exclude_newer_value.to_string())); + for (name, setting) in &exclude_newer.package { + match setting { + PackageExcludeNewer::Timestamp(exclude_newer_value) => { + if let Some(span) = exclude_newer_value.span() { + // Serialize as inline table with timestamp and span + let mut inline = toml_edit::InlineTable::new(); + inline.insert( + "timestamp", + exclude_newer_value.timestamp().to_string().into(), + ); + inline.insert("span", span.to_string().into()); + package_table.insert(name.as_ref(), Item::Value(inline.into())); + } else { + // Serialize as simple string + package_table.insert( + name.as_ref(), + value(exclude_newer_value.to_string()), + ); + } + } + PackageExcludeNewer::Disabled => { + package_table.insert(name.as_ref(), value(false)); + } } } options_table.insert("exclude-newer-package", Item::Table(package_table)); diff --git a/crates/uv-settings/src/combine.rs b/crates/uv-settings/src/combine.rs index 15a63e1dc..8e3f46152 100644 --- a/crates/uv-settings/src/combine.rs +++ b/crates/uv-settings/src/combine.rs @@ -228,9 +228,11 @@ impl Combine for ExcludeNewer { if self.package.is_empty() { self.package = other.package; } else { - // Merge package-specific timestamps, with self taking precedence - for (pkg, timestamp) in &other.package { - self.package.entry(pkg.clone()).or_insert(timestamp.clone()); + // Merge package-specific settings, with self taking precedence + for (pkg, setting) in &other.package { + self.package + .entry(pkg.clone()) + .or_insert_with(|| setting.clone()); } } } diff --git a/crates/uv/tests/it/lock.rs b/crates/uv/tests/it/lock.rs index 6f7256932..e2e8780ba 100644 --- a/crates/uv/tests/it/lock.rs +++ b/crates/uv/tests/it/lock.rs @@ -31101,6 +31101,148 @@ fn test_tilde_equals_python_version() -> Result<()> { Ok(()) } +/// Test that exclude-newer-package can be disabled for specific packages using `false`. +#[test] +fn lock_exclude_newer_package_disable() -> 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 = ["requests", "tqdm"] + "#, + )?; + + // Lock with global exclude-newer and disable it for tqdm + uv_snapshot!(context.filters(), context + .lock() + .env_remove(EnvVars::UV_EXCLUDE_NEWER) + .arg("--exclude-newer") + .arg("2022-04-04T12:00:00Z") + .arg("--exclude-newer-package") + .arg("tqdm=false"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 8 packages in [TIME] + "###); + + let lock = context.read("uv.lock"); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + lock, @r#" + version = 1 + revision = 3 + requires-python = ">=3.12" + + [options] + exclude-newer = "2022-04-04T12:00:00Z" + + [options.exclude-newer-package] + tqdm = false + + [[package]] + name = "certifi" + version = "2021.10.8" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/6c/ae/d26450834f0acc9e3d1f74508da6df1551ceab6c2ce0766a593362d6d57f/certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872", size = 151214, upload-time = "2021-10-08T19:32:15.277Z" } + wheels = [ + { url = "https://files.pythonhosted.org/packages/37/45/946c02767aabb873146011e665728b680884cd8fe70dde973c640e45b775/certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569", size = 149195, upload-time = "2021-10-08T19:32:10.712Z" }, + ] + + [[package]] + name = "charset-normalizer" + version = "2.0.12" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/56/31/7bcaf657fafb3c6db8c787a865434290b726653c912085fbd371e9b92e1c/charset-normalizer-2.0.12.tar.gz", hash = "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597", size = 79105, upload-time = "2022-02-12T14:33:13.788Z" } + wheels = [ + { url = "https://files.pythonhosted.org/packages/06/b3/24afc8868eba069a7f03650ac750a778862dc34941a4bebeb58706715726/charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df", size = 39623, upload-time = "2022-02-12T14:33:12.294Z" }, + ] + + [[package]] + name = "colorama" + version = "0.4.4" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/1f/bb/5d3246097ab77fa083a61bd8d3d527b7ae063c7d8e8671b1cf8c4ec10cbe/colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b", size = 27813, upload-time = "2020-10-15T18:36:33.372Z" } + wheels = [ + { url = "https://files.pythonhosted.org/packages/44/98/5b86278fbbf250d239ae0ecb724f8572af1c91f4a11edf4d36a206189440/colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2", size = 16028, upload-time = "2020-10-13T02:42:26.463Z" }, + ] + + [[package]] + name = "idna" + version = "3.3" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/62/08/e3fc7c8161090f742f504f40b1bccbfc544d4a4e09eb774bf40aafce5436/idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d", size = 286689, upload-time = "2021-10-12T23:33:41.312Z" } + wheels = [ + { url = "https://files.pythonhosted.org/packages/04/a2/d918dcd22354d8958fe113e1a3630137e0fc8b44859ade3063982eacd2a4/idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff", size = 61160, upload-time = "2021-10-12T23:33:38.02Z" }, + ] + + [[package]] + name = "project" + version = "0.1.0" + source = { virtual = "." } + dependencies = [ + { name = "requests" }, + { name = "tqdm" }, + ] + + [package.metadata] + requires-dist = [ + { name = "requests" }, + { name = "tqdm" }, + ] + + [[package]] + name = "requests" + version = "2.27.1" + source = { registry = "https://pypi.org/simple" } + dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, + ] + sdist = { url = "https://files.pythonhosted.org/packages/60/f3/26ff3767f099b73e0efa138a9998da67890793bfa475d8278f84a30fec77/requests-2.27.1.tar.gz", hash = "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61", size = 106758, upload-time = "2022-01-05T15:40:51.698Z" } + wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/61/08076519c80041bc0ffa1a8af0cbd3bf3e2b62af10435d269a9d0f40564d/requests-2.27.1-py2.py3-none-any.whl", hash = "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d", size = 63133, upload-time = "2022-01-05T15:40:49.334Z" }, + ] + + [[package]] + name = "tqdm" + version = "4.67.1" + source = { registry = "https://pypi.org/simple" } + dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + ] + sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" } + wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" }, + ] + + [[package]] + name = "urllib3" + version = "1.26.9" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/1b/a5/4eab74853625505725cefdf168f48661b2cd04e7843ab836f3f63abf81da/urllib3-1.26.9.tar.gz", hash = "sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e", size = 295258, upload-time = "2022-03-16T13:28:19.197Z" } + wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/03/062e6444ce4baf1eac17a6a0ebfe36bb1ad05e1df0e20b110de59c278498/urllib3-1.26.9-py2.py3-none-any.whl", hash = "sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14", size = 138990, upload-time = "2022-03-16T13:28:16.026Z" }, + ] + "# + ); + }); + + Ok(()) +} + /// Test that exclude-newer-package is properly serialized in the lockfile. #[test] fn lock_exclude_newer_package() -> Result<()> { diff --git a/crates/uv/tests/it/pip_compile.rs b/crates/uv/tests/it/pip_compile.rs index 9d01ae9b3..764afb794 100644 --- a/crates/uv/tests/it/pip_compile.rs +++ b/crates/uv/tests/it/pip_compile.rs @@ -3565,7 +3565,7 @@ fn compile_exclude_newer_package_errors() -> Result<()> { ----- stdout ----- ----- stderr ----- - error: invalid value 'tqdm' for '--exclude-newer-package ': Invalid `exclude-newer-package` value `tqdm`: expected format `PACKAGE=DATE` + error: invalid value 'tqdm' for '--exclude-newer-package ': Invalid `exclude-newer-package` value `tqdm`: expected format `PACKAGE=DATE` or `PACKAGE=false` For more information, try '--help'. " diff --git a/docs/concepts/resolution.md b/docs/concepts/resolution.md index 65db075c5..c34f44d8e 100644 --- a/docs/concepts/resolution.md +++ b/docs/concepts/resolution.md @@ -654,12 +654,16 @@ may be specified as an [RFC 3339](https://www.rfc-editor.org/rfc/rfc3339.html) t `2006-12-02T02:07:43Z`) or a local date in the same format (e.g., `2006-12-02`) in your system's configured time zone. +Use `--exclude-newer-package =` to apply the cutoff to specific packages rather than +globally. The same flag also accepts `=false` to opt a package out of the `--exclude-newer` +restriction, e.g., to allow resolving packages from an index that does not publish upload times. + !!! important The package index must support the `upload-time` field as specified in [`PEP 700`](https://peps.python.org/pep-0700/). If the field is not present for a given - distribution, the distribution will be treated as unavailable. PyPI provides `upload-time` for - all packages. + distribution, the distribution will be treated as unavailable unless the package is opted out + via `--exclude-newer-package =false`. PyPI provides `upload-time` for all packages. To ensure reproducibility, messages for unsatisfiable resolutions will not mention that distributions were excluded due to the `--exclude-newer` flag — newer distributions will be treated diff --git a/uv.schema.json b/uv.schema.json index bcabfdfd1..6aaac2a56 100644 --- a/uv.schema.json +++ b/uv.schema.json @@ -888,7 +888,7 @@ "ExcludeNewerPackage": { "type": "object", "additionalProperties": { - "$ref": "#/definitions/ExcludeNewerTimestamp" + "$ref": "#/definitions/PackageExcludeNewer" } }, "ExcludeNewerTimestamp": { @@ -1203,6 +1203,29 @@ "$ref": "#/definitions/ConfigSettings" } }, + "PackageExcludeNewer": { + "description": "Per-package exclude-newer setting.\n\nThis enum represents whether exclude-newer should be disabled for a package,\nor if a specific timestamp cutoff should be used.", + "oneOf": [ + { + "description": "Disable exclude-newer for this package (allow all versions regardless of upload date).", + "type": "string", + "const": "Disabled" + }, + { + "description": "Use this specific timestamp cutoff for this package.", + "type": "object", + "properties": { + "Timestamp": { + "$ref": "#/definitions/ExcludeNewerTimestamp" + } + }, + "additionalProperties": false, + "required": [ + "Timestamp" + ] + } + ] + }, "PackageName": { "description": "The normalized name of a package.\n\nConverts the name to lowercase and collapses runs of `-`, `_`, and `.` down to a single `-`.\nFor example, `---`, `.`, and `__` are all converted to a single `-`.\n\nSee: ", "type": "string" From ea827aa054f8142d8bcc927471c9f14af5fe9a84 Mon Sep 17 00:00:00 2001 From: Liam Date: Tue, 9 Dec 2025 17:22:37 -0500 Subject: [PATCH 2/9] Rename and remove backwards compatibility scheme --- crates/uv-resolver/src/exclude_newer.rs | 44 ++++++++++++------------- crates/uv-resolver/src/lib.rs | 4 +-- crates/uv-resolver/src/lock/mod.rs | 2 +- 3 files changed, 25 insertions(+), 25 deletions(-) diff --git a/crates/uv-resolver/src/exclude_newer.rs b/crates/uv-resolver/src/exclude_newer.rs index 90818eda2..9c256fcce 100644 --- a/crates/uv-resolver/src/exclude_newer.rs +++ b/crates/uv-resolver/src/exclude_newer.rs @@ -112,7 +112,7 @@ impl ExcludeNewerPackageChange { impl std::fmt::Display for ExcludeNewerPackageChange { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Self::PackageAdded(name, PackageExcludeNewer::Timestamp(value)) => { + Self::PackageAdded(name, PackageExcludeNewer::Cutoff(value)) => { write!( f, "addition of exclude newer `{}` for package `{name}`", @@ -120,7 +120,10 @@ impl std::fmt::Display for ExcludeNewerPackageChange { ) } Self::PackageAdded(name, PackageExcludeNewer::Disabled) => { - write!(f, "addition of exclude newer disable for package `{name}`") + write!( + f, + "addition of exclude newer exception for package `{name}`" + ) } Self::PackageRemoved(name) => { write!(f, "removal of exclude newer for package `{name}`") @@ -257,7 +260,7 @@ impl ExcludeNewerValue { self.span.as_ref() } - /// Create a new [`ExcludeNewerTimestamp`]. + /// Create a new [`ExcludeNewerValue`]. pub fn new(timestamp: Timestamp, span: Option) -> Self { Self { timestamp, span } } @@ -328,7 +331,7 @@ fn format_exclude_newer_error( impl FromStr for ExcludeNewerValue { type Err = String; - /// Parse an [`ExcludeNewerTimestamp`] from a string. + /// Parse an [`ExcludeNewerValue`] from a string. /// /// Accepts RFC 3339 timestamps (e.g., `2006-12-02T02:07:43Z`), local dates in the same format /// (e.g., `2006-12-02`), "friendly" durations (e.g., `1 week`, `30 days`), and ISO 8601 @@ -427,20 +430,17 @@ impl std::fmt::Display for ExcludeNewerValue { } } -/// Backwards-compatible alias for the exclude-newer timestamp type. -pub type ExcludeNewerTimestamp = ExcludeNewerValue; - /// Per-package exclude-newer setting. /// /// This enum represents whether exclude-newer should be disabled for a package, -/// or if a specific timestamp cutoff should be used. +/// or if a specific cutoff (absolute or relative) should be used. #[derive(Debug, Clone, PartialEq, Eq)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub enum PackageExcludeNewer { /// Disable exclude-newer for this package (allow all versions regardless of upload date). Disabled, - /// Use this specific timestamp cutoff for this package. - Timestamp(Box), + /// Use this specific cutoff for this package. + Cutoff(Box), } /// A package-specific exclude-newer entry. @@ -469,7 +469,7 @@ impl FromStr for ExcludeNewerPackageEntry { let setting = if value == "false" { PackageExcludeNewer::Disabled } else { - PackageExcludeNewer::Timestamp(Box::new(ExcludeNewerValue::from_str(value).map_err( + PackageExcludeNewer::Cutoff(Box::new(ExcludeNewerValue::from_str(value).map_err( |err| format!("Invalid `exclude-newer-package` timestamp `{value}`: {err}"), )?)) }; @@ -488,7 +488,7 @@ impl From<(PackageName, ExcludeNewerValue)> for ExcludeNewerPackageEntry { fn from((package, timestamp): (PackageName, ExcludeNewerValue)) -> Self { Self { package, - setting: PackageExcludeNewer::Timestamp(Box::new(timestamp)), + setting: PackageExcludeNewer::Cutoff(Box::new(timestamp)), } } } @@ -512,7 +512,7 @@ impl<'de> serde::Deserialize<'de> for PackageExcludeNewer { E: serde::de::Error, { ExcludeNewerValue::from_str(v) - .map(|ts| PackageExcludeNewer::Timestamp(Box::new(ts))) + .map(|ts| PackageExcludeNewer::Cutoff(Box::new(ts))) .map_err(|e| E::custom(format!("failed to parse timestamp: {e}"))) } @@ -554,7 +554,7 @@ impl serde::Serialize for PackageExcludeNewer { S: serde::Serializer, { match self { - Self::Timestamp(timestamp) => timestamp.to_string().serialize(serializer), + Self::Cutoff(timestamp) => timestamp.to_string().serialize(serializer), Self::Disabled => serializer.serialize_bool(false), } } @@ -651,8 +651,8 @@ impl ExcludeNewerPackage { for (package, setting) in self { match (setting, other.get(package)) { ( - PackageExcludeNewer::Timestamp(self_timestamp), - Some(PackageExcludeNewer::Timestamp(other_timestamp)), + PackageExcludeNewer::Cutoff(self_timestamp), + Some(PackageExcludeNewer::Cutoff(other_timestamp)), ) => { if let Some(change) = self_timestamp.compare(other_timestamp) { return Some(ExcludeNewerPackageChange::PackageChanged( @@ -662,7 +662,7 @@ impl ExcludeNewerPackage { } } ( - PackageExcludeNewer::Timestamp(self_timestamp), + PackageExcludeNewer::Cutoff(self_timestamp), Some(PackageExcludeNewer::Disabled), ) => { return Some(ExcludeNewerPackageChange::PackageChanged( @@ -674,7 +674,7 @@ impl ExcludeNewerPackage { } ( PackageExcludeNewer::Disabled, - Some(PackageExcludeNewer::Timestamp(other_timestamp)), + Some(PackageExcludeNewer::Cutoff(other_timestamp)), ) => { return Some(ExcludeNewerPackageChange::PackageChanged( package.clone(), @@ -745,7 +745,7 @@ impl ExcludeNewer { /// if no exclude-newer is configured. pub fn exclude_newer_package(&self, package_name: &PackageName) -> Option { match self.package.get(package_name) { - Some(PackageExcludeNewer::Timestamp(timestamp)) => Some(timestamp.as_ref().clone()), + Some(PackageExcludeNewer::Cutoff(timestamp)) => Some(timestamp.as_ref().clone()), Some(PackageExcludeNewer::Disabled) => None, None => self.global.clone(), } @@ -789,7 +789,7 @@ impl std::fmt::Display for ExcludeNewer { write!(f, ", ")?; } match setting { - PackageExcludeNewer::Timestamp(timestamp) => { + PackageExcludeNewer::Cutoff(timestamp) => { write!(f, "{name}: {}", timestamp.as_ref())?; } PackageExcludeNewer::Disabled => { @@ -867,7 +867,7 @@ mod tests { let entry = ExcludeNewerPackageEntry::from_str("numpy=2023-01-01T00:00:00Z").unwrap(); assert_eq!(entry.package.as_ref(), "numpy"); match entry.setting { - PackageExcludeNewer::Timestamp(ref timestamp) => { + PackageExcludeNewer::Cutoff(ref timestamp) => { assert!(timestamp.to_string().contains("2023-01-01")); } PackageExcludeNewer::Disabled => panic!("expected timestamp"), @@ -876,7 +876,7 @@ mod tests { // Test with relative timestamp let entry = ExcludeNewerPackageEntry::from_str("requests=7 days").unwrap(); assert_eq!(entry.package.as_ref(), "requests"); - assert!(matches!(entry.setting, PackageExcludeNewer::Timestamp(_))); + assert!(matches!(entry.setting, PackageExcludeNewer::Cutoff(_))); // Test disabling exclude-newer per package let entry = ExcludeNewerPackageEntry::from_str("torch=false").unwrap(); diff --git a/crates/uv-resolver/src/lib.rs b/crates/uv-resolver/src/lib.rs index 5d1ff4916..756f2e57b 100644 --- a/crates/uv-resolver/src/lib.rs +++ b/crates/uv-resolver/src/lib.rs @@ -2,8 +2,8 @@ pub use dependency_mode::DependencyMode; pub use error::{ErrorTree, NoSolutionError, NoSolutionHeader, ResolveError, SentinelRange}; pub use exclude_newer::{ ExcludeNewer, ExcludeNewerChange, ExcludeNewerPackage, ExcludeNewerPackageChange, - ExcludeNewerPackageEntry, ExcludeNewerTimestamp, ExcludeNewerValue, ExcludeNewerValueChange, - PackageExcludeNewer, PackageExcludeNewerChange, + ExcludeNewerPackageEntry, ExcludeNewerValue, ExcludeNewerValueChange, PackageExcludeNewer, + PackageExcludeNewerChange, }; pub use exclusions::Exclusions; pub use flat_index::{FlatDistributions, FlatIndex}; diff --git a/crates/uv-resolver/src/lock/mod.rs b/crates/uv-resolver/src/lock/mod.rs index d2cfa03d5..f611408df 100644 --- a/crates/uv-resolver/src/lock/mod.rs +++ b/crates/uv-resolver/src/lock/mod.rs @@ -1073,7 +1073,7 @@ impl Lock { let mut package_table = toml_edit::Table::new(); for (name, setting) in &exclude_newer.package { match setting { - PackageExcludeNewer::Timestamp(exclude_newer_value) => { + PackageExcludeNewer::Cutoff(exclude_newer_value) => { if let Some(span) = exclude_newer_value.span() { // Serialize as inline table with timestamp and span let mut inline = toml_edit::InlineTable::new(); From 20774e0284dcdd0b4858d2d970ebd3c6301ce27a Mon Sep 17 00:00:00 2001 From: Liam Date: Tue, 9 Dec 2025 17:23:56 -0500 Subject: [PATCH 3/9] Update schema --- uv.schema.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/uv.schema.json b/uv.schema.json index 6aaac2a56..8c5736357 100644 --- a/uv.schema.json +++ b/uv.schema.json @@ -1204,7 +1204,7 @@ } }, "PackageExcludeNewer": { - "description": "Per-package exclude-newer setting.\n\nThis enum represents whether exclude-newer should be disabled for a package,\nor if a specific timestamp cutoff should be used.", + "description": "Per-package exclude-newer setting.\n\nThis enum represents whether exclude-newer should be disabled for a package,\nor if a specific cutoff (absolute or relative) should be used.", "oneOf": [ { "description": "Disable exclude-newer for this package (allow all versions regardless of upload date).", @@ -1212,16 +1212,16 @@ "const": "Disabled" }, { - "description": "Use this specific timestamp cutoff for this package.", + "description": "Use this specific cutoff for this package.", "type": "object", "properties": { - "Timestamp": { + "Cutoff": { "$ref": "#/definitions/ExcludeNewerTimestamp" } }, "additionalProperties": false, "required": [ - "Timestamp" + "Cutoff" ] } ] From 498faabaf70672c5759b6ffc664dbb90bbb06585 Mon Sep 17 00:00:00 2001 From: Liam Date: Tue, 9 Dec 2025 17:26:03 -0500 Subject: [PATCH 4/9] Rename variant and shuffle documentation --- crates/uv-resolver/src/exclude_newer.rs | 30 ++++++++++++------------- crates/uv-resolver/src/lock/mod.rs | 2 +- docs/concepts/resolution.md | 8 +++---- 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/crates/uv-resolver/src/exclude_newer.rs b/crates/uv-resolver/src/exclude_newer.rs index 9c256fcce..51ce92445 100644 --- a/crates/uv-resolver/src/exclude_newer.rs +++ b/crates/uv-resolver/src/exclude_newer.rs @@ -112,7 +112,7 @@ impl ExcludeNewerPackageChange { impl std::fmt::Display for ExcludeNewerPackageChange { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Self::PackageAdded(name, PackageExcludeNewer::Cutoff(value)) => { + Self::PackageAdded(name, PackageExcludeNewer::Enabled(value)) => { write!( f, "addition of exclude newer `{}` for package `{name}`", @@ -439,8 +439,8 @@ impl std::fmt::Display for ExcludeNewerValue { pub enum PackageExcludeNewer { /// Disable exclude-newer for this package (allow all versions regardless of upload date). Disabled, - /// Use this specific cutoff for this package. - Cutoff(Box), + /// Enable exclude-newer with this cutoff for this package. + Enabled(Box), } /// A package-specific exclude-newer entry. @@ -469,7 +469,7 @@ impl FromStr for ExcludeNewerPackageEntry { let setting = if value == "false" { PackageExcludeNewer::Disabled } else { - PackageExcludeNewer::Cutoff(Box::new(ExcludeNewerValue::from_str(value).map_err( + PackageExcludeNewer::Enabled(Box::new(ExcludeNewerValue::from_str(value).map_err( |err| format!("Invalid `exclude-newer-package` timestamp `{value}`: {err}"), )?)) }; @@ -488,7 +488,7 @@ impl From<(PackageName, ExcludeNewerValue)> for ExcludeNewerPackageEntry { fn from((package, timestamp): (PackageName, ExcludeNewerValue)) -> Self { Self { package, - setting: PackageExcludeNewer::Cutoff(Box::new(timestamp)), + setting: PackageExcludeNewer::Enabled(Box::new(timestamp)), } } } @@ -512,7 +512,7 @@ impl<'de> serde::Deserialize<'de> for PackageExcludeNewer { E: serde::de::Error, { ExcludeNewerValue::from_str(v) - .map(|ts| PackageExcludeNewer::Cutoff(Box::new(ts))) + .map(|ts| PackageExcludeNewer::Enabled(Box::new(ts))) .map_err(|e| E::custom(format!("failed to parse timestamp: {e}"))) } @@ -554,7 +554,7 @@ impl serde::Serialize for PackageExcludeNewer { S: serde::Serializer, { match self { - Self::Cutoff(timestamp) => timestamp.to_string().serialize(serializer), + Self::Enabled(timestamp) => timestamp.to_string().serialize(serializer), Self::Disabled => serializer.serialize_bool(false), } } @@ -651,8 +651,8 @@ impl ExcludeNewerPackage { for (package, setting) in self { match (setting, other.get(package)) { ( - PackageExcludeNewer::Cutoff(self_timestamp), - Some(PackageExcludeNewer::Cutoff(other_timestamp)), + PackageExcludeNewer::Enabled(self_timestamp), + Some(PackageExcludeNewer::Enabled(other_timestamp)), ) => { if let Some(change) = self_timestamp.compare(other_timestamp) { return Some(ExcludeNewerPackageChange::PackageChanged( @@ -662,7 +662,7 @@ impl ExcludeNewerPackage { } } ( - PackageExcludeNewer::Cutoff(self_timestamp), + PackageExcludeNewer::Enabled(self_timestamp), Some(PackageExcludeNewer::Disabled), ) => { return Some(ExcludeNewerPackageChange::PackageChanged( @@ -674,7 +674,7 @@ impl ExcludeNewerPackage { } ( PackageExcludeNewer::Disabled, - Some(PackageExcludeNewer::Cutoff(other_timestamp)), + Some(PackageExcludeNewer::Enabled(other_timestamp)), ) => { return Some(ExcludeNewerPackageChange::PackageChanged( package.clone(), @@ -745,7 +745,7 @@ impl ExcludeNewer { /// if no exclude-newer is configured. pub fn exclude_newer_package(&self, package_name: &PackageName) -> Option { match self.package.get(package_name) { - Some(PackageExcludeNewer::Cutoff(timestamp)) => Some(timestamp.as_ref().clone()), + Some(PackageExcludeNewer::Enabled(timestamp)) => Some(timestamp.as_ref().clone()), Some(PackageExcludeNewer::Disabled) => None, None => self.global.clone(), } @@ -789,7 +789,7 @@ impl std::fmt::Display for ExcludeNewer { write!(f, ", ")?; } match setting { - PackageExcludeNewer::Cutoff(timestamp) => { + PackageExcludeNewer::Enabled(timestamp) => { write!(f, "{name}: {}", timestamp.as_ref())?; } PackageExcludeNewer::Disabled => { @@ -867,7 +867,7 @@ mod tests { let entry = ExcludeNewerPackageEntry::from_str("numpy=2023-01-01T00:00:00Z").unwrap(); assert_eq!(entry.package.as_ref(), "numpy"); match entry.setting { - PackageExcludeNewer::Cutoff(ref timestamp) => { + PackageExcludeNewer::Enabled(ref timestamp) => { assert!(timestamp.to_string().contains("2023-01-01")); } PackageExcludeNewer::Disabled => panic!("expected timestamp"), @@ -876,7 +876,7 @@ mod tests { // Test with relative timestamp let entry = ExcludeNewerPackageEntry::from_str("requests=7 days").unwrap(); assert_eq!(entry.package.as_ref(), "requests"); - assert!(matches!(entry.setting, PackageExcludeNewer::Cutoff(_))); + assert!(matches!(entry.setting, PackageExcludeNewer::Enabled(_))); // Test disabling exclude-newer per package let entry = ExcludeNewerPackageEntry::from_str("torch=false").unwrap(); diff --git a/crates/uv-resolver/src/lock/mod.rs b/crates/uv-resolver/src/lock/mod.rs index f611408df..dc0d0f44a 100644 --- a/crates/uv-resolver/src/lock/mod.rs +++ b/crates/uv-resolver/src/lock/mod.rs @@ -1073,7 +1073,7 @@ impl Lock { let mut package_table = toml_edit::Table::new(); for (name, setting) in &exclude_newer.package { match setting { - PackageExcludeNewer::Cutoff(exclude_newer_value) => { + PackageExcludeNewer::Enabled(exclude_newer_value) => { if let Some(span) = exclude_newer_value.span() { // Serialize as inline table with timestamp and span let mut inline = toml_edit::InlineTable::new(); diff --git a/docs/concepts/resolution.md b/docs/concepts/resolution.md index c34f44d8e..000a4b253 100644 --- a/docs/concepts/resolution.md +++ b/docs/concepts/resolution.md @@ -654,10 +654,6 @@ may be specified as an [RFC 3339](https://www.rfc-editor.org/rfc/rfc3339.html) t `2006-12-02T02:07:43Z`) or a local date in the same format (e.g., `2006-12-02`) in your system's configured time zone. -Use `--exclude-newer-package =` to apply the cutoff to specific packages rather than -globally. The same flag also accepts `=false` to opt a package out of the `--exclude-newer` -restriction, e.g., to allow resolving packages from an index that does not publish upload times. - !!! important The package index must support the `upload-time` field as specified in @@ -693,6 +689,10 @@ Values may also be specified for specific packages, e.g., exclude-newer-package = { setuptools = "2006-12-02T02:07:43Z" } ``` +Use `--exclude-newer-package =` to apply the cutoff to specific packages rather than +globally. The same flag also accepts `=false` to opt a package out of the `--exclude-newer` +restriction, e.g., to allow resolving packages from an index that does not publish upload times. + Package-specific values will take precedence over global values. ## Dependency cooldowns From 72916d388173598a1bae16d570f9ce4b76c8e561 Mon Sep 17 00:00:00 2001 From: Liam Date: Tue, 9 Dec 2025 17:26:28 -0500 Subject: [PATCH 5/9] Update schema --- uv.schema.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/uv.schema.json b/uv.schema.json index 8c5736357..8231325ed 100644 --- a/uv.schema.json +++ b/uv.schema.json @@ -1212,16 +1212,16 @@ "const": "Disabled" }, { - "description": "Use this specific cutoff for this package.", + "description": "Enable exclude-newer with this cutoff for this package.", "type": "object", "properties": { - "Cutoff": { + "Enabled": { "$ref": "#/definitions/ExcludeNewerTimestamp" } }, "additionalProperties": false, "required": [ - "Cutoff" + "Enabled" ] } ] From e3eb27c27614e182a963278ac61abea36bea454e Mon Sep 17 00:00:00 2001 From: Liam Date: Tue, 9 Dec 2025 17:38:02 -0500 Subject: [PATCH 6/9] Tweak naming for consistency --- crates/uv-resolver/src/exclude_newer.rs | 36 +++++++++---------------- crates/uv/tests/it/pip_compile.rs | 2 +- 2 files changed, 13 insertions(+), 25 deletions(-) diff --git a/crates/uv-resolver/src/exclude_newer.rs b/crates/uv-resolver/src/exclude_newer.rs index 51ce92445..8ddc3a144 100644 --- a/crates/uv-resolver/src/exclude_newer.rs +++ b/crates/uv-resolver/src/exclude_newer.rs @@ -122,7 +122,7 @@ impl std::fmt::Display for ExcludeNewerPackageChange { Self::PackageAdded(name, PackageExcludeNewer::Disabled) => { write!( f, - "addition of exclude newer exception for package `{name}`" + "addition of exclude newer exclusion for package `{name}`" ) } Self::PackageRemoved(name) => { @@ -470,7 +470,7 @@ impl FromStr for ExcludeNewerPackageEntry { PackageExcludeNewer::Disabled } else { PackageExcludeNewer::Enabled(Box::new(ExcludeNewerValue::from_str(value).map_err( - |err| format!("Invalid `exclude-newer-package` timestamp `{value}`: {err}"), + |err| format!("Invalid `exclude-newer-package` value `{value}`: {err}"), )?)) }; @@ -504,7 +504,9 @@ impl<'de> serde::Deserialize<'de> for PackageExcludeNewer { type Value = PackageExcludeNewer; fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - formatter.write_str("a timestamp string or false/null to disable exclude-newer") + formatter.write_str( + "a date/timestamp/duration string, or false to disable exclude-newer", + ) } fn visit_str(self, v: &str) -> Result @@ -513,7 +515,7 @@ impl<'de> serde::Deserialize<'de> for PackageExcludeNewer { { ExcludeNewerValue::from_str(v) .map(|ts| PackageExcludeNewer::Enabled(Box::new(ts))) - .map_err(|e| E::custom(format!("failed to parse timestamp: {e}"))) + .map_err(|e| E::custom(format!("failed to parse exclude-newer value: {e}"))) } fn visit_bool(self, v: bool) -> Result @@ -528,20 +530,6 @@ impl<'de> serde::Deserialize<'de> for PackageExcludeNewer { Ok(PackageExcludeNewer::Disabled) } } - - fn visit_none(self) -> Result - where - E: serde::de::Error, - { - Ok(PackageExcludeNewer::Disabled) - } - - fn visit_unit(self) -> Result - where - E: serde::de::Error, - { - Ok(PackageExcludeNewer::Disabled) - } } deserializer.deserialize_any(Visitor) @@ -580,10 +568,10 @@ impl std::fmt::Display for PackageExcludeNewerChange { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Disabled { was } => { - write!(f, "disable exclude newer (was `{was}`)") + write!(f, "add exclude newer exclusion (was `{was}`)") } Self::Enabled { now } => { - write!(f, "enable exclude newer `{now}`") + write!(f, "remove exclude newer exclusion (now `{now}`)") } Self::TimestampChanged(change) => write!(f, "{change}"), } @@ -739,10 +727,10 @@ impl ExcludeNewer { Self { global, package } } - /// Returns the exclude-newer timestamp for a specific package, returning `Some(timestamp)` - /// if the package has a package-specific timestamp or falls back to the global timestamp if - /// set, or `None` if exclude-newer is explicitly disabled for the package (set to `false`) or - /// if no exclude-newer is configured. + /// Returns the exclude-newer value for a specific package, returning `Some(value)` if the + /// package has a package-specific setting or falls back to the global value if set, or `None` + /// if exclude-newer is explicitly disabled for the package (set to `false`) or if no + /// exclude-newer is configured. pub fn exclude_newer_package(&self, package_name: &PackageName) -> Option { match self.package.get(package_name) { Some(PackageExcludeNewer::Enabled(timestamp)) => Some(timestamp.as_ref().clone()), diff --git a/crates/uv/tests/it/pip_compile.rs b/crates/uv/tests/it/pip_compile.rs index 764afb794..e22239fad 100644 --- a/crates/uv/tests/it/pip_compile.rs +++ b/crates/uv/tests/it/pip_compile.rs @@ -3583,7 +3583,7 @@ fn compile_exclude_newer_package_errors() -> Result<()> { ----- stdout ----- ----- stderr ----- - error: invalid value 'tqdm=invalid-date' for '--exclude-newer-package ': Invalid `exclude-newer-package` timestamp `invalid-date`: `invalid-date` could not be parsed as a valid exclude-newer value (expected a date like `2024-01-01`, a timestamp like `2024-01-01T00:00:00Z`, or a duration like `3 days` or `P3D`) + error: invalid value 'tqdm=invalid-date' for '--exclude-newer-package ': Invalid `exclude-newer-package` value `invalid-date`: `invalid-date` could not be parsed as a valid exclude-newer value (expected a date like `2024-01-01`, a timestamp like `2024-01-01T00:00:00Z`, or a duration like `3 days` or `P3D`) For more information, try '--help'. " From 64e7e294140a7bdac685167eabcf461c19a5b0b1 Mon Sep 17 00:00:00 2001 From: Liam Date: Tue, 9 Dec 2025 17:52:37 -0500 Subject: [PATCH 7/9] Use `iniconfig` for disabled exclude newer test --- crates/uv-resolver/src/exclude_newer.rs | 16 ++- crates/uv/tests/it/lock.rs | 180 +++++------------------- docs/concepts/resolution.md | 3 +- 3 files changed, 51 insertions(+), 148 deletions(-) diff --git a/crates/uv-resolver/src/exclude_newer.rs b/crates/uv-resolver/src/exclude_newer.rs index 8ddc3a144..b8b1245ee 100644 --- a/crates/uv-resolver/src/exclude_newer.rs +++ b/crates/uv-resolver/src/exclude_newer.rs @@ -7,6 +7,8 @@ use std::{ use jiff::{Span, Timestamp, ToSpan, Unit, tz::TimeZone}; use rustc_hash::FxHashMap; +use serde::Deserialize; +use serde::de::value::MapAccessDeserializer; use uv_normalize::PackageName; #[derive(Debug, Clone, PartialEq, Eq)] @@ -500,12 +502,13 @@ impl<'de> serde::Deserialize<'de> for PackageExcludeNewer { { struct Visitor; - impl serde::de::Visitor<'_> for Visitor { + impl<'de> serde::de::Visitor<'de> for Visitor { type Value = PackageExcludeNewer; fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { formatter.write_str( - "a date/timestamp/duration string, or false to disable exclude-newer", + "a date/timestamp/duration string, false to disable exclude-newer, or a table \ + with timestamp/span", ) } @@ -530,6 +533,15 @@ impl<'de> serde::Deserialize<'de> for PackageExcludeNewer { Ok(PackageExcludeNewer::Disabled) } } + + fn visit_map(self, map: A) -> Result + where + A: serde::de::MapAccess<'de>, + { + Ok(PackageExcludeNewer::Enabled(Box::new( + ExcludeNewerValue::deserialize(MapAccessDeserializer::new(map))?, + ))) + } } deserializer.deserialize_any(Visitor) diff --git a/crates/uv/tests/it/lock.rs b/crates/uv/tests/it/lock.rs index e2e8780ba..e9d9c81af 100644 --- a/crates/uv/tests/it/lock.rs +++ b/crates/uv/tests/it/lock.rs @@ -31113,24 +31113,24 @@ fn lock_exclude_newer_package_disable() -> Result<()> { name = "project" version = "0.1.0" requires-python = ">=3.12" - dependencies = ["requests", "tqdm"] + dependencies = ["idna", "iniconfig"] "#, )?; - // Lock with global exclude-newer and disable it for tqdm + // Lock with global exclude-newer and disable it for idna uv_snapshot!(context.filters(), context .lock() .env_remove(EnvVars::UV_EXCLUDE_NEWER) .arg("--exclude-newer") .arg("2022-04-04T12:00:00Z") .arg("--exclude-newer-package") - .arg("tqdm=false"), @r###" + .arg("idna=false"), @r###" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- - Resolved 8 packages in [TIME] + Resolved 3 packages in [TIME] "###); let lock = context.read("uv.lock"); @@ -31148,42 +31148,24 @@ fn lock_exclude_newer_package_disable() -> Result<()> { exclude-newer = "2022-04-04T12:00:00Z" [options.exclude-newer-package] - tqdm = false - - [[package]] - name = "certifi" - version = "2021.10.8" - source = { registry = "https://pypi.org/simple" } - sdist = { url = "https://files.pythonhosted.org/packages/6c/ae/d26450834f0acc9e3d1f74508da6df1551ceab6c2ce0766a593362d6d57f/certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872", size = 151214, upload-time = "2021-10-08T19:32:15.277Z" } - wheels = [ - { url = "https://files.pythonhosted.org/packages/37/45/946c02767aabb873146011e665728b680884cd8fe70dde973c640e45b775/certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569", size = 149195, upload-time = "2021-10-08T19:32:10.712Z" }, - ] - - [[package]] - name = "charset-normalizer" - version = "2.0.12" - source = { registry = "https://pypi.org/simple" } - sdist = { url = "https://files.pythonhosted.org/packages/56/31/7bcaf657fafb3c6db8c787a865434290b726653c912085fbd371e9b92e1c/charset-normalizer-2.0.12.tar.gz", hash = "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597", size = 79105, upload-time = "2022-02-12T14:33:13.788Z" } - wheels = [ - { url = "https://files.pythonhosted.org/packages/06/b3/24afc8868eba069a7f03650ac750a778862dc34941a4bebeb58706715726/charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df", size = 39623, upload-time = "2022-02-12T14:33:12.294Z" }, - ] - - [[package]] - name = "colorama" - version = "0.4.4" - source = { registry = "https://pypi.org/simple" } - sdist = { url = "https://files.pythonhosted.org/packages/1f/bb/5d3246097ab77fa083a61bd8d3d527b7ae063c7d8e8671b1cf8c4ec10cbe/colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b", size = 27813, upload-time = "2020-10-15T18:36:33.372Z" } - wheels = [ - { url = "https://files.pythonhosted.org/packages/44/98/5b86278fbbf250d239ae0ecb724f8572af1c91f4a11edf4d36a206189440/colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2", size = 16028, upload-time = "2020-10-13T02:42:26.463Z" }, - ] + idna = false [[package]] name = "idna" - version = "3.3" + version = "3.11" source = { registry = "https://pypi.org/simple" } - sdist = { url = "https://files.pythonhosted.org/packages/62/08/e3fc7c8161090f742f504f40b1bccbfc544d4a4e09eb774bf40aafce5436/idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d", size = 286689, upload-time = "2021-10-12T23:33:41.312Z" } + sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/04/a2/d918dcd22354d8958fe113e1a3630137e0fc8b44859ade3063982eacd2a4/idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff", size = 61160, upload-time = "2021-10-12T23:33:38.02Z" }, + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, + ] + + [[package]] + name = "iniconfig" + version = "1.1.1" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/23/a2/97899f6bd0e873fed3a7e67ae8d3a08b21799430fb4da15cfedf10d6e2c2/iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32", size = 8104, upload-time = "2020-10-14T10:20:18.572Z" } + wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/dd/b3c12c6d707058fa947864b67f0c4e0c39ef8610988d7baea9578f3c48f3/iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3", size = 4990, upload-time = "2020-10-16T17:37:23.05Z" }, ] [[package]] @@ -31191,50 +31173,14 @@ fn lock_exclude_newer_package_disable() -> Result<()> { version = "0.1.0" source = { virtual = "." } dependencies = [ - { name = "requests" }, - { name = "tqdm" }, + { name = "idna" }, + { name = "iniconfig" }, ] [package.metadata] requires-dist = [ - { name = "requests" }, - { name = "tqdm" }, - ] - - [[package]] - name = "requests" - version = "2.27.1" - source = { registry = "https://pypi.org/simple" } - dependencies = [ - { name = "certifi" }, - { name = "charset-normalizer" }, { name = "idna" }, - { name = "urllib3" }, - ] - sdist = { url = "https://files.pythonhosted.org/packages/60/f3/26ff3767f099b73e0efa138a9998da67890793bfa475d8278f84a30fec77/requests-2.27.1.tar.gz", hash = "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61", size = 106758, upload-time = "2022-01-05T15:40:51.698Z" } - wheels = [ - { url = "https://files.pythonhosted.org/packages/2d/61/08076519c80041bc0ffa1a8af0cbd3bf3e2b62af10435d269a9d0f40564d/requests-2.27.1-py2.py3-none-any.whl", hash = "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d", size = 63133, upload-time = "2022-01-05T15:40:49.334Z" }, - ] - - [[package]] - name = "tqdm" - version = "4.67.1" - source = { registry = "https://pypi.org/simple" } - dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, - ] - sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" } - wheels = [ - { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" }, - ] - - [[package]] - name = "urllib3" - version = "1.26.9" - source = { registry = "https://pypi.org/simple" } - sdist = { url = "https://files.pythonhosted.org/packages/1b/a5/4eab74853625505725cefdf168f48661b2cd04e7843ab836f3f63abf81da/urllib3-1.26.9.tar.gz", hash = "sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e", size = 295258, upload-time = "2022-03-16T13:28:19.197Z" } - wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/03/062e6444ce4baf1eac17a6a0ebfe36bb1ad05e1df0e20b110de59c278498/urllib3-1.26.9-py2.py3-none-any.whl", hash = "sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14", size = 138990, upload-time = "2022-03-16T13:28:16.026Z" }, + { name = "iniconfig" }, ] "# ); @@ -31255,7 +31201,7 @@ fn lock_exclude_newer_package() -> Result<()> { name = "project" version = "0.1.0" requires-python = ">=3.12" - dependencies = ["requests", "tqdm"] + dependencies = ["idna", "iniconfig"] "#, )?; @@ -31266,13 +31212,13 @@ fn lock_exclude_newer_package() -> Result<()> { .arg("--exclude-newer") .arg("2022-04-04T12:00:00Z") .arg("--exclude-newer-package") - .arg("tqdm=2022-09-04T00:00:00Z"), @r###" + .arg("idna=2022-09-04T00:00:00Z"), @r###" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- - Resolved 8 packages in [TIME] + Resolved 3 packages in [TIME] "###); let lock = context.read("uv.lock"); @@ -31290,34 +31236,7 @@ fn lock_exclude_newer_package() -> Result<()> { exclude-newer = "2022-04-04T12:00:00Z" [options.exclude-newer-package] - tqdm = "2022-09-04T00:00:00Z" - - [[package]] - name = "certifi" - version = "2021.10.8" - source = { registry = "https://pypi.org/simple" } - sdist = { url = "https://files.pythonhosted.org/packages/6c/ae/d26450834f0acc9e3d1f74508da6df1551ceab6c2ce0766a593362d6d57f/certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872", size = 151214, upload-time = "2021-10-08T19:32:15.277Z" } - wheels = [ - { url = "https://files.pythonhosted.org/packages/37/45/946c02767aabb873146011e665728b680884cd8fe70dde973c640e45b775/certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569", size = 149195, upload-time = "2021-10-08T19:32:10.712Z" }, - ] - - [[package]] - name = "charset-normalizer" - version = "2.0.12" - source = { registry = "https://pypi.org/simple" } - sdist = { url = "https://files.pythonhosted.org/packages/56/31/7bcaf657fafb3c6db8c787a865434290b726653c912085fbd371e9b92e1c/charset-normalizer-2.0.12.tar.gz", hash = "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597", size = 79105, upload-time = "2022-02-12T14:33:13.788Z" } - wheels = [ - { url = "https://files.pythonhosted.org/packages/06/b3/24afc8868eba069a7f03650ac750a778862dc34941a4bebeb58706715726/charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df", size = 39623, upload-time = "2022-02-12T14:33:12.294Z" }, - ] - - [[package]] - name = "colorama" - version = "0.4.4" - source = { registry = "https://pypi.org/simple" } - sdist = { url = "https://files.pythonhosted.org/packages/1f/bb/5d3246097ab77fa083a61bd8d3d527b7ae063c7d8e8671b1cf8c4ec10cbe/colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b", size = 27813, upload-time = "2020-10-15T18:36:33.372Z" } - wheels = [ - { url = "https://files.pythonhosted.org/packages/44/98/5b86278fbbf250d239ae0ecb724f8572af1c91f4a11edf4d36a206189440/colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2", size = 16028, upload-time = "2020-10-13T02:42:26.463Z" }, - ] + idna = "2022-09-04T00:00:00Z" [[package]] name = "idna" @@ -31328,55 +31247,28 @@ fn lock_exclude_newer_package() -> Result<()> { { url = "https://files.pythonhosted.org/packages/04/a2/d918dcd22354d8958fe113e1a3630137e0fc8b44859ade3063982eacd2a4/idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff", size = 61160, upload-time = "2021-10-12T23:33:38.02Z" }, ] + [[package]] + name = "iniconfig" + version = "1.1.1" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/23/a2/97899f6bd0e873fed3a7e67ae8d3a08b21799430fb4da15cfedf10d6e2c2/iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32", size = 8104, upload-time = "2020-10-14T10:20:18.572Z" } + wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/dd/b3c12c6d707058fa947864b67f0c4e0c39ef8610988d7baea9578f3c48f3/iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3", size = 4990, upload-time = "2020-10-16T17:37:23.05Z" }, + ] + [[package]] name = "project" version = "0.1.0" source = { virtual = "." } dependencies = [ - { name = "requests" }, - { name = "tqdm" }, + { name = "idna" }, + { name = "iniconfig" }, ] [package.metadata] requires-dist = [ - { name = "requests" }, - { name = "tqdm" }, - ] - - [[package]] - name = "requests" - version = "2.27.1" - source = { registry = "https://pypi.org/simple" } - dependencies = [ - { name = "certifi" }, - { name = "charset-normalizer" }, { name = "idna" }, - { name = "urllib3" }, - ] - sdist = { url = "https://files.pythonhosted.org/packages/60/f3/26ff3767f099b73e0efa138a9998da67890793bfa475d8278f84a30fec77/requests-2.27.1.tar.gz", hash = "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61", size = 106758, upload-time = "2022-01-05T15:40:51.698Z" } - wheels = [ - { url = "https://files.pythonhosted.org/packages/2d/61/08076519c80041bc0ffa1a8af0cbd3bf3e2b62af10435d269a9d0f40564d/requests-2.27.1-py2.py3-none-any.whl", hash = "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d", size = 63133, upload-time = "2022-01-05T15:40:49.334Z" }, - ] - - [[package]] - name = "tqdm" - version = "4.64.1" - source = { registry = "https://pypi.org/simple" } - dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, - ] - sdist = { url = "https://files.pythonhosted.org/packages/c1/c2/d8a40e5363fb01806870e444fc1d066282743292ff32a9da54af51ce36a2/tqdm-4.64.1.tar.gz", hash = "sha256:5f4f682a004951c1b450bc753c710e9280c5746ce6ffedee253ddbcbf54cf1e4", size = 169599, upload-time = "2022-09-03T11:10:30.943Z" } - wheels = [ - { url = "https://files.pythonhosted.org/packages/47/bb/849011636c4da2e44f1253cd927cfb20ada4374d8b3a4e425416e84900cc/tqdm-4.64.1-py2.py3-none-any.whl", hash = "sha256:6fee160d6ffcd1b1c68c65f14c829c22832bc401726335ce92c52d395944a6a1", size = 78468, upload-time = "2022-09-03T11:10:27.148Z" }, - ] - - [[package]] - name = "urllib3" - version = "1.26.9" - source = { registry = "https://pypi.org/simple" } - sdist = { url = "https://files.pythonhosted.org/packages/1b/a5/4eab74853625505725cefdf168f48661b2cd04e7843ab836f3f63abf81da/urllib3-1.26.9.tar.gz", hash = "sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e", size = 295258, upload-time = "2022-03-16T13:28:19.197Z" } - wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/03/062e6444ce4baf1eac17a6a0ebfe36bb1ad05e1df0e20b110de59c278498/urllib3-1.26.9-py2.py3-none-any.whl", hash = "sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14", size = 138990, upload-time = "2022-03-16T13:28:16.026Z" }, + { name = "iniconfig" }, ] "# ); diff --git a/docs/concepts/resolution.md b/docs/concepts/resolution.md index 000a4b253..293dad3f9 100644 --- a/docs/concepts/resolution.md +++ b/docs/concepts/resolution.md @@ -689,8 +689,7 @@ Values may also be specified for specific packages, e.g., exclude-newer-package = { setuptools = "2006-12-02T02:07:43Z" } ``` -Use `--exclude-newer-package =` to apply the cutoff to specific packages rather than -globally. The same flag also accepts `=false` to opt a package out of the `--exclude-newer` +The same flag also accepts `=false` to opt a package out of the `--exclude-newer` restriction, e.g., to allow resolving packages from an index that does not publish upload times. Package-specific values will take precedence over global values. From 1c13ffcebec3a547140c07814f67b411a235a2e4 Mon Sep 17 00:00:00 2001 From: Liam Date: Tue, 9 Dec 2025 18:01:31 -0500 Subject: [PATCH 8/9] Add separate test for disable --- crates/uv/tests/it/lock.rs | 86 +++++++++++++++++++++++++++++++------- 1 file changed, 70 insertions(+), 16 deletions(-) diff --git a/crates/uv/tests/it/lock.rs b/crates/uv/tests/it/lock.rs index e9d9c81af..81763211e 100644 --- a/crates/uv/tests/it/lock.rs +++ b/crates/uv/tests/it/lock.rs @@ -31201,7 +31201,7 @@ fn lock_exclude_newer_package() -> Result<()> { name = "project" version = "0.1.0" requires-python = ">=3.12" - dependencies = ["idna", "iniconfig"] + dependencies = ["requests", "tqdm"] "#, )?; @@ -31212,13 +31212,13 @@ fn lock_exclude_newer_package() -> Result<()> { .arg("--exclude-newer") .arg("2022-04-04T12:00:00Z") .arg("--exclude-newer-package") - .arg("idna=2022-09-04T00:00:00Z"), @r###" + .arg("tqdm=2022-09-04T00:00:00Z"), @r###" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- - Resolved 3 packages in [TIME] + Resolved 8 packages in [TIME] "###); let lock = context.read("uv.lock"); @@ -31236,7 +31236,34 @@ fn lock_exclude_newer_package() -> Result<()> { exclude-newer = "2022-04-04T12:00:00Z" [options.exclude-newer-package] - idna = "2022-09-04T00:00:00Z" + tqdm = "2022-09-04T00:00:00Z" + + [[package]] + name = "certifi" + version = "2021.10.8" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/6c/ae/d26450834f0acc9e3d1f74508da6df1551ceab6c2ce0766a593362d6d57f/certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872", size = 151214, upload-time = "2021-10-08T19:32:15.277Z" } + wheels = [ + { url = "https://files.pythonhosted.org/packages/37/45/946c02767aabb873146011e665728b680884cd8fe70dde973c640e45b775/certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569", size = 149195, upload-time = "2021-10-08T19:32:10.712Z" }, + ] + + [[package]] + name = "charset-normalizer" + version = "2.0.12" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/56/31/7bcaf657fafb3c6db8c787a865434290b726653c912085fbd371e9b92e1c/charset-normalizer-2.0.12.tar.gz", hash = "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597", size = 79105, upload-time = "2022-02-12T14:33:13.788Z" } + wheels = [ + { url = "https://files.pythonhosted.org/packages/06/b3/24afc8868eba069a7f03650ac750a778862dc34941a4bebeb58706715726/charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df", size = 39623, upload-time = "2022-02-12T14:33:12.294Z" }, + ] + + [[package]] + name = "colorama" + version = "0.4.4" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/1f/bb/5d3246097ab77fa083a61bd8d3d527b7ae063c7d8e8671b1cf8c4ec10cbe/colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b", size = 27813, upload-time = "2020-10-15T18:36:33.372Z" } + wheels = [ + { url = "https://files.pythonhosted.org/packages/44/98/5b86278fbbf250d239ae0ecb724f8572af1c91f4a11edf4d36a206189440/colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2", size = 16028, upload-time = "2020-10-13T02:42:26.463Z" }, + ] [[package]] name = "idna" @@ -31247,28 +31274,55 @@ fn lock_exclude_newer_package() -> Result<()> { { url = "https://files.pythonhosted.org/packages/04/a2/d918dcd22354d8958fe113e1a3630137e0fc8b44859ade3063982eacd2a4/idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff", size = 61160, upload-time = "2021-10-12T23:33:38.02Z" }, ] - [[package]] - name = "iniconfig" - version = "1.1.1" - source = { registry = "https://pypi.org/simple" } - sdist = { url = "https://files.pythonhosted.org/packages/23/a2/97899f6bd0e873fed3a7e67ae8d3a08b21799430fb4da15cfedf10d6e2c2/iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32", size = 8104, upload-time = "2020-10-14T10:20:18.572Z" } - wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/dd/b3c12c6d707058fa947864b67f0c4e0c39ef8610988d7baea9578f3c48f3/iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3", size = 4990, upload-time = "2020-10-16T17:37:23.05Z" }, - ] - [[package]] name = "project" version = "0.1.0" source = { virtual = "." } dependencies = [ - { name = "idna" }, - { name = "iniconfig" }, + { name = "requests" }, + { name = "tqdm" }, ] [package.metadata] requires-dist = [ + { name = "requests" }, + { name = "tqdm" }, + ] + + [[package]] + name = "requests" + version = "2.27.1" + source = { registry = "https://pypi.org/simple" } + dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, { name = "idna" }, - { name = "iniconfig" }, + { name = "urllib3" }, + ] + sdist = { url = "https://files.pythonhosted.org/packages/60/f3/26ff3767f099b73e0efa138a9998da67890793bfa475d8278f84a30fec77/requests-2.27.1.tar.gz", hash = "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61", size = 106758, upload-time = "2022-01-05T15:40:51.698Z" } + wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/61/08076519c80041bc0ffa1a8af0cbd3bf3e2b62af10435d269a9d0f40564d/requests-2.27.1-py2.py3-none-any.whl", hash = "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d", size = 63133, upload-time = "2022-01-05T15:40:49.334Z" }, + ] + + [[package]] + name = "tqdm" + version = "4.64.1" + source = { registry = "https://pypi.org/simple" } + dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + ] + sdist = { url = "https://files.pythonhosted.org/packages/c1/c2/d8a40e5363fb01806870e444fc1d066282743292ff32a9da54af51ce36a2/tqdm-4.64.1.tar.gz", hash = "sha256:5f4f682a004951c1b450bc753c710e9280c5746ce6ffedee253ddbcbf54cf1e4", size = 169599, upload-time = "2022-09-03T11:10:30.943Z" } + wheels = [ + { url = "https://files.pythonhosted.org/packages/47/bb/849011636c4da2e44f1253cd927cfb20ada4374d8b3a4e425416e84900cc/tqdm-4.64.1-py2.py3-none-any.whl", hash = "sha256:6fee160d6ffcd1b1c68c65f14c829c22832bc401726335ce92c52d395944a6a1", size = 78468, upload-time = "2022-09-03T11:10:27.148Z" }, + ] + + [[package]] + name = "urllib3" + version = "1.26.9" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/1b/a5/4eab74853625505725cefdf168f48661b2cd04e7843ab836f3f63abf81da/urllib3-1.26.9.tar.gz", hash = "sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e", size = 295258, upload-time = "2022-03-16T13:28:19.197Z" } + wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/03/062e6444ce4baf1eac17a6a0ebfe36bb1ad05e1df0e20b110de59c278498/urllib3-1.26.9-py2.py3-none-any.whl", hash = "sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14", size = 138990, upload-time = "2022-03-16T13:28:16.026Z" }, ] "# ); From 54bdc410e21f9169901cf7fa2543c39c85282370 Mon Sep 17 00:00:00 2001 From: Liam Date: Thu, 11 Dec 2025 10:56:25 -0500 Subject: [PATCH 9/9] Remove tests --- crates/uv-resolver/src/exclude_newer.rs | 68 ------------------------- 1 file changed, 68 deletions(-) diff --git a/crates/uv-resolver/src/exclude_newer.rs b/crates/uv-resolver/src/exclude_newer.rs index b8b1245ee..878519ef7 100644 --- a/crates/uv-resolver/src/exclude_newer.rs +++ b/crates/uv-resolver/src/exclude_newer.rs @@ -816,71 +816,3 @@ impl schemars::JsonSchema for ExcludeNewerValue { }) } } - -#[cfg(test)] -mod tests { - use super::*; - use std::str::FromStr; - - #[test] - fn test_exclude_newer_timestamp_absolute() { - // Test RFC 3339 timestamp - let timestamp = ExcludeNewerValue::from_str("2023-01-01T00:00:00Z").unwrap(); - assert!(timestamp.to_string().contains("2023-01-01")); - - // Test local date - let timestamp = ExcludeNewerValue::from_str("2023-06-15").unwrap(); - assert!(timestamp.to_string().contains("2023-06-16")); // Should be next day - } - - #[test] - fn test_exclude_newer_timestamp_relative() { - // Test "1 hour" - simpler test case - let timestamp = ExcludeNewerValue::from_str("1 hour").unwrap(); - let now = jiff::Timestamp::now(); - let diff = now.as_second() - timestamp.timestamp.as_second(); - // Should be approximately 1 hour (3600 seconds) ago - assert!( - (3550..=3650).contains(&diff), - "Expected ~3600 seconds, got {diff}" - ); - - // Test that we get a timestamp in the past - assert!(timestamp.timestamp < now, "Timestamp should be in the past"); - - // Test parsing succeeds for various formats - assert!(ExcludeNewerValue::from_str("2 days").is_ok()); - assert!(ExcludeNewerValue::from_str("1 week").is_ok()); - assert!(ExcludeNewerValue::from_str("30 days").is_ok()); - } - - #[test] - fn test_exclude_newer_timestamp_invalid() { - // Test invalid formats - assert!(ExcludeNewerValue::from_str("invalid").is_err()); - assert!(ExcludeNewerValue::from_str("not a date").is_err()); - assert!(ExcludeNewerValue::from_str("").is_err()); - } - - #[test] - fn test_exclude_newer_package_entry() { - let entry = ExcludeNewerPackageEntry::from_str("numpy=2023-01-01T00:00:00Z").unwrap(); - assert_eq!(entry.package.as_ref(), "numpy"); - match entry.setting { - PackageExcludeNewer::Enabled(ref timestamp) => { - assert!(timestamp.to_string().contains("2023-01-01")); - } - PackageExcludeNewer::Disabled => panic!("expected timestamp"), - } - - // Test with relative timestamp - let entry = ExcludeNewerPackageEntry::from_str("requests=7 days").unwrap(); - assert_eq!(entry.package.as_ref(), "requests"); - assert!(matches!(entry.setting, PackageExcludeNewer::Enabled(_))); - - // Test disabling exclude-newer per package - let entry = ExcludeNewerPackageEntry::from_str("torch=false").unwrap(); - assert_eq!(entry.package.as_ref(), "torch"); - assert!(matches!(entry.setting, PackageExcludeNewer::Disabled)); - } -}