This commit is contained in:
liam 2025-12-16 17:38:22 +01:00 committed by GitHub
commit b71e9d8ef0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 367 additions and 65 deletions

View File

@ -7,6 +7,8 @@ use std::{
use jiff::{Span, Timestamp, ToSpan, Unit, tz::TimeZone}; use jiff::{Span, Timestamp, ToSpan, Unit, tz::TimeZone};
use rustc_hash::FxHashMap; use rustc_hash::FxHashMap;
use serde::Deserialize;
use serde::de::value::MapAccessDeserializer;
use uv_normalize::PackageName; use uv_normalize::PackageName;
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
@ -95,9 +97,9 @@ impl std::fmt::Display for ExcludeNewerChange {
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub enum ExcludeNewerPackageChange { pub enum ExcludeNewerPackageChange {
PackageAdded(PackageName, ExcludeNewerValue), PackageAdded(PackageName, PackageExcludeNewer),
PackageRemoved(PackageName), PackageRemoved(PackageName),
PackageChanged(PackageName, ExcludeNewerValueChange), PackageChanged(PackageName, Box<PackageExcludeNewerChange>),
} }
impl ExcludeNewerPackageChange { impl ExcludeNewerPackageChange {
@ -112,18 +114,23 @@ impl ExcludeNewerPackageChange {
impl std::fmt::Display for ExcludeNewerPackageChange { impl std::fmt::Display for ExcludeNewerPackageChange {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { match self {
Self::PackageAdded(name, value) => { Self::PackageAdded(name, PackageExcludeNewer::Enabled(value)) => {
write!( write!(
f, 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 exclusion for package `{name}`"
) )
} }
Self::PackageRemoved(name) => { Self::PackageRemoved(name) => {
write!(f, "removal of exclude newer for package `{name}`") write!(f, "removal of exclude newer for package `{name}`")
} }
Self::PackageChanged(name, change) => { Self::PackageChanged(name, change) => write!(f, "{change} for package `{name}`"),
write!(f, "{change} for package `{name}`")
}
} }
} }
} }
@ -255,7 +262,7 @@ impl ExcludeNewerValue {
self.span.as_ref() self.span.as_ref()
} }
/// Create a new [`ExcludeNewerTimestamp`]. /// Create a new [`ExcludeNewerValue`].
pub fn new(timestamp: Timestamp, span: Option<ExcludeNewerSpan>) -> Self { pub fn new(timestamp: Timestamp, span: Option<ExcludeNewerSpan>) -> Self {
Self { timestamp, span } Self { timestamp, span }
} }
@ -326,7 +333,7 @@ fn format_exclude_newer_error(
impl FromStr for ExcludeNewerValue { impl FromStr for ExcludeNewerValue {
type Err = String; 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 /// 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 /// (e.g., `2006-12-02`), "friendly" durations (e.g., `1 week`, `30 days`), and ISO 8601
@ -425,47 +432,170 @@ impl std::fmt::Display for ExcludeNewerValue {
} }
} }
/// Per-package exclude-newer setting.
///
/// This enum represents whether exclude-newer should be disabled for a package,
/// 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,
/// Enable exclude-newer with this cutoff for this package.
Enabled(Box<ExcludeNewerValue>),
}
/// A package-specific exclude-newer entry. /// A package-specific exclude-newer entry.
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct ExcludeNewerPackageEntry { pub struct ExcludeNewerPackageEntry {
pub package: PackageName, pub package: PackageName,
pub timestamp: ExcludeNewerValue, pub setting: PackageExcludeNewer,
} }
impl FromStr for ExcludeNewerPackageEntry { impl FromStr for ExcludeNewerPackageEntry {
type Err = String; 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<Self, Self::Err> { fn from_str(s: &str) -> Result<Self, Self::Err> {
let Some((package, date)) = s.split_once('=') else { let Some((package, value)) = s.split_once('=') else {
return Err(format!( 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| { let package = PackageName::from_str(package).map_err(|err| {
format!("Invalid `exclude-newer-package` package name `{package}`: {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::Enabled(Box::new(ExcludeNewerValue::from_str(value).map_err(
|err| format!("Invalid `exclude-newer-package` value `{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 { impl From<(PackageName, ExcludeNewerValue)> for ExcludeNewerPackageEntry {
fn from((package, timestamp): (PackageName, ExcludeNewerValue)) -> Self { fn from((package, timestamp): (PackageName, ExcludeNewerValue)) -> Self {
Self { package, timestamp } Self {
package,
setting: PackageExcludeNewer::Enabled(Box::new(timestamp)),
}
}
}
impl<'de> serde::Deserialize<'de> for PackageExcludeNewer {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
struct 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, false to disable exclude-newer, or a table \
with timestamp/span",
)
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
ExcludeNewerValue::from_str(v)
.map(|ts| PackageExcludeNewer::Enabled(Box::new(ts)))
.map_err(|e| E::custom(format!("failed to parse exclude-newer value: {e}")))
}
fn visit_bool<E>(self, v: bool) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
if v {
Err(E::custom(
"expected false to disable exclude-newer, got true",
))
} else {
Ok(PackageExcludeNewer::Disabled)
}
}
fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>
where
A: serde::de::MapAccess<'de>,
{
Ok(PackageExcludeNewer::Enabled(Box::new(
ExcludeNewerValue::deserialize(MapAccessDeserializer::new(map))?,
)))
}
}
deserializer.deserialize_any(Visitor)
}
}
impl serde::Serialize for PackageExcludeNewer {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match self {
Self::Enabled(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, "add exclude newer exclusion (was `{was}`)")
}
Self::Enabled { now } => {
write!(f, "remove exclude newer exclusion (now `{now}`)")
}
Self::TimestampChanged(change) => write!(f, "{change}"),
}
} }
} }
#[derive(Debug, Clone, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct ExcludeNewerPackage(FxHashMap<PackageName, ExcludeNewerValue>); pub struct ExcludeNewerPackage(FxHashMap<PackageName, PackageExcludeNewer>);
impl Deref for ExcludeNewerPackage { impl Deref for ExcludeNewerPackage {
type Target = FxHashMap<PackageName, ExcludeNewerValue>; type Target = FxHashMap<PackageName, PackageExcludeNewer>;
fn deref(&self) -> &Self::Target { fn deref(&self) -> &Self::Target {
&self.0 &self.0
@ -482,15 +612,15 @@ impl FromIterator<ExcludeNewerPackageEntry> for ExcludeNewerPackage {
fn from_iter<T: IntoIterator<Item = ExcludeNewerPackageEntry>>(iter: T) -> Self { fn from_iter<T: IntoIterator<Item = ExcludeNewerPackageEntry>>(iter: T) -> Self {
Self( Self(
iter.into_iter() iter.into_iter()
.map(|entry| (entry.package, entry.timestamp)) .map(|entry| (entry.package, entry.setting))
.collect(), .collect(),
) )
} }
} }
impl IntoIterator for ExcludeNewerPackage { impl IntoIterator for ExcludeNewerPackage {
type Item = (PackageName, ExcludeNewerValue); type Item = (PackageName, PackageExcludeNewer);
type IntoIter = std::collections::hash_map::IntoIter<PackageName, ExcludeNewerValue>; type IntoIter = std::collections::hash_map::IntoIter<PackageName, PackageExcludeNewer>;
fn into_iter(self) -> Self::IntoIter { fn into_iter(self) -> Self::IntoIter {
self.0.into_iter() self.0.into_iter()
@ -498,8 +628,8 @@ impl IntoIterator for ExcludeNewerPackage {
} }
impl<'a> IntoIterator for &'a ExcludeNewerPackage { impl<'a> IntoIterator for &'a ExcludeNewerPackage {
type Item = (&'a PackageName, &'a ExcludeNewerValue); type Item = (&'a PackageName, &'a PackageExcludeNewer);
type IntoIter = std::collections::hash_map::Iter<'a, PackageName, ExcludeNewerValue>; type IntoIter = std::collections::hash_map::Iter<'a, PackageName, PackageExcludeNewer>;
fn into_iter(self) -> Self::IntoIter { fn into_iter(self) -> Self::IntoIter {
self.0.iter() self.0.iter()
@ -508,22 +638,57 @@ impl<'a> IntoIterator for &'a ExcludeNewerPackage {
impl ExcludeNewerPackage { impl ExcludeNewerPackage {
/// Convert to the inner `HashMap`. /// Convert to the inner `HashMap`.
pub fn into_inner(self) -> FxHashMap<PackageName, ExcludeNewerValue> { pub fn into_inner(self) -> FxHashMap<PackageName, PackageExcludeNewer> {
self.0 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<ExcludeNewerPackageChange> { pub fn compare(&self, other: &Self) -> Option<ExcludeNewerPackageChange> {
for (package, timestamp) in self { for (package, setting) in self {
let Some(other_timestamp) = other.get(package) else { match (setting, other.get(package)) {
return Some(ExcludeNewerPackageChange::PackageRemoved(package.clone())); (
}; PackageExcludeNewer::Enabled(self_timestamp),
if let Some(change) = timestamp.compare(other_timestamp) { Some(PackageExcludeNewer::Enabled(other_timestamp)),
) => {
if let Some(change) = self_timestamp.compare(other_timestamp) {
return Some(ExcludeNewerPackageChange::PackageChanged( return Some(ExcludeNewerPackageChange::PackageChanged(
package.clone(), package.clone(),
change, Box::new(PackageExcludeNewerChange::TimestampChanged(change)),
)); ));
} }
} }
(
PackageExcludeNewer::Enabled(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::Enabled(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()));
}
}
}
for (package, value) in other { for (package, value) in other {
if !self.contains_key(package) { if !self.contains_key(package) {
@ -574,12 +739,16 @@ impl ExcludeNewer {
Self { global, package } Self { global, package }
} }
/// Returns the timestamp for a specific package, falling back to the global timestamp if set. /// 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<ExcludeNewerValue> { pub fn exclude_newer_package(&self, package_name: &PackageName) -> Option<ExcludeNewerValue> {
self.package match self.package.get(package_name) {
.get(package_name) Some(PackageExcludeNewer::Enabled(timestamp)) => Some(timestamp.as_ref().clone()),
.cloned() Some(PackageExcludeNewer::Disabled) => None,
.or(self.global.clone()) None => self.global.clone(),
}
} }
/// Returns true if this has any configuration (global or per-package). /// 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; let mut first = true;
for (name, timestamp) in &self.package { for (name, setting) in &self.package {
if !first { if !first {
write!(f, ", ")?; write!(f, ", ")?;
} }
write!(f, "{name}: {timestamp}")?; match setting {
PackageExcludeNewer::Enabled(timestamp) => {
write!(f, "{name}: {}", timestamp.as_ref())?;
}
PackageExcludeNewer::Disabled => {
write!(f, "{name}: disabled")?;
}
}
first = false; first = false;
} }
Ok(()) Ok(())

View File

@ -2,7 +2,8 @@ pub use dependency_mode::DependencyMode;
pub use error::{ErrorTree, NoSolutionError, NoSolutionHeader, ResolveError, SentinelRange}; pub use error::{ErrorTree, NoSolutionError, NoSolutionHeader, ResolveError, SentinelRange};
pub use exclude_newer::{ pub use exclude_newer::{
ExcludeNewer, ExcludeNewerChange, ExcludeNewerPackage, ExcludeNewerPackageChange, ExcludeNewer, ExcludeNewerChange, ExcludeNewerPackage, ExcludeNewerPackageChange,
ExcludeNewerPackageEntry, ExcludeNewerValue, ExcludeNewerValueChange, ExcludeNewerPackageEntry, ExcludeNewerValue, ExcludeNewerValueChange, PackageExcludeNewer,
PackageExcludeNewerChange,
}; };
pub use exclusions::Exclusions; pub use exclusions::Exclusions;
pub use flat_index::{FlatDistributions, FlatIndex}; pub use flat_index::{FlatDistributions, FlatIndex};

View File

@ -61,7 +61,7 @@ use crate::resolution::{AnnotatedDist, ResolutionGraphNode};
use crate::universal_marker::{ConflictMarker, UniversalMarker}; use crate::universal_marker::{ConflictMarker, UniversalMarker};
use crate::{ use crate::{
ExcludeNewer, ExcludeNewerPackage, ExcludeNewerValue, InMemoryIndex, MetadataResponse, ExcludeNewer, ExcludeNewerPackage, ExcludeNewerValue, InMemoryIndex, MetadataResponse,
PrereleaseMode, ResolutionMode, ResolverOutput, PackageExcludeNewer, PrereleaseMode, ResolutionMode, ResolverOutput,
}; };
mod export; mod export;
@ -1071,7 +1071,9 @@ impl Lock {
// Serialize package-specific exclusions as a separate field // Serialize package-specific exclusions as a separate field
if !exclude_newer.package.is_empty() { if !exclude_newer.package.is_empty() {
let mut package_table = toml_edit::Table::new(); let mut package_table = toml_edit::Table::new();
for (name, exclude_newer_value) in &exclude_newer.package { for (name, setting) in &exclude_newer.package {
match setting {
PackageExcludeNewer::Enabled(exclude_newer_value) => {
if let Some(span) = exclude_newer_value.span() { if let Some(span) = exclude_newer_value.span() {
// Serialize as inline table with timestamp and span // Serialize as inline table with timestamp and span
let mut inline = toml_edit::InlineTable::new(); let mut inline = toml_edit::InlineTable::new();
@ -1083,8 +1085,15 @@ impl Lock {
package_table.insert(name.as_ref(), Item::Value(inline.into())); package_table.insert(name.as_ref(), Item::Value(inline.into()));
} else { } else {
// Serialize as simple string // Serialize as simple string
package_table package_table.insert(
.insert(name.as_ref(), value(exclude_newer_value.to_string())); 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)); options_table.insert("exclude-newer-package", Item::Table(package_table));

View File

@ -228,9 +228,11 @@ impl Combine for ExcludeNewer {
if self.package.is_empty() { if self.package.is_empty() {
self.package = other.package; self.package = other.package;
} else { } else {
// Merge package-specific timestamps, with self taking precedence // Merge package-specific settings, with self taking precedence
for (pkg, timestamp) in &other.package { for (pkg, setting) in &other.package {
self.package.entry(pkg.clone()).or_insert(timestamp.clone()); self.package
.entry(pkg.clone())
.or_insert_with(|| setting.clone());
} }
} }
} }

View File

@ -31179,6 +31179,94 @@ fn test_tilde_equals_python_version() -> Result<()> {
Ok(()) 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 = ["idna", "iniconfig"]
"#,
)?;
// 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("idna=false"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 3 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]
idna = false
[[package]]
name = "idna"
version = "3.11"
source = { registry = "https://pypi.org/simple" }
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/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]]
name = "project"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "idna" },
{ name = "iniconfig" },
]
[package.metadata]
requires-dist = [
{ name = "idna" },
{ name = "iniconfig" },
]
"#
);
});
Ok(())
}
/// Test that exclude-newer-package is properly serialized in the lockfile. /// Test that exclude-newer-package is properly serialized in the lockfile.
#[test] #[test]
fn lock_exclude_newer_package() -> Result<()> { fn lock_exclude_newer_package() -> Result<()> {

View File

@ -3565,7 +3565,7 @@ fn compile_exclude_newer_package_errors() -> Result<()> {
----- stdout ----- ----- stdout -----
----- stderr ----- ----- stderr -----
error: invalid value 'tqdm' for '--exclude-newer-package <EXCLUDE_NEWER_PACKAGE>': Invalid `exclude-newer-package` value `tqdm`: expected format `PACKAGE=DATE` error: invalid value 'tqdm' for '--exclude-newer-package <EXCLUDE_NEWER_PACKAGE>': Invalid `exclude-newer-package` value `tqdm`: expected format `PACKAGE=DATE` or `PACKAGE=false`
For more information, try '--help'. For more information, try '--help'.
" "
@ -3583,7 +3583,7 @@ fn compile_exclude_newer_package_errors() -> Result<()> {
----- stdout ----- ----- stdout -----
----- stderr ----- ----- stderr -----
error: invalid value 'tqdm=invalid-date' for '--exclude-newer-package <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 <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'. For more information, try '--help'.
" "

View File

@ -658,8 +658,8 @@ configured time zone.
The package index must support the `upload-time` field as specified in 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 [`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 distribution, the distribution will be treated as unavailable unless the package is opted out
all packages. via `--exclude-newer-package <package>=false`. PyPI provides `upload-time` for all packages.
To ensure reproducibility, messages for unsatisfiable resolutions will not mention that 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 distributions were excluded due to the `--exclude-newer` flag — newer distributions will be treated
@ -689,6 +689,9 @@ Values may also be specified for specific packages, e.g.,
exclude-newer-package = { setuptools = "2006-12-02T02:07:43Z" } exclude-newer-package = { setuptools = "2006-12-02T02:07:43Z" }
``` ```
The same flag also accepts `<package>=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. Package-specific values will take precedence over global values.
## Dependency cooldowns ## Dependency cooldowns

25
uv.schema.json generated
View File

@ -899,7 +899,7 @@
"ExcludeNewerPackage": { "ExcludeNewerPackage": {
"type": "object", "type": "object",
"additionalProperties": { "additionalProperties": {
"$ref": "#/definitions/ExcludeNewerTimestamp" "$ref": "#/definitions/PackageExcludeNewer"
} }
}, },
"ExcludeNewerTimestamp": { "ExcludeNewerTimestamp": {
@ -1213,6 +1213,29 @@
"$ref": "#/definitions/ConfigSettings" "$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 cutoff (absolute or relative) should be used.",
"oneOf": [
{
"description": "Disable exclude-newer for this package (allow all versions regardless of upload date).",
"type": "string",
"const": "Disabled"
},
{
"description": "Enable exclude-newer with this cutoff for this package.",
"type": "object",
"properties": {
"Enabled": {
"$ref": "#/definitions/ExcludeNewerTimestamp"
}
},
"additionalProperties": false,
"required": [
"Enabled"
]
}
]
},
"PackageName": { "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: <https://packaging.python.org/en/latest/specifications/name-normalization/>", "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: <https://packaging.python.org/en/latest/specifications/name-normalization/>",
"type": "string" "type": "string"