diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index 6096a0babe411..c5afff6021a87 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -28,7 +28,7 @@ use uv_pypi_types::VerbatimParsedUrl; use uv_python::{PythonDownloads, PythonPreference, PythonVersion}; use uv_redacted::DisplaySafeUrl; use uv_resolver::{ - AnnotationStyle, ExcludeNewerPackageEntry, ExcludeNewerTimestamp, ForkStrategy, PrereleaseMode, + AnnotationStyle, ExcludeNewerPackageEntry, ExcludeNewerValue, ForkStrategy, PrereleaseMode, ResolutionMode, }; use uv_settings::PythonInstallMirrors; @@ -3041,15 +3041,27 @@ pub struct VenvArgs { /// Limit candidate packages to those that were uploaded prior to the given date. /// - /// Accepts both RFC 3339 timestamps (e.g., `2006-12-02T02:07:43Z`) and local dates in the same - /// format (e.g., `2006-12-02`) in your system's configured time zone. + /// Accepts RFC 3339 timestamps (e.g., `2006-12-02T02:07:43Z`), local dates in the same format + /// (e.g., `2006-12-02`) which use your system's configured time zone, and relative durations + /// (e.g., `24 hours`, `1 week`, `30 days`). + /// + /// Relative durations do not respect semantics of the local time zone and are always resolved + /// to a fixed number of seconds assuming that a day is 24 hours (i.e., DST transitions are + /// ignored). Calendar units such as months and years are not allowed. #[arg(long, env = EnvVars::UV_EXCLUDE_NEWER)] - pub exclude_newer: Option, + pub exclude_newer: Option, - /// Limit candidate packages for a specific package to those that were uploaded prior to the given date. + /// Limit candidate packages for a specific package to those that were uploaded prior to the + /// given date. + /// + /// Accepts package-date pairs in the format `PACKAGE=DATE`, where `DATE` is an RFC 3339 + /// timestamp (e.g., `2006-12-02T02:07:43Z`), a local date in the same format (e.g., + /// `2006-12-02`) which use your system's configured time zone, or relative duration (e.g., `24 + /// hours`, `1 week`, `30 days`). /// - /// Accepts package-date pairs in the format `PACKAGE=DATE`, where `DATE` is an RFC 3339 timestamp - /// (e.g., `2006-12-02T02:07:43Z`) or local date (e.g., `2006-12-02`) in your system's configured time zone. + /// Relative durations do not respect semantics of the local time zone and are always resolved + /// to a fixed number of seconds assuming that a day is 24 hours (i.e., DST transitions are + /// ignored). Calendar units such as months and years are not allowed. /// /// Can be provided multiple times for different packages. #[arg(long)] @@ -5517,15 +5529,27 @@ pub struct ToolUpgradeArgs { /// Limit candidate packages to those that were uploaded prior to the given date. /// - /// Accepts both RFC 3339 timestamps (e.g., `2006-12-02T02:07:43Z`) and local dates in the same - /// format (e.g., `2006-12-02`) in your system's configured time zone. + /// Accepts RFC 3339 timestamps (e.g., `2006-12-02T02:07:43Z`), local dates in the same format + /// (e.g., `2006-12-02`) which use your system's configured time zone, and relative durations + /// (e.g., `24 hours`, `1 week`, `30 days`). + /// + /// Relative durations do not respect semantics of the local time zone and are always resolved + /// to a fixed number of seconds assuming that a day is 24 hours (i.e., DST transitions are + /// ignored). Calendar units such as months and years are not allowed. #[arg(long, env = EnvVars::UV_EXCLUDE_NEWER, help_heading = "Resolver options")] - pub exclude_newer: Option, + pub exclude_newer: Option, - /// Limit candidate packages for specific packages to those that were uploaded prior to the given date. + /// Limit candidate packages for specific packages to those that were uploaded prior to the + /// given date. /// - /// Accepts package-date pairs in the format `PACKAGE=DATE`, where `DATE` is an RFC 3339 timestamp - /// (e.g., `2006-12-02T02:07:43Z`) or local date (e.g., `2006-12-02`) in your system's configured time zone. + /// Accepts package-date pairs in the format `PACKAGE=DATE`, where `DATE` is an RFC 3339 + /// timestamp (e.g., `2006-12-02T02:07:43Z`), a local date in the same format (e.g., + /// `2006-12-02`) which use your system's configured time zone, or relative duration (e.g., `24 + /// hours`, `1 week`, `30 days`). + /// + /// Relative durations do not respect semantics of the local time zone and are always resolved + /// to a fixed number of seconds assuming that a day is 24 hours (i.e., DST transitions are + /// ignored). Calendar units such as months and years are not allowed. /// /// Can be provided multiple times for different packages. #[arg(long, help_heading = "Resolver options")] @@ -6451,15 +6475,27 @@ pub struct InstallerArgs { /// Limit candidate packages to those that were uploaded prior to the given date. /// - /// Accepts both RFC 3339 timestamps (e.g., `2006-12-02T02:07:43Z`) and local dates in the same - /// format (e.g., `2006-12-02`) in your system's configured time zone. + /// Accepts RFC 3339 timestamps (e.g., `2006-12-02T02:07:43Z`), local dates in the same format + /// (e.g., `2006-12-02`) which use your system's configured time zone, and relative durations + /// (e.g., `24 hours`, `1 week`, `30 days`). + /// + /// Relative durations do not respect semantics of the local time zone and are always resolved + /// to a fixed number of seconds assuming that a day is 24 hours (i.e., DST transitions are + /// ignored). Calendar units such as months and years are not allowed. #[arg(long, env = EnvVars::UV_EXCLUDE_NEWER, help_heading = "Resolver options")] - pub exclude_newer: Option, + pub exclude_newer: Option, - /// Limit candidate packages for specific packages to those that were uploaded prior to the given date. + /// Limit candidate packages for specific packages to those that were uploaded prior to the + /// given date. + /// + /// Accepts package-date pairs in the format `PACKAGE=DATE`, where `DATE` is an RFC 3339 + /// timestamp (e.g., `2006-12-02T02:07:43Z`), a local date in the same format (e.g., + /// `2006-12-02`) which use your system's configured time zone, or relative duration (e.g., `24 + /// hours`, `1 week`, `30 days`). /// - /// Accepts package-date pairs in the format `PACKAGE=DATE`, where `DATE` is an RFC 3339 timestamp - /// (e.g., `2006-12-02T02:07:43Z`) or local date (e.g., `2006-12-02`) in your system's configured time zone. + /// Relative durations do not respect semantics of the local time zone and are always resolved + /// to a fixed number of seconds assuming that a day is 24 hours (i.e., DST transitions are + /// ignored). Calendar units such as months and years are not allowed. /// /// Can be provided multiple times for different packages. #[arg(long, help_heading = "Resolver options")] @@ -6671,15 +6707,27 @@ pub struct ResolverArgs { /// Limit candidate packages to those that were uploaded prior to the given date. /// - /// Accepts both RFC 3339 timestamps (e.g., `2006-12-02T02:07:43Z`) and local dates in the same - /// format (e.g., `2006-12-02`) in your system's configured time zone. + /// Accepts RFC 3339 timestamps (e.g., `2006-12-02T02:07:43Z`), local dates in the same format + /// (e.g., `2006-12-02`) which use your system's configured time zone, and relative durations + /// (e.g., `24 hours`, `1 week`, `30 days`). + /// + /// Relative durations do not respect semantics of the local time zone and are always resolved + /// to a fixed number of seconds assuming that a day is 24 hours (i.e., DST transitions are + /// ignored). Calendar units such as months and years are not allowed. #[arg(long, env = EnvVars::UV_EXCLUDE_NEWER, help_heading = "Resolver options")] - pub exclude_newer: Option, + pub exclude_newer: Option, - /// Limit candidate packages for a specific package to those that were uploaded prior to the given date. + /// Limit candidate packages for specific packages to those that were uploaded prior to the + /// given date. + /// + /// Accepts package-date pairs in the format `PACKAGE=DATE`, where `DATE` is an RFC 3339 + /// timestamp (e.g., `2006-12-02T02:07:43Z`), a local date in the same format (e.g., + /// `2006-12-02`) which use your system's configured time zone, or relative duration (e.g., `24 + /// hours`, `1 week`, `30 days`). /// - /// Accepts package-date pairs in the format `PACKAGE=DATE`, where `DATE` is an RFC 3339 timestamp - /// (e.g., `2006-12-02T02:07:43Z`) or local date (e.g., `2006-12-02`) in your system's configured time zone. + /// Relative durations do not respect semantics of the local time zone and are always resolved + /// to a fixed number of seconds assuming that a day is 24 hours (i.e., DST transitions are + /// ignored). Calendar units such as months and years are not allowed. /// /// Can be provided multiple times for different packages. #[arg(long, help_heading = "Resolver options")] @@ -6887,15 +6935,27 @@ pub struct ResolverInstallerArgs { /// Limit candidate packages to those that were uploaded prior to the given date. /// - /// Accepts both RFC 3339 timestamps (e.g., `2006-12-02T02:07:43Z`) and local dates in the same - /// format (e.g., `2006-12-02`) in your system's configured time zone. + /// Accepts RFC 3339 timestamps (e.g., `2006-12-02T02:07:43Z`), local dates in the same format + /// (e.g., `2006-12-02`) which use your system's configured time zone, and relative durations + /// (e.g., `24 hours`, `1 week`, `30 days`). + /// + /// Relative durations do not respect semantics of the local time zone and are always resolved + /// to a fixed number of seconds assuming that a day is 24 hours (i.e., DST transitions are + /// ignored). Calendar units such as months and years are not allowed. #[arg(long, env = EnvVars::UV_EXCLUDE_NEWER, help_heading = "Resolver options")] - pub exclude_newer: Option, + pub exclude_newer: Option, - /// Limit candidate packages for specific packages to those that were uploaded prior to the given date. + /// Limit candidate packages for specific packages to those that were uploaded prior to the + /// given date. /// - /// Accepts package-date pairs in the format `PACKAGE=DATE`, where `DATE` is an RFC 3339 timestamp - /// (e.g., `2006-12-02T02:07:43Z`) or local date (e.g., `2006-12-02`) in your system's configured time zone. + /// Accepts package-date pairs in the format `PACKAGE=DATE`, where `DATE` is an RFC 3339 + /// timestamp (e.g., `2006-12-02T02:07:43Z`), a local date in the same format (e.g., + /// `2006-12-02`) which use your system's configured time zone, or relative duration (e.g., `24 + /// hours`, `1 week`, `30 days`). + /// + /// Relative durations do not respect semantics of the local time zone and are always resolved + /// to a fixed number of seconds assuming that a day is 24 hours (i.e., DST transitions are + /// ignored). Calendar units such as months and years are not allowed. /// /// Can be provided multiple times for different packages. #[arg(long, help_heading = "Resolver options")] @@ -6995,10 +7055,15 @@ pub struct FetchArgs { /// Limit candidate packages to those that were uploaded prior to the given date. /// - /// Accepts both RFC 3339 timestamps (e.g., `2006-12-02T02:07:43Z`) and local dates in the same - /// format (e.g., `2006-12-02`) in your system's configured time zone. + /// Accepts RFC 3339 timestamps (e.g., `2006-12-02T02:07:43Z`), local dates in the same format + /// (e.g., `2006-12-02`) which use your system's configured time zone, and relative durations + /// (e.g., `24 hours`, `1 week`, `30 days`). + /// + /// Relative durations do not respect semantics of the local time zone and are always resolved + /// to a fixed number of seconds assuming that a day is 24 hours (i.e., DST transitions are + /// ignored). Calendar units such as months and years are not allowed. #[arg(long, env = EnvVars::UV_EXCLUDE_NEWER, help_heading = "Resolver options")] - pub exclude_newer: Option, + pub exclude_newer: Option, } #[derive(Args)] diff --git a/crates/uv-resolver/src/exclude_newer.rs b/crates/uv-resolver/src/exclude_newer.rs index 7f4166f98a238..4e3b33d7cce80 100644 --- a/crates/uv-resolver/src/exclude_newer.rs +++ b/crates/uv-resolver/src/exclude_newer.rs @@ -5,70 +5,280 @@ use std::{ str::FromStr, }; -use jiff::{Timestamp, ToSpan, tz::TimeZone}; +use jiff::{Span, Timestamp, ToSpan, Unit, tz::TimeZone}; use rustc_hash::FxHashMap; use uv_normalize::PackageName; +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ExcludeNewerValueChange { + SpanChanged(ExcludeNewerSpan, ExcludeNewerSpan), + SpanAdded(ExcludeNewerSpan), + SpanRemoved, + TimestampChanged(Timestamp, Timestamp), +} + +impl std::fmt::Display for ExcludeNewerValueChange { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::SpanChanged(old, new) => { + write!(f, "change of exclude newer span from `{old}` to `{new}`") + } + Self::SpanAdded(span) => { + write!(f, "addition of exclude newer span `{span}`") + } + Self::SpanRemoved => { + write!(f, "removal of exclude newer span") + } + Self::TimestampChanged(old, new) => { + write!( + f, + "change of exclude newer timestamp from `{old}` to `{new}`" + ) + } + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ExcludeNewerChange { + GlobalChanged(ExcludeNewerValueChange), + GlobalAdded(ExcludeNewerValue), + GlobalRemoved, + Package(ExcludeNewerPackageChange), +} + +impl std::fmt::Display for ExcludeNewerChange { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::GlobalChanged(change) => { + write!(f, "{change}") + } + Self::GlobalAdded(value) => { + write!(f, "addition of global exclude newer {value}") + } + Self::GlobalRemoved => write!(f, "removal of global exclude newer"), + Self::Package(change) => { + write!(f, "{change}") + } + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ExcludeNewerPackageChange { + PackageAdded(PackageName, ExcludeNewerValue), + PackageRemoved(PackageName), + PackageChanged(PackageName, ExcludeNewerValueChange), +} + +impl std::fmt::Display for ExcludeNewerPackageChange { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::PackageAdded(name, value) => { + write!( + f, + "addition of exclude newer `{value}` 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}`") + } + } + } +} /// A timestamp that excludes files newer than it. -#[derive(Debug, Copy, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)] -pub struct ExcludeNewerTimestamp(Timestamp); +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ExcludeNewerValue { + /// The resolved timestamp. + timestamp: Timestamp, + /// The span used to derive the [`Timestamp`], if any. + span: Option, +} + +impl ExcludeNewerValue { + pub fn compare(&self, other: &Self) -> Option { + if self.timestamp != other.timestamp { + return Some(ExcludeNewerValueChange::TimestampChanged( + self.timestamp, + other.timestamp, + )); + } + match (&self.span, &other.span) { + (None, Some(span)) => Some(ExcludeNewerValueChange::SpanAdded(span.clone())), + (Some(_), None) => Some(ExcludeNewerValueChange::SpanRemoved), + (Some(self_span), Some(other_span)) if self_span != other_span => Some( + ExcludeNewerValueChange::SpanChanged(self_span.clone(), other_span.clone()), + ), + (Some(_), Some(_)) | (None, None) => None, + } + } +} + +#[derive(Debug, Clone)] +pub struct ExcludeNewerSpan(Span); + +impl std::fmt::Display for ExcludeNewerSpan { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +impl PartialEq for ExcludeNewerSpan { + fn eq(&self, other: &Self) -> bool { + self.0.fieldwise() == other.0.fieldwise() + } +} -impl ExcludeNewerTimestamp { - /// Returns the timestamp in milliseconds. +impl Eq for ExcludeNewerSpan {} + +impl serde::Serialize for ExcludeNewerValue { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + self.timestamp.serialize(serializer) + } +} + +impl<'de> serde::Deserialize<'de> for ExcludeNewerValue { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + Self::from_str(&s).map_err(serde::de::Error::custom) + } +} + +impl ExcludeNewerValue { + /// Return the [`Timestamp`] in milliseconds. pub fn timestamp_millis(&self) -> i64 { - self.0.as_millisecond() + self.timestamp.as_millisecond() + } + + /// Return the [`Timestamp`]. + pub fn timestamp(&self) -> Timestamp { + self.timestamp + } + + /// Return the [`ExcludeNewerSpan`] used to construct the [`Timestamp`], if any. + pub fn span(&self) -> Option<&ExcludeNewerSpan> { + self.span.as_ref() + } + + /// Create a new [`ExcludeNewerTimestamp`]. + pub fn new(timestamp: Timestamp, span: Option) -> Self { + Self { + timestamp, + span: span.map(ExcludeNewerSpan), + } } } -impl From for ExcludeNewerTimestamp { +impl From for ExcludeNewerValue { fn from(timestamp: Timestamp) -> Self { - Self(timestamp) + Self { + timestamp, + span: None, + } } } -impl FromStr for ExcludeNewerTimestamp { +impl FromStr for ExcludeNewerValue { type Err = String; /// Parse an [`ExcludeNewerTimestamp`] from a string. /// - /// Accepts both RFC 3339 timestamps (e.g., `2006-12-02T02:07:43Z`) and local dates in the same - /// format (e.g., `2006-12-02`). + /// Accepts RFC 3339 timestamps (e.g., `2006-12-02T02:07:43Z`), local dates in the same format + /// (e.g., `2006-12-02`), and relative durations (e.g., `1 week`, `30 days`). fn from_str(input: &str) -> Result { - // NOTE(burntsushi): Previously, when using Chrono, we tried - // to parse as a date first, then a timestamp, and if both - // failed, we combined both of the errors into one message. - // But in Jiff, if an RFC 3339 timestamp could be parsed, then - // it must necessarily be the case that a date can also be - // parsed. So we can collapse the error cases here. That is, - // if we fail to parse a timestamp and a date, then it should - // be sufficient to just report the error from parsing the date. - // If someone tried to write a timestamp but committed an error - // in the non-date portion, the date parsing below will still - // report a holistic error that will make sense to the user. - // (I added a snapshot test for that case.) + // Try parsing as a timestamp first if let Ok(timestamp) = input.parse::() { - return Ok(Self(timestamp)); + return Ok(Self::new(timestamp, None)); } - let date = input - .parse::() - .map_err(|err| format!("`{input}` could not be parsed as a valid date: {err}"))?; - let timestamp = date - .checked_add(1.day()) - .and_then(|date| date.to_zoned(TimeZone::system())) - .map(|zdt| zdt.timestamp()) - .map_err(|err| { - format!( - "`{input}` parsed to date `{date}`, but could not \ - be converted to a timestamp: {err}", - ) - })?; - Ok(Self(timestamp)) + + // Try parsing as a date + // In Jiff, if an RFC 3339 timestamp could be parsed, then it must necessarily be the case + // that a date can also be parsed. So we can collapse the error cases here. That is, if we + // fail to parse a timestamp and a date, then it should be sufficient to just report the + // error from parsing the date. If someone tried to write a timestamp but committed an error + // in the non-date portion, the date parsing below will still report a holistic error that + // will make sense to the user. (I added a snapshot test for that case.) + let date_err = match input.parse::() { + Ok(date) => { + let timestamp = date + .checked_add(1.day()) + .and_then(|date| date.to_zoned(TimeZone::system())) + .map(|zdt| zdt.timestamp()) + .map_err(|err| { + format!( + "`{input}` parsed to date `{date}`, but could not \ + be converted to a timestamp: {err}", + ) + })?; + return Ok(Self::new(timestamp, None)); + } + Err(err) => err, + }; + + // Try parsing as a span + let span_err = match input.parse::() { + Ok(span) => { + // Allow overriding the current time in tests for deterministic snapshots + let now = if let Ok(test_time) = std::env::var("UV_TEST_CURRENT_TIMESTAMP") { + test_time + .parse::() + .expect("UV_TEST_CURRENT_TIMESTAMP must be a valid RFC 3339 timestamp") + .to_zoned(TimeZone::UTC) + } else { + Timestamp::now().to_zoned(TimeZone::UTC) + }; + + // We do not allow years and months as units, as the amount of time they represent + // is not fixed and can differ depending on the local time zone. We could allow this + // via the CLI in the future, but shouldn't allow it via persistent configuration. + if span.get_years() != 0 { + let years = span.total((Unit::Year, &now)).map(f64::ceil).unwrap_or(1.0); + let days = years * 365.0; + return Err(format!( + "Duration `{input}` uses unit 'years' which is not allowed; use days instead, e.g., `{days:.0} days`.", + )); + } + if span.get_months() != 0 { + let months = span + .total((Unit::Month, &now)) + .map(f64::ceil) + .unwrap_or(1.0); + let days = months * 30.0; + return Err(format!( + "Duration `{input}` uses 'months' which is not allowed; use days instead, e.g., `{days:.0} days`." + )); + } + + // We're using a UTC timezone so there are no transitions (e.g., DST) and days are + // always 24 hours. This means that we can also allow weeks as a unit. + let cutoff = now.checked_sub(span).map_err(|err| { + format!("Duration `{input}` is too large to subtract from current time: {err}") + })?; + + return Ok(Self::new(cutoff.into(), Some(span))); + } + Err(err) => err, + }; + + // Return a comprehensive error message + Err(format!( + "`{input}` could not be parsed as a timestamp, date, or relative duration: {date_err} and {span_err}" + )) } } -impl std::fmt::Display for ExcludeNewerTimestamp { +impl std::fmt::Display for ExcludeNewerValue { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.0.fmt(f) + self.timestamp.fmt(f) } } @@ -77,7 +287,7 @@ impl std::fmt::Display for ExcludeNewerTimestamp { #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub struct ExcludeNewerPackageEntry { pub package: PackageName, - pub timestamp: ExcludeNewerTimestamp, + pub timestamp: ExcludeNewerValue, } impl FromStr for ExcludeNewerPackageEntry { @@ -94,25 +304,25 @@ impl FromStr for ExcludeNewerPackageEntry { let package = PackageName::from_str(package).map_err(|err| { format!("Invalid `exclude-newer-package` package name `{package}`: {err}") })?; - let timestamp = ExcludeNewerTimestamp::from_str(date) + let timestamp = ExcludeNewerValue::from_str(date) .map_err(|err| format!("Invalid `exclude-newer-package` timestamp `{date}`: {err}"))?; Ok(Self { package, timestamp }) } } -impl From<(PackageName, ExcludeNewerTimestamp)> for ExcludeNewerPackageEntry { - fn from((package, timestamp): (PackageName, ExcludeNewerTimestamp)) -> Self { +impl From<(PackageName, ExcludeNewerValue)> for ExcludeNewerPackageEntry { + fn from((package, timestamp): (PackageName, ExcludeNewerValue)) -> Self { Self { package, timestamp } } } #[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 @@ -136,8 +346,8 @@ impl FromIterator for ExcludeNewerPackage { } impl IntoIterator for ExcludeNewerPackage { - type Item = (PackageName, ExcludeNewerTimestamp); - type IntoIter = std::collections::hash_map::IntoIter; + type Item = (PackageName, ExcludeNewerValue); + type IntoIter = std::collections::hash_map::IntoIter; fn into_iter(self) -> Self::IntoIter { self.0.into_iter() @@ -145,8 +355,8 @@ impl IntoIterator for ExcludeNewerPackage { } impl<'a> IntoIterator for &'a ExcludeNewerPackage { - type Item = (&'a PackageName, &'a ExcludeNewerTimestamp); - type IntoIter = std::collections::hash_map::Iter<'a, PackageName, ExcludeNewerTimestamp>; + type Item = (&'a PackageName, &'a ExcludeNewerValue); + type IntoIter = std::collections::hash_map::Iter<'a, PackageName, ExcludeNewerValue>; fn into_iter(self) -> Self::IntoIter { self.0.iter() @@ -155,9 +365,38 @@ 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 } + + pub fn compare(&self, other: &Self) -> Option { + for (package, timestamp) in self { + match other.get(package) { + Some(other_timestamp) => { + if let Some(change) = timestamp.compare(other_timestamp) { + return Some(ExcludeNewerPackageChange::PackageChanged( + package.clone(), + change, + )); + } + } + None => { + return Some(ExcludeNewerPackageChange::PackageRemoved(package.clone())); + } + } + } + + for (package, value) in other { + if !self.contains_key(package) { + return Some(ExcludeNewerPackageChange::PackageAdded( + package.clone(), + value.clone(), + )); + } + } + + None + } } /// A setting that excludes files newer than a timestamp, at a global level or per-package. @@ -166,7 +405,7 @@ impl ExcludeNewerPackage { pub struct ExcludeNewer { /// Global timestamp that applies to all packages if no package-specific timestamp is set. #[serde(default, skip_serializing_if = "Option::is_none")] - pub global: Option, + pub global: Option, /// Per-package timestamps that override the global timestamp. #[serde(default, skip_serializing_if = "FxHashMap::is_empty")] pub package: ExcludeNewerPackage, @@ -174,7 +413,7 @@ pub struct ExcludeNewer { impl ExcludeNewer { /// Create a new exclude newer configuration with just a global timestamp. - pub fn global(global: ExcludeNewerTimestamp) -> Self { + pub fn global(global: ExcludeNewerValue) -> Self { Self { global: Some(global), package: ExcludeNewerPackage::default(), @@ -182,13 +421,13 @@ impl ExcludeNewer { } /// Create a new exclude newer configuration. - pub fn new(global: Option, package: ExcludeNewerPackage) -> Self { + pub fn new(global: Option, package: ExcludeNewerPackage) -> Self { Self { global, package } } /// Create from CLI arguments. pub fn from_args( - global: Option, + global: Option, package: Vec, ) -> Self { let package: ExcludeNewerPackage = package.into_iter().collect(); @@ -197,22 +436,40 @@ impl ExcludeNewer { } /// Returns the timestamp for a specific package, falling back to the global timestamp if set. - pub fn exclude_newer_package( - &self, - package_name: &PackageName, - ) -> Option { - self.package.get(package_name).copied().or(self.global) + pub fn exclude_newer_package(&self, package_name: &PackageName) -> Option { + self.package + .get(package_name) + .cloned() + .or(self.global.clone()) } /// Returns true if this has any configuration (global or per-package). pub fn is_empty(&self) -> bool { self.global.is_none() && self.package.is_empty() } + + pub fn compare(&self, other: &Self) -> Option { + match (&self.global, &other.global) { + (Some(self_global), Some(other_global)) => { + if let Some(change) = self_global.compare(other_global) { + return Some(ExcludeNewerChange::GlobalChanged(change)); + } + } + (None, Some(global)) => { + return Some(ExcludeNewerChange::GlobalAdded(global.clone())); + } + (Some(_), None) => return Some(ExcludeNewerChange::GlobalRemoved), + (None, None) => (), + } + self.package + .compare(&other.package) + .map(ExcludeNewerChange::Package) + } } impl std::fmt::Display for ExcludeNewer { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - if let Some(global) = self.global { + if let Some(global) = &self.global { write!(f, "global: {global}")?; if !self.package.is_empty() { write!(f, ", ")?; @@ -231,7 +488,7 @@ impl std::fmt::Display for ExcludeNewer { } #[cfg(feature = "schemars")] -impl schemars::JsonSchema for ExcludeNewerTimestamp { +impl schemars::JsonSchema for ExcludeNewerValue { fn schema_name() -> Cow<'static, str> { Cow::Borrowed("ExcludeNewerTimestamp") } @@ -240,7 +497,65 @@ impl schemars::JsonSchema for ExcludeNewerTimestamp { schemars::json_schema!({ "type": "string", "pattern": r"^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(Z|[+-]\d{2}:\d{2}))?$", - "description": "Exclude distributions uploaded after the given timestamp.\n\nAccepts both RFC 3339 timestamps (e.g., `2006-12-02T02:07:43Z`) and local dates in the same format (e.g., `2006-12-02`).", + "description": "Exclude distributions uploaded after the given timestamp.\n\nAccepts both RFC 3339 timestamps (e.g., `2006-12-02T02:07:43Z`) and local dates in the same format (e.g., `2006-12-02`), as well as relative durations (e.g., `1 week`, `30 days`, `6 months`). Relative durations are resolved to an absolute timestamp at lock time.", }) } } + +#[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"); + assert!(entry.timestamp.to_string().contains("2023-01-01")); + + // 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 + } +} diff --git a/crates/uv-resolver/src/lib.rs b/crates/uv-resolver/src/lib.rs index 901f8366a8ae1..d57f8f097c08c 100644 --- a/crates/uv-resolver/src/lib.rs +++ b/crates/uv-resolver/src/lib.rs @@ -1,7 +1,8 @@ pub use dependency_mode::DependencyMode; pub use error::{ErrorTree, NoSolutionError, NoSolutionHeader, ResolveError, SentinelRange}; pub use exclude_newer::{ - ExcludeNewer, ExcludeNewerPackage, ExcludeNewerPackageEntry, ExcludeNewerTimestamp, + ExcludeNewer, ExcludeNewerChange, ExcludeNewerPackage, ExcludeNewerPackageChange, + ExcludeNewerPackageEntry, ExcludeNewerValue, ExcludeNewerValueChange, }; 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 2bab3ab7ffed7..f441268bdaac2 100644 --- a/crates/uv-resolver/src/lock/mod.rs +++ b/crates/uv-resolver/src/lock/mod.rs @@ -59,7 +59,7 @@ pub use crate::lock::tree::TreeDisplay; use crate::resolution::{AnnotatedDist, ResolutionGraphNode}; use crate::universal_marker::{ConflictMarker, UniversalMarker}; use crate::{ - ExcludeNewer, ExcludeNewerPackage, ExcludeNewerTimestamp, InMemoryIndex, MetadataResponse, + ExcludeNewer, ExcludeNewerPackage, ExcludeNewerValue, InMemoryIndex, MetadataResponse, PrereleaseMode, ResolutionMode, ResolverOutput, }; @@ -1059,8 +1059,12 @@ impl Lock { let exclude_newer = ExcludeNewer::from(self.options.exclude_newer.clone()); if !exclude_newer.is_empty() { // Always serialize global exclude-newer as a string - if let Some(global) = exclude_newer.global { + if let Some(global) = &exclude_newer.global { options_table.insert("exclude-newer", value(global.to_string())); + // Serialize the original span if present + if let Some(span) = global.span() { + options_table.insert("exclude-newer-span", value(span.to_string())); + } } // Serialize package-specific exclusions as a separate field @@ -2130,7 +2134,7 @@ struct ResolverOptions { #[derive(Clone, Debug, Default, serde::Deserialize, PartialEq, Eq)] #[serde(rename_all = "kebab-case")] struct ExcludeNewerWire { - exclude_newer: Option, + exclude_newer: Option, #[serde(default, skip_serializing_if = "ExcludeNewerPackage::is_empty")] exclude_newer_package: ExcludeNewerPackage, } diff --git a/crates/uv-resolver/src/version_map.rs b/crates/uv-resolver/src/version_map.rs index 77415022d7b61..a35e47220a91d 100644 --- a/crates/uv-resolver/src/version_map.rs +++ b/crates/uv-resolver/src/version_map.rs @@ -23,7 +23,7 @@ use uv_types::HashStrategy; use uv_warnings::warn_user_once; use crate::flat_index::FlatDistributions; -use crate::{ExcludeNewer, ExcludeNewerTimestamp, yanks::AllowedYanks}; +use crate::{ExcludeNewer, ExcludeNewerValue, yanks::AllowedYanks}; /// A map from versions to distributions. #[derive(Debug)] @@ -390,7 +390,7 @@ struct VersionMapLazy { /// in the current environment. tags: Option, /// Whether files newer than this timestamp should be excluded or not. - exclude_newer: Option, + exclude_newer: Option, /// Which yanked versions are allowed allowed_yanks: AllowedYanks, /// The hashes of allowed distributions. diff --git a/crates/uv-settings/src/combine.rs b/crates/uv-settings/src/combine.rs index 00c5976551f40..15a63e1dcb30f 100644 --- a/crates/uv-settings/src/combine.rs +++ b/crates/uv-settings/src/combine.rs @@ -16,7 +16,7 @@ use uv_pypi_types::{SchemaConflicts, SupportedEnvironments}; use uv_python::{PythonDownloads, PythonPreference, PythonVersion}; use uv_redacted::DisplaySafeUrl; use uv_resolver::{ - AnnotationStyle, ExcludeNewer, ExcludeNewerPackage, ExcludeNewerTimestamp, ForkStrategy, + AnnotationStyle, ExcludeNewer, ExcludeNewerPackage, ExcludeNewerValue, ForkStrategy, PrereleaseMode, ResolutionMode, }; use uv_torch::TorchMode; @@ -85,7 +85,7 @@ macro_rules! impl_combine_or { impl_combine_or!(AddBoundsKind); impl_combine_or!(AnnotationStyle); impl_combine_or!(ExcludeNewer); -impl_combine_or!(ExcludeNewerTimestamp); +impl_combine_or!(ExcludeNewerValue); impl_combine_or!(ExportFormat); impl_combine_or!(ForkStrategy); impl_combine_or!(Index); @@ -230,7 +230,7 @@ impl Combine for ExcludeNewer { } else { // Merge package-specific timestamps, with self taking precedence for (pkg, timestamp) in &other.package { - self.package.entry(pkg.clone()).or_insert(*timestamp); + self.package.entry(pkg.clone()).or_insert(timestamp.clone()); } } } diff --git a/crates/uv-settings/src/settings.rs b/crates/uv-settings/src/settings.rs index c25d5f0edef31..7169f32d21ac7 100644 --- a/crates/uv-settings/src/settings.rs +++ b/crates/uv-settings/src/settings.rs @@ -19,7 +19,7 @@ use uv_pypi_types::{SupportedEnvironments, VerbatimParsedUrl}; use uv_python::{PythonDownloads, PythonPreference, PythonVersion}; use uv_redacted::DisplaySafeUrl; use uv_resolver::{ - AnnotationStyle, ExcludeNewer, ExcludeNewerPackage, ExcludeNewerTimestamp, ForkStrategy, + AnnotationStyle, ExcludeNewer, ExcludeNewerPackage, ExcludeNewerValue, ForkStrategy, PrereleaseMode, ResolutionMode, }; use uv_torch::TorchMode; @@ -340,7 +340,7 @@ pub struct InstallerOptions { pub index_strategy: Option, pub keyring_provider: Option, pub config_settings: Option, - pub exclude_newer: Option, + pub exclude_newer: Option, pub link_mode: Option, pub compile_bytecode: Option, pub reinstall: Option, @@ -401,7 +401,7 @@ pub struct ResolverInstallerOptions { pub build_isolation: Option, pub extra_build_dependencies: Option, pub extra_build_variables: Option, - pub exclude_newer: Option, + pub exclude_newer: Option, pub exclude_newer_package: Option, pub link_mode: Option, pub compile_bytecode: Option, @@ -815,7 +815,7 @@ pub struct ResolverInstallerSchema { exclude-newer = "2006-12-02T02:07:43Z" "# )] - pub exclude_newer: Option, + pub exclude_newer: Option, /// Limit candidate packages for specific packages to those that were uploaded prior to the given date. /// /// Accepts package-date pairs in a dictionary format. @@ -1576,7 +1576,7 @@ pub struct PipOptions { exclude-newer = "2006-12-02T02:07:43Z" "# )] - pub exclude_newer: Option, + pub exclude_newer: Option, /// Limit candidate packages for specific packages to those that were uploaded prior to the given date. /// /// Accepts package-date pairs in a dictionary format. @@ -1958,7 +1958,7 @@ pub struct ToolOptions { pub build_isolation: Option, pub extra_build_dependencies: Option, pub extra_build_variables: Option, - pub exclude_newer: Option, + pub exclude_newer: Option, pub exclude_newer_package: Option, pub link_mode: Option, pub compile_bytecode: Option, @@ -2074,7 +2074,7 @@ pub struct OptionsWire { no_build_isolation_package: Option>, extra_build_dependencies: Option, extra_build_variables: Option, - exclude_newer: Option, + exclude_newer: Option, exclude_newer_package: Option, link_mode: Option, compile_bytecode: Option, diff --git a/crates/uv-static/src/env_vars.rs b/crates/uv-static/src/env_vars.rs index 8f57d95655bec..562f17da4b62d 100644 --- a/crates/uv-static/src/env_vars.rs +++ b/crates/uv-static/src/env_vars.rs @@ -1035,6 +1035,12 @@ impl EnvVars { #[attr_added_in("0.5.29")] pub const UV_TEST_NO_CLI_PROGRESS: &'static str = "UV_TEST_NO_CLI_PROGRESS"; + /// Used to mock the current timestamp for relative `--exclude-newer` times in tests. + /// Should be set to an RFC 3339 timestamp (e.g., `2025-11-21T12:00:00Z`). + #[attr_hidden] + #[attr_added_in("0.9.8")] + pub const UV_TEST_CURRENT_TIMESTAMP: &'static str = "UV_TEST_CURRENT_TIMESTAMP"; + /// `.env` files from which to load environment variables when executing `uv run` commands. #[attr_added_in("0.4.30")] pub const UV_ENV_FILE: &'static str = "UV_ENV_FILE"; diff --git a/crates/uv/src/commands/project/lock.rs b/crates/uv/src/commands/project/lock.rs index e90b05ca3efff..43d81a879f78f 100644 --- a/crates/uv/src/commands/project/lock.rs +++ b/crates/uv/src/commands/project/lock.rs @@ -1028,40 +1028,12 @@ impl ValidatedLock { ); return Ok(Self::Unusable(lock)); } - let lock_exclude_newer = lock.exclude_newer(); - let options_exclude_newer = &options.exclude_newer; - - match ( - lock_exclude_newer.is_empty(), - options_exclude_newer.is_empty(), - ) { - (true, true) => (), - (false, false) if lock_exclude_newer == *options_exclude_newer => (), - (false, false) => { - let _ = writeln!( - printer.stderr(), - "Ignoring existing lockfile due to change in timestamp cutoff: `{}` vs. `{}`", - lock_exclude_newer.cyan(), - options_exclude_newer.cyan() - ); - return Ok(Self::Unusable(lock)); - } - (false, true) => { - let _ = writeln!( - printer.stderr(), - "Ignoring existing lockfile due to removal of timestamp cutoff: `{}`", - lock_exclude_newer.cyan(), - ); - return Ok(Self::Unusable(lock)); - } - (true, false) => { - let _ = writeln!( - printer.stderr(), - "Ignoring existing lockfile due to addition of timestamp cutoff: `{}`", - options_exclude_newer.cyan() - ); - return Ok(Self::Unusable(lock)); - } + if let Some(change) = lock.exclude_newer().compare(&options.exclude_newer) { + let _ = writeln!( + printer.stderr(), + "Ignoring existing lockfile due to {change}", + ); + return Ok(Self::Unusable(lock)); } match upgrade { diff --git a/crates/uv/tests/it/lock_exclude_newer_relative.rs b/crates/uv/tests/it/lock_exclude_newer_relative.rs new file mode 100644 index 0000000000000..f0eacb0a4fded --- /dev/null +++ b/crates/uv/tests/it/lock_exclude_newer_relative.rs @@ -0,0 +1,1267 @@ +use anyhow::Result; +use assert_cmd::assert::OutputAssertExt; +use assert_fs::fixture::{FileWriteStr, PathChild}; +use insta::assert_snapshot; +use uv_static::EnvVars; + +use crate::common::{TestContext, uv_snapshot}; + +/// Lock with a relative exclude-newer timestamp. +#[test] +fn lock_exclude_newer_relative_timestamp() -> 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 = ["iniconfig"] + "#, + )?; + + uv_snapshot!(context.filters(), context + .lock() + .env_remove(EnvVars::UV_EXCLUDE_NEWER) + .env(EnvVars::UV_TEST_CURRENT_TIMESTAMP, "2025-11-21T12:00:00Z") + .arg("--exclude-newer") + .arg("2 weeks"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 2 packages in [TIME] + "###); + + let lock = context.read("uv.lock"); + assert_snapshot!(lock, @r#" + version = 1 + revision = 3 + requires-python = ">=3.12" + + [options] + exclude-newer = "2025-11-07T12:00:00Z" + exclude-newer-span = "P2W" + + [[package]] + name = "iniconfig" + version = "2.3.0" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } + wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, + ] + + [[package]] + name = "project" + version = "0.1.0" + source = { virtual = "." } + dependencies = [ + { name = "iniconfig" }, + ] + + [package.metadata] + requires-dist = [{ name = "iniconfig" }] + "#); + + context + .lock() + .env("UV_EXCLUDE_NEWER", "2024-03-25T00:00:00Z") + .arg("--locked") + .assert() + .failure(); + + context + .lock() + .arg("--upgrade") + .env_remove(EnvVars::UV_EXCLUDE_NEWER) + .env(EnvVars::UV_TEST_CURRENT_TIMESTAMP, "2025-11-21T12:00:00Z") + .arg("--exclude-newer") + .arg("1 week") + .assert() + .success(); + + Ok(()) +} + +/// Lock with various relative exclude-newer formats. +#[test] +fn lock_exclude_newer_relative_formats() -> 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 = ["iniconfig"] + "#, + )?; + + uv_snapshot!(context.filters(), context + .lock() + .env_remove(EnvVars::UV_EXCLUDE_NEWER) + .arg("--exclude-newer") + .arg("1 day"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 2 packages in [TIME] + "###); + + let _ = fs_err::remove_file(context.temp_dir.child("uv.lock")); + uv_snapshot!(context.filters(), context + .lock() + .env_remove(EnvVars::UV_EXCLUDE_NEWER) + .arg("--exclude-newer") + .arg("30days"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 2 packages in [TIME] + "###); + + let _ = fs_err::remove_file(context.temp_dir.child("uv.lock")); + uv_snapshot!(context.filters(), context + .lock() + .env_remove(EnvVars::UV_EXCLUDE_NEWER) + .env(EnvVars::UV_TEST_CURRENT_TIMESTAMP, "2025-11-21T12:00:00Z") + .arg("--exclude-newer") + .arg("3 months"), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: invalid value '3 months' for '--exclude-newer ': Duration `3 months` uses 'months' which is not allowed; use days instead, e.g., `90 days`. + + For more information, try '--help'. + "); + + Ok(()) +} + +/// Error on invalid relative timestamps. +#[test] +fn lock_exclude_newer_invalid_relative() -> 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 = ["iniconfig"] + "#, + )?; + + uv_snapshot!(context.filters(), context + .lock() + .env_remove(EnvVars::UV_EXCLUDE_NEWER) + .arg("--exclude-newer") + .arg("invalid span"), @r#" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: invalid value 'invalid span' for '--exclude-newer ': `invalid span` could not be parsed as a timestamp, date, or relative duration: failed to parse year in date "invalid span": failed to parse "inva" as year (a four digit integer): invalid digit, expected 0-9 but got i and failed to parse "invalid span" in the "friendly" format: parsing a friendly duration requires it to start with a unit value (a decimal integer) after an optional sign, but no integer was found + + For more information, try '--help'. + "#); + + Ok(()) +} + +/// Lock with package-specific relative exclude-newer should reject months/years. +#[test] +fn lock_exclude_newer_package_relative() -> 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"] + "#, + )?; + + // Test that months are rejected in global exclude-newer + uv_snapshot!(context.filters(), context + .lock() + .env_remove(EnvVars::UV_EXCLUDE_NEWER) + .env(EnvVars::UV_TEST_CURRENT_TIMESTAMP, "2025-11-21T12:00:00Z") + .arg("--exclude-newer") + .arg("6 months"), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: invalid value '6 months' for '--exclude-newer ': Duration `6 months` uses 'months' which is not allowed; use days instead, e.g., `180 days`. + + For more information, try '--help'. + "); + + // Test that years are rejected in package-specific exclude-newer + uv_snapshot!(context.filters(), context + .lock() + .env_remove(EnvVars::UV_EXCLUDE_NEWER) + .env(EnvVars::UV_TEST_CURRENT_TIMESTAMP, "2025-11-21T12:00:00Z") + .arg("--exclude-newer") + .arg("2 weeks") + .arg("--exclude-newer-package") + .arg("tqdm=1 year"), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: invalid value 'tqdm=1 year' for '--exclude-newer-package ': Invalid `exclude-newer-package` timestamp `1 year`: Duration `1 year` uses unit 'years' which is not allowed; use days instead, e.g., `365 days`. + + For more information, try '--help'. + "); + + Ok(()) +} + +/// Error messages for invalid exclude-newer values. +#[test] +fn lock_exclude_newer_error_messages() -> 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 = [] + "#, + )?; + + uv_snapshot!(context.filters(), context + .lock() + .arg("--exclude-newer") + .arg("invalid span"), @r#" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: invalid value 'invalid span' for '--exclude-newer ': `invalid span` could not be parsed as a timestamp, date, or relative duration: failed to parse year in date "invalid span": failed to parse "inva" as year (a four digit integer): invalid digit, expected 0-9 but got i and failed to parse "invalid span" in the "friendly" format: parsing a friendly duration requires it to start with a unit value (a decimal integer) after an optional sign, but no integer was found + + For more information, try '--help'. + "#); + + uv_snapshot!(context.filters(), context + .lock() + .arg("--exclude-newer") + .arg("2006-12-02T02:07:43"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + "###); + + uv_snapshot!(context.filters(), context + .lock() + .arg("--exclude-newer") + .arg("12/02/2006"), @r#" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: invalid value '12/02/2006' for '--exclude-newer ': `12/02/2006` could not be parsed as a timestamp, date, or relative duration: failed to parse year in date "12/02/2006": failed to parse "12/0" as year (a four digit integer): invalid digit, expected 0-9 but got / and failed to parse "12/02/2006" in the "friendly" format: expected to find unit designator suffix (e.g., 'years' or 'secs'), but found input beginning with "/02/2006" instead + + For more information, try '--help'. + "#); + + uv_snapshot!(context.filters(), context + .lock() + .arg("--exclude-newer") + .arg("2 weak"), @r#" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: invalid value '2 weak' for '--exclude-newer ': `2 weak` could not be parsed as a timestamp, date, or relative duration: failed to parse year in date "2 weak": failed to parse "2 we" as year (a four digit integer): invalid digit, expected 0-9 but got and failed to parse "2 weak" in the "friendly" format: parsed value 'P2W', but unparsed input "eak" remains (expected no unparsed input) + + For more information, try '--help'. + "#); + + uv_snapshot!(context.filters(), context + .lock() + .arg("--exclude-newer") + .arg("30"), @r#" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: invalid value '30' for '--exclude-newer ': `30` could not be parsed as a timestamp, date, or relative duration: failed to parse year in date "30": expected four digit year (or leading sign for six digit year), but found end of input and failed to parse "30" in the "friendly" format: expected to find unit designator suffix (e.g., 'years' or 'secs'), but found end of input + + For more information, try '--help'. + "#); + + uv_snapshot!(context.filters(), context + .lock() + .arg("--exclude-newer") + .arg("1000000 years"), @r#" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: invalid value '1000000 years' for '--exclude-newer ': `1000000 years` could not be parsed as a timestamp, date, or relative duration: failed to parse month in date "1000000 years": month is not valid: parameter 'month' with value 0 is not in the required range of 1..=12 and failed to parse "1000000 years" in the "friendly" format: failed to set value 1000000 as year unit on span: parameter 'years' with value 1000000 is not in the required range of -19998..=19998 + + For more information, try '--help'. + "#); + + Ok(()) +} + +/// Lock with relative exclude-newer in pyproject.toml. +#[test] +fn lock_exclude_newer_relative_pyproject() -> 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 = ["iniconfig"] + + [tool.uv] + exclude-newer = "2 weeks" + "#, + )?; + + uv_snapshot!(context.filters(), context.lock().env_remove("UV_EXCLUDE_NEWER").env(EnvVars::UV_TEST_CURRENT_TIMESTAMP, "2025-11-21T12:00:00Z").env(EnvVars::UV_TEST_CURRENT_TIMESTAMP, "2025-11-21T12:00:00Z"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 2 packages in [TIME] + "###); + + let lock = context.read("uv.lock"); + assert_snapshot!(lock, @r#" + version = 1 + revision = 3 + requires-python = ">=3.12" + + [options] + exclude-newer = "2025-11-07T12:00:00Z" + exclude-newer-span = "P2W" + + [[package]] + name = "iniconfig" + version = "2.3.0" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } + wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, + ] + + [[package]] + name = "project" + version = "0.1.0" + source = { virtual = "." } + dependencies = [ + { name = "iniconfig" }, + ] + + [package.metadata] + requires-dist = [{ name = "iniconfig" }] + "#); + uv_snapshot!(context.filters(), context.lock().env_remove("UV_EXCLUDE_NEWER").env(EnvVars::UV_TEST_CURRENT_TIMESTAMP, "2025-11-21T12:00:00Z").env(EnvVars::UV_TEST_CURRENT_TIMESTAMP, "2025-11-21T12:00:00Z"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Ignoring existing lockfile due to addition of exclude newer span `P2W` + Resolved 2 packages in [TIME] + "); + + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["iniconfig"] + + [tool.uv] + exclude-newer = "2024-01-01T00:00:00Z" + "#, + )?; + + uv_snapshot!(context.filters(), context.lock().env_remove("UV_EXCLUDE_NEWER").env(EnvVars::UV_TEST_CURRENT_TIMESTAMP, "2025-11-21T12:00:00Z"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Ignoring existing lockfile due to change of exclude newer timestamp from `2025-11-07T12:00:00Z` to `2024-01-01T00:00:00Z` + Resolved 2 packages in [TIME] + Updated iniconfig v2.3.0 -> v2.0.0 + "); + + let lock2 = context.read("uv.lock"); + assert_snapshot!(lock2, @r#" + version = 1 + revision = 3 + requires-python = ">=3.12" + + [options] + exclude-newer = "2024-01-01T00:00:00Z" + + [[package]] + name = "iniconfig" + version = "2.0.0" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646, upload-time = "2023-01-07T11:08:11.254Z" } + wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892, upload-time = "2023-01-07T11:08:09.864Z" }, + ] + + [[package]] + name = "project" + version = "0.1.0" + source = { virtual = "." } + dependencies = [ + { name = "iniconfig" }, + ] + + [package.metadata] + requires-dist = [{ name = "iniconfig" }] + "#); + + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["iniconfig"] + + [tool.uv] + exclude-newer = "invalid span" + "#, + )?; + + uv_snapshot!(context.filters(), context.lock().env_remove("UV_EXCLUDE_NEWER").env(EnvVars::UV_TEST_CURRENT_TIMESTAMP, "2025-11-21T12:00:00Z"), @r#" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + warning: Failed to parse `pyproject.toml` during settings discovery: + TOML parse error at line 9, column 25 + | + 9 | exclude-newer = "invalid span" + | ^^^^^^^^^^^^^^ + `invalid span` could not be parsed as a timestamp, date, or relative duration: failed to parse year in date "invalid span": failed to parse "inva" as year (a four digit integer): invalid digit, expected 0-9 but got i and failed to parse "invalid span" in the "friendly" format: parsing a friendly duration requires it to start with a unit value (a decimal integer) after an optional sign, but no integer was found + + Ignoring existing lockfile due to removal of global exclude newer + Resolved 2 packages in [TIME] + Updated iniconfig v2.0.0 -> v2.3.0 + "#); + + Ok(()) +} + +/// Update lockfile with --upgrade when exclude-newer changes in pyproject.toml. +#[test] +fn lock_exclude_newer_pyproject_upgrade_works() -> 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 = ["iniconfig"] + + [tool.uv] + exclude-newer = "2 weeks" + "#, + )?; + + uv_snapshot!(context.filters(), context.lock().env_remove("UV_EXCLUDE_NEWER").env(EnvVars::UV_TEST_CURRENT_TIMESTAMP, "2025-11-21T12:00:00Z"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 2 packages in [TIME] + "###); + + let lock1 = context.read("uv.lock"); + let timestamp1 = lock1 + .lines() + .find(|line| line.contains("exclude-newer = ")) + .expect("Should find exclude-newer line"); + + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["iniconfig"] + + [tool.uv] + exclude-newer = "30 days" + "#, + )?; + + context + .lock() + .env_remove("UV_EXCLUDE_NEWER") + .arg("--upgrade") + .assert() + .success(); + + let lock2 = context.read("uv.lock"); + let timestamp2 = lock2 + .lines() + .find(|line| line.contains("exclude-newer = ")) + .expect("Should find exclude-newer line"); + + assert_ne!( + timestamp1, timestamp2, + "Timestamp should change with --upgrade" + ); + + Ok(()) +} + +/// Update lockfile when absolute exclude-newer changes in pyproject.toml. +#[test] +fn lock_exclude_newer_pyproject_absolute_update() -> 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 = ["iniconfig"] + "#, + )?; + + uv_snapshot!(context.filters(), context.lock().arg("--exclude-newer").arg("2024-03-25T00:00:00Z"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 2 packages in [TIME] + "###); + + let lock1 = context.read("uv.lock"); + assert_snapshot!(lock1, @r#" + version = 1 + revision = 3 + requires-python = ">=3.12" + + [options] + exclude-newer = "2024-03-25T00:00:00Z" + + [[package]] + name = "iniconfig" + version = "2.0.0" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646, upload-time = "2023-01-07T11:08:11.254Z" } + wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892, upload-time = "2023-01-07T11:08:09.864Z" }, + ] + + [[package]] + name = "project" + version = "0.1.0" + source = { virtual = "." } + dependencies = [ + { name = "iniconfig" }, + ] + + [package.metadata] + requires-dist = [{ name = "iniconfig" }] + "#); + + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["iniconfig"] + + [tool.uv] + exclude-newer = "2024-01-01T00:00:00Z" + "#, + )?; + + uv_snapshot!(context.filters(), context.lock().env_remove("UV_EXCLUDE_NEWER").env(EnvVars::UV_TEST_CURRENT_TIMESTAMP, "2025-11-21T12:00:00Z").arg("--upgrade"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Ignoring existing lockfile due to change of exclude newer timestamp from `2024-03-25T00:00:00Z` to `2024-01-01T00:00:00Z` + Resolved 2 packages in [TIME] + "); + + let lock2 = context.read("uv.lock"); + assert_snapshot!(lock2, @r#" + version = 1 + revision = 3 + requires-python = ">=3.12" + + [options] + exclude-newer = "2024-01-01T00:00:00Z" + + [[package]] + name = "iniconfig" + version = "2.0.0" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646, upload-time = "2023-01-07T11:08:11.254Z" } + wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892, upload-time = "2023-01-07T11:08:09.864Z" }, + ] + + [[package]] + name = "project" + version = "0.1.0" + source = { virtual = "." } + dependencies = [ + { name = "iniconfig" }, + ] + + [package.metadata] + requires-dist = [{ name = "iniconfig" }] + "#); + + Ok(()) +} + +/// Relative timestamps in pyproject.toml produce reproducible lockfiles. +#[test] +fn lock_exclude_newer_pyproject_relative_reproducible() -> 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 = ["iniconfig"] + + [tool.uv] + exclude-newer = "2 weeks" + "#, + )?; + + // Initial lock (remove env var to use pyproject.toml value) + context + .lock() + .env_remove("UV_EXCLUDE_NEWER") + .assert() + .success(); + + // Check the initial lockfile + let lock1 = context.read("uv.lock"); + let timestamp1 = lock1 + .lines() + .find(|line| line.contains("exclude-newer = ")) + .expect("Should find exclude-newer line"); + + // Sleep for a bit to ensure time passes + std::thread::sleep(std::time::Duration::from_millis(100)); + + // Re-lock WITHOUT any changes - relative timestamps get recalculated each time + // to the current time minus the span + context + .lock() + .env_remove("UV_EXCLUDE_NEWER") + .assert() + .success(); + + // Check the lockfile again + let lock2 = context.read("uv.lock"); + let timestamp2 = lock2 + .lines() + .find(|line| line.contains("exclude-newer = ")) + .expect("Should find exclude-newer line"); + + // Verify both lockfiles have the span stored (for reproducibility tracking) + assert!(lock1.contains("exclude-newer-span = \"P2W\"")); + assert!(lock2.contains("exclude-newer-span = \"P2W\"")); + + // The timestamps will be different because they're computed from the current time, + // but they should be close (within a few hundred milliseconds) + assert_ne!( + timestamp1, timestamp2, + "Relative timestamps get recalculated each lock run" + ); + + // Now test with ABSOLUTE timestamp + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["iniconfig"] + + [tool.uv] + exclude-newer = "2024-03-25T00:00:00Z" + "#, + )?; + + // Lock with absolute timestamp + context + .lock() + .env_remove("UV_EXCLUDE_NEWER") + .assert() + .success(); + + let lock3 = context.read("uv.lock"); + assert_snapshot!(lock3, @r#" + version = 1 + revision = 3 + requires-python = ">=3.12" + + [options] + exclude-newer = "2024-03-25T00:00:00Z" + + [[package]] + name = "iniconfig" + version = "2.0.0" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646, upload-time = "2023-01-07T11:08:11.254Z" } + wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892, upload-time = "2023-01-07T11:08:09.864Z" }, + ] + + [[package]] + name = "project" + version = "0.1.0" + source = { virtual = "." } + dependencies = [ + { name = "iniconfig" }, + ] + + [package.metadata] + requires-dist = [{ name = "iniconfig" }] + "#); + + // Re-lock - with absolute timestamp, it should be stable + context + .lock() + .env_remove("UV_EXCLUDE_NEWER") + .assert() + .success(); + + let lock4 = context.read("uv.lock"); + assert_snapshot!(lock4, @r#" + version = 1 + revision = 3 + requires-python = ">=3.12" + + [options] + exclude-newer = "2024-03-25T00:00:00Z" + + [[package]] + name = "iniconfig" + version = "2.0.0" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646, upload-time = "2023-01-07T11:08:11.254Z" } + wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892, upload-time = "2023-01-07T11:08:09.864Z" }, + ] + + [[package]] + name = "project" + version = "0.1.0" + source = { virtual = "." } + dependencies = [ + { name = "iniconfig" }, + ] + + [package.metadata] + requires-dist = [{ name = "iniconfig" }] + "#); + + Ok(()) +} + +/// Test that years are explicitly rejected with specific error message. +#[test] +fn lock_exclude_newer_rejects_years() -> 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 = ["iniconfig"] + "#, + )?; + + uv_snapshot!(context.filters(), context + .lock() + .env_remove(EnvVars::UV_EXCLUDE_NEWER) + .env(EnvVars::UV_TEST_CURRENT_TIMESTAMP, "2025-11-21T12:00:00Z") + .arg("--exclude-newer") + .arg("1 year"), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: invalid value '1 year' for '--exclude-newer ': Duration `1 year` uses unit 'years' which is not allowed; use days instead, e.g., `365 days`. + + For more information, try '--help'. + "); + + Ok(()) +} + +/// Test that months are explicitly rejected with specific error message. +#[test] +fn lock_exclude_newer_rejects_months() -> 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 = ["iniconfig"] + "#, + )?; + + uv_snapshot!(context.filters(), context + .lock() + .env_remove(EnvVars::UV_EXCLUDE_NEWER) + .env(EnvVars::UV_TEST_CURRENT_TIMESTAMP, "2025-11-21T12:00:00Z") + .arg("--exclude-newer") + .arg("1 month"), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: invalid value '1 month' for '--exclude-newer ': Duration `1 month` uses 'months' which is not allowed; use days instead, e.g., `30 days`. + + For more information, try '--help'. + "); + + Ok(()) +} + +/// Test that package-specific exclude-newer works with valid relative times. +#[test] +fn lock_exclude_newer_package_valid_relative() -> 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"] + "#, + )?; + + // Test with days + uv_snapshot!(context.filters(), context + .lock() + .env_remove(EnvVars::UV_EXCLUDE_NEWER) + .env(EnvVars::UV_TEST_CURRENT_TIMESTAMP, "2025-11-21T12:00:00Z") + .arg("--exclude-newer-package") + .arg("tqdm=7 days"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 8 packages in [TIME] + "###); + + let _ = fs_err::remove_file(context.temp_dir.child("uv.lock")); + + // Test with weeks + uv_snapshot!(context.filters(), context + .lock() + .env_remove(EnvVars::UV_EXCLUDE_NEWER) + .env(EnvVars::UV_TEST_CURRENT_TIMESTAMP, "2025-11-21T12:00:00Z") + .arg("--exclude-newer-package") + .arg("tqdm=2 weeks"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 8 packages in [TIME] + "###); + + let _ = fs_err::remove_file(context.temp_dir.child("uv.lock")); + + // Test with hours + uv_snapshot!(context.filters(), context + .lock() + .env_remove(EnvVars::UV_EXCLUDE_NEWER) + .env(EnvVars::UV_TEST_CURRENT_TIMESTAMP, "2025-11-21T12:00:00Z") + .arg("--exclude-newer-package") + .arg("tqdm=24 hours"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 8 packages in [TIME] + "###); + + Ok(()) +} + +/// Test that package-specific exclude-newer rejects years. +#[test] +fn lock_exclude_newer_package_rejects_years() -> 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"] + "#, + )?; + + uv_snapshot!(context.filters(), context + .lock() + .env_remove(EnvVars::UV_EXCLUDE_NEWER) + .arg("--exclude-newer-package") + .arg("numpy=1 year"), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: invalid value 'numpy=1 year' for '--exclude-newer-package ': Invalid `exclude-newer-package` timestamp `1 year`: Duration `1 year` uses unit 'years' which is not allowed; use days instead, e.g., `365 days`. + + For more information, try '--help'. + "); + + Ok(()) +} + +/// Test that package-specific exclude-newer rejects months. +#[test] +fn lock_exclude_newer_package_rejects_months() -> 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"] + "#, + )?; + + uv_snapshot!(context.filters(), context + .lock() + .env_remove(EnvVars::UV_EXCLUDE_NEWER) + .arg("--exclude-newer-package") + .arg("tqdm=3 months"), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: invalid value 'tqdm=3 months' for '--exclude-newer-package ': Invalid `exclude-newer-package` timestamp `3 months`: Duration `3 months` uses 'months' which is not allowed; use days instead, e.g., `90 days`. + + For more information, try '--help'. + "); + + Ok(()) +} + +/// Test package-specific relative times in pyproject.toml. +#[test] +fn lock_exclude_newer_package_pyproject_relative() -> 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"] + + [tool.uv.exclude-newer-package] + tqdm = "7 days" + requests = "2 weeks" + "#, + )?; + + uv_snapshot!(context.filters(), context.lock().env_remove("UV_EXCLUDE_NEWER").env(EnvVars::UV_TEST_CURRENT_TIMESTAMP, "2025-11-21T12:00:00Z"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 8 packages in [TIME] + "###); + + let lock = context.read("uv.lock"); + // Verify the lock file contains package-specific exclude-newer entries + assert!(lock.contains("[options.exclude-newer-package]")); + assert!(lock.contains("tqdm = ")); + assert!(lock.contains("requests = ")); + + Ok(()) +} + +/// Test mixing absolute global with relative package-specific timestamps. +#[test] +fn lock_exclude_newer_mixed_absolute_global_relative_package() -> 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"] + "#, + )?; + + uv_snapshot!(context.filters(), context + .lock() + .env_remove(EnvVars::UV_EXCLUDE_NEWER) + .arg("--exclude-newer") + .arg("2024-01-01T00:00:00Z") + .arg("--exclude-newer-package") + .arg("tqdm=7 days"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 8 packages in [TIME] + "###); + + let lock = context.read("uv.lock"); + assert!(lock.contains("exclude-newer = \"2024-01-01T00:00:00Z\"")); + assert!(lock.contains("[options.exclude-newer-package]")); + + Ok(()) +} + +/// Test mixing relative global with absolute package-specific timestamps. +#[test] +fn lock_exclude_newer_mixed_relative_global_absolute_package() -> 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"] + "#, + )?; + + uv_snapshot!(context.filters(), context + .lock() + .env_remove(EnvVars::UV_EXCLUDE_NEWER) + .env(EnvVars::UV_TEST_CURRENT_TIMESTAMP, "2025-11-21T12:00:00Z") + .arg("--exclude-newer") + .arg("1 week") + .arg("--exclude-newer-package") + .arg("tqdm=2024-01-01T00:00:00Z"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 8 packages in [TIME] + "###); + + let lock = context.read("uv.lock"); + assert!(lock.contains("exclude-newer-span = \"P1W\"")); + assert!(lock.contains("[options.exclude-newer-package]")); + assert!(lock.contains("tqdm = \"2024-01-01T00:00:00Z\"")); + + Ok(()) +} + +/// Changing the span in pyproject.toml invalidates the lockfile. +#[test] +fn lock_exclude_newer_span_change_invalidates() -> Result<()> { + let context = TestContext::new("3.12"); + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + + // Initial pyproject.toml with exclude-newer using "2 weeks" span + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["iniconfig"] + + [tool.uv] + exclude-newer = "2 weeks" + "#, + )?; + + // Initial lock (remove env var to use pyproject.toml value) + uv_snapshot!(context.filters(), context.lock().env_remove("UV_EXCLUDE_NEWER").env(EnvVars::UV_TEST_CURRENT_TIMESTAMP, "2025-11-21T12:00:00Z"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 2 packages in [TIME] + "###); + + // Check the initial lockfile contains both exclude-newer and exclude-newer-span + let lock1 = context.read("uv.lock"); + assert_snapshot!(lock1, @r#" + version = 1 + revision = 3 + requires-python = ">=3.12" + + [options] + exclude-newer = "2025-11-07T12:00:00Z" + exclude-newer-span = "P2W" + + [[package]] + name = "iniconfig" + version = "2.3.0" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } + wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, + ] + + [[package]] + name = "project" + version = "0.1.0" + source = { virtual = "." } + dependencies = [ + { name = "iniconfig" }, + ] + + [package.metadata] + requires-dist = [{ name = "iniconfig" }] + "#); + + // Change the span in pyproject.toml to "30 days" + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["iniconfig"] + + [tool.uv] + exclude-newer = "30 days" + "#, + )?; + + uv_snapshot!(context.filters(), context.lock().env_remove("UV_EXCLUDE_NEWER").env(EnvVars::UV_TEST_CURRENT_TIMESTAMP, "2025-11-21T12:00:00Z"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Ignoring existing lockfile due to change of exclude newer timestamp from `2025-11-07T12:00:00Z` to `2025-10-22T12:00:00Z` + Resolved 2 packages in [TIME] + "); + + let lock2 = context.read("uv.lock"); + assert_snapshot!(lock2, @r#" + version = 1 + revision = 3 + requires-python = ">=3.12" + + [options] + exclude-newer = "2025-10-22T12:00:00Z" + exclude-newer-span = "P30D" + + [[package]] + name = "iniconfig" + version = "2.3.0" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } + wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, + ] + + [[package]] + name = "project" + version = "0.1.0" + source = { virtual = "." } + dependencies = [ + { name = "iniconfig" }, + ] + + [package.metadata] + requires-dist = [{ name = "iniconfig" }] + "#); + uv_snapshot!(context.filters(), context.lock().env_remove("UV_EXCLUDE_NEWER").env(EnvVars::UV_TEST_CURRENT_TIMESTAMP, "2025-11-21T12:00:00Z"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Ignoring existing lockfile due to addition of exclude newer span `P30D` + Resolved 2 packages in [TIME] + "); + + Ok(()) +} diff --git a/crates/uv/tests/it/main.rs b/crates/uv/tests/it/main.rs index 489a45228d804..f1659e7a9a030 100644 --- a/crates/uv/tests/it/main.rs +++ b/crates/uv/tests/it/main.rs @@ -45,6 +45,9 @@ mod lock; #[cfg(all(feature = "python", feature = "pypi"))] mod lock_conflict; +#[cfg(all(feature = "python", feature = "pypi"))] +mod lock_exclude_newer_relative; + mod lock_scenarios; mod network; diff --git a/crates/uv/tests/it/pip_compile.rs b/crates/uv/tests/it/pip_compile.rs index 112b1bf688436..d3217f705e62c 100644 --- a/crates/uv/tests/it/pip_compile.rs +++ b/crates/uv/tests/it/pip_compile.rs @@ -3403,16 +3403,16 @@ fn compile_exclude_newer() -> Result<()> { .env_remove(EnvVars::UV_EXCLUDE_NEWER) .arg("requirements.in") .arg("--exclude-newer") - .arg("2022-04-04+02:00"), @r###" + .arg("2022-04-04+02:00"), @r#" success: false exit_code: 2 ----- stdout ----- ----- stderr ----- - error: invalid value '2022-04-04+02:00' for '--exclude-newer ': `2022-04-04+02:00` could not be parsed as a valid date: parsed value '2022-04-04', but unparsed input "+02:00" remains (expected no unparsed input) + error: invalid value '2022-04-04+02:00' for '--exclude-newer ': `2022-04-04+02:00` could not be parsed as a timestamp, date, or relative duration: parsed value '2022-04-04', but unparsed input "+02:00" remains (expected no unparsed input) and failed to parse "2022-04-04+02:00" in the "friendly" format: expected to find unit designator suffix (e.g., 'years' or 'secs'), but found input beginning with "-04-04+02:00" instead For more information, try '--help'. - "### + "# ); // Check the error message for the case of @@ -3423,16 +3423,16 @@ fn compile_exclude_newer() -> Result<()> { .env_remove(EnvVars::UV_EXCLUDE_NEWER) .arg("requirements.in") .arg("--exclude-newer") - .arg("2022-04-04T26:00:00+00"), @r###" + .arg("2022-04-04T26:00:00+00"), @r#" success: false exit_code: 2 ----- stdout ----- ----- stderr ----- - error: invalid value '2022-04-04T26:00:00+00' for '--exclude-newer ': `2022-04-04T26:00:00+00` could not be parsed as a valid date: failed to parse hour in time "26:00:00+00": hour is not valid: parameter 'hour' with value 26 is not in the required range of 0..=23 + error: invalid value '2022-04-04T26:00:00+00' for '--exclude-newer ': `2022-04-04T26:00:00+00` could not be parsed as a timestamp, date, or relative duration: failed to parse hour in time "26:00:00+00": hour is not valid: parameter 'hour' with value 26 is not in the required range of 0..=23 and failed to parse "2022-04-04T26:00:00+00" in the "friendly" format: expected to find unit designator suffix (e.g., 'years' or 'secs'), but found input beginning with "-04-04T26:00:00+00" instead For more information, try '--help'. - "### + "# ); Ok(()) @@ -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 date: failed to parse year in date "invalid-date": failed to parse "inva" as year (a four digit integer): invalid digit, expected 0-9 but got i + 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 timestamp, date, or relative duration: failed to parse year in date "invalid-date": failed to parse "inva" as year (a four digit integer): invalid digit, expected 0-9 but got i and failed to parse "invalid-date" in the "friendly" format: parsing a friendly duration requires it to start with a unit value (a decimal integer) after an optional sign, but no integer was found For more information, try '--help'. "# diff --git a/crates/uv/tests/it/snapshots/it__ecosystem__home-assistant-core-uv-lock-output.snap b/crates/uv/tests/it/snapshots/it__ecosystem__home-assistant-core-uv-lock-output.snap index a827fc15395c7..16a93567e6db6 100644 --- a/crates/uv/tests/it/snapshots/it__ecosystem__home-assistant-core-uv-lock-output.snap +++ b/crates/uv/tests/it/snapshots/it__ecosystem__home-assistant-core-uv-lock-output.snap @@ -1,5 +1,5 @@ --- -source: crates/uv/tests/ecosystem.rs +source: crates/uv/tests/it/ecosystem.rs expression: snapshot --- success: true diff --git a/crates/uv/tests/it/snapshots/it__ecosystem__saleor-uv-lock-output.snap b/crates/uv/tests/it/snapshots/it__ecosystem__saleor-uv-lock-output.snap index f6ace74a1f274..c5aaef2cf6d58 100644 --- a/crates/uv/tests/it/snapshots/it__ecosystem__saleor-uv-lock-output.snap +++ b/crates/uv/tests/it/snapshots/it__ecosystem__saleor-uv-lock-output.snap @@ -1,5 +1,5 @@ --- -source: crates/uv/tests/ecosystem.rs +source: crates/uv/tests/it/ecosystem.rs expression: snapshot --- success: true diff --git a/crates/uv/tests/it/sync.rs b/crates/uv/tests/it/sync.rs index fad338270f4e7..150cd35b67eb2 100644 --- a/crates/uv/tests/it/sync.rs +++ b/crates/uv/tests/it/sync.rs @@ -12536,7 +12536,7 @@ dependencies = [ ----- stdout ----- ----- stderr ----- - Ignoring existing lockfile due to change in timestamp cutoff: `global: 2022-04-04T12:00:00Z` vs. `global: 2022-04-04T12:00:00Z, tqdm: 2022-09-04T00:00:00Z` + Ignoring existing lockfile due to addition of exclude newer `2022-09-04T00:00:00Z` for package `tqdm` Resolved [N] packages in [TIME] Prepared [N] packages in [TIME] Uninstalled [N] packages in [TIME] @@ -12619,7 +12619,7 @@ exclude-newer-package = { tqdm = "2022-09-04T00:00:00Z" } ----- stdout ----- ----- stderr ----- - Ignoring existing lockfile due to change in timestamp cutoff: `global: 2022-04-04T12:00:00Z` vs. `global: 2022-04-04T12:00:00Z, tqdm: 2022-09-04T00:00:00Z` + Ignoring existing lockfile due to addition of exclude newer `2022-09-04T00:00:00Z` for package `tqdm` Resolved [N] packages in [TIME] Prepared [N] packages in [TIME] Uninstalled [N] packages in [TIME] diff --git a/docs/concepts/resolution.md b/docs/concepts/resolution.md index 9b89afc0c36d9..951b3b41338fd 100644 --- a/docs/concepts/resolution.md +++ b/docs/concepts/resolution.md @@ -651,8 +651,14 @@ with an unexpected error. uv supports an `--exclude-newer` option to limit resolution to distributions published before a specific date, allowing reproduction of installations regardless of new package releases. The date may be specified as an [RFC 3339](https://www.rfc-editor.org/rfc/rfc3339.html) timestamp (e.g., -`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. +`2006-12-02T02:07:43Z`), a local date in the same format (e.g., `2006-12-02`) in your system's +configured time zone, or a relative duration (e.g., `24 hours`, `1 week`, `30 days`). + +!!! note + + Relative durations do not respect semantics of the local time zone and are always resolved to a + fixed number of seconds assuming that a day is 24 hours (i.e., DST transitions are ignored). + Calendar units such as months and years are not allowed. Note 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 diff --git a/docs/guides/scripts.md b/docs/guides/scripts.md index 26d85e76ddeb5..a4d358166a8de 100644 --- a/docs/guides/scripts.md +++ b/docs/guides/scripts.md @@ -296,7 +296,7 @@ of inline script metadata to limit uv to only considering distributions released date. This is useful for improving the reproducibility of your script when run at a later point in time. -The date must be specified as an [RFC 3339](https://www.rfc-editor.org/rfc/rfc3339.html) timestamp +The date should be specified as an [RFC 3339](https://www.rfc-editor.org/rfc/rfc3339.html) timestamp (e.g., `2006-12-02T02:07:43Z`). ```python title="example.py" diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 77cec33db6d75..a25ca7f7837e6 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -399,9 +399,11 @@ uv run [OPTIONS] [COMMAND]

May also be set with the UV_ENV_FILE environment variable.

--exact

Perform an exact sync, removing extraneous packages.

When enabled, uv will remove any extraneous packages from the environment. By default, uv run will make the minimum necessary changes to satisfy the requirements.

--exclude-newer exclude-newer

Limit candidate packages to those that were uploaded prior to the given date.

-

Accepts both RFC 3339 timestamps (e.g., 2006-12-02T02:07:43Z) and local dates in the same format (e.g., 2006-12-02) in your system's configured time zone.

+

Accepts RFC 3339 timestamps (e.g., 2006-12-02T02:07:43Z), local dates in the same format (e.g., 2006-12-02) which use your system's configured time zone, and relative durations (e.g., 24 hours, 1 week, 30 days).

+

Relative durations do not respect semantics of the local time zone and are always resolved to a fixed number of seconds assuming that a day is 24 hours (i.e., DST transitions are ignored). Calendar units such as months and years are not allowed.

May also be set with the UV_EXCLUDE_NEWER environment variable.

--exclude-newer-package exclude-newer-package

Limit candidate packages for specific packages to those that were uploaded prior to the given date.

-

Accepts package-date pairs in the format PACKAGE=DATE, where DATE is an RFC 3339 timestamp (e.g., 2006-12-02T02:07:43Z) or local date (e.g., 2006-12-02) in your system's configured time zone.

+

Accepts package-date pairs in the format PACKAGE=DATE, where DATE is an RFC 3339 timestamp (e.g., 2006-12-02T02:07:43Z), a local date in the same format (e.g., 2006-12-02) which use your system's configured time zone, or relative duration (e.g., 24 hours, 1 week, 30 days).

+

Relative durations do not respect semantics of the local time zone and are always resolved to a fixed number of seconds assuming that a day is 24 hours (i.e., DST transitions are ignored). Calendar units such as months and years are not allowed.

Can be provided multiple times for different packages.

--extra extra

Include optional dependencies from the specified extra name.

May be provided more than once.

@@ -819,9 +821,11 @@ uv add [OPTIONS] >

See --project to only change the project root directory.

May also be set with the UV_WORKING_DIRECTORY environment variable.

--editable

Add the requirements as editable

--exclude-newer exclude-newer

Limit candidate packages to those that were uploaded prior to the given date.

-

Accepts both RFC 3339 timestamps (e.g., 2006-12-02T02:07:43Z) and local dates in the same format (e.g., 2006-12-02) in your system's configured time zone.

+

Accepts RFC 3339 timestamps (e.g., 2006-12-02T02:07:43Z), local dates in the same format (e.g., 2006-12-02) which use your system's configured time zone, and relative durations (e.g., 24 hours, 1 week, 30 days).

+

Relative durations do not respect semantics of the local time zone and are always resolved to a fixed number of seconds assuming that a day is 24 hours (i.e., DST transitions are ignored). Calendar units such as months and years are not allowed.

May also be set with the UV_EXCLUDE_NEWER environment variable.

--exclude-newer-package exclude-newer-package

Limit candidate packages for specific packages to those that were uploaded prior to the given date.

-

Accepts package-date pairs in the format PACKAGE=DATE, where DATE is an RFC 3339 timestamp (e.g., 2006-12-02T02:07:43Z) or local date (e.g., 2006-12-02) in your system's configured time zone.

+

Accepts package-date pairs in the format PACKAGE=DATE, where DATE is an RFC 3339 timestamp (e.g., 2006-12-02T02:07:43Z), a local date in the same format (e.g., 2006-12-02) which use your system's configured time zone, or relative duration (e.g., 24 hours, 1 week, 30 days).

+

Relative durations do not respect semantics of the local time zone and are always resolved to a fixed number of seconds assuming that a day is 24 hours (i.e., DST transitions are ignored). Calendar units such as months and years are not allowed.

Can be provided multiple times for different packages.

--extra extra

Extras to enable for the dependency.

May be provided more than once.

@@ -1030,9 +1034,11 @@ uv remove [OPTIONS] ...

Relative paths are resolved with the given directory as the base.

See --project to only change the project root directory.

May also be set with the UV_WORKING_DIRECTORY environment variable.

--exclude-newer exclude-newer

Limit candidate packages to those that were uploaded prior to the given date.

-

Accepts both RFC 3339 timestamps (e.g., 2006-12-02T02:07:43Z) and local dates in the same format (e.g., 2006-12-02) in your system's configured time zone.

+

Accepts RFC 3339 timestamps (e.g., 2006-12-02T02:07:43Z), local dates in the same format (e.g., 2006-12-02) which use your system's configured time zone, and relative durations (e.g., 24 hours, 1 week, 30 days).

+

Relative durations do not respect semantics of the local time zone and are always resolved to a fixed number of seconds assuming that a day is 24 hours (i.e., DST transitions are ignored). Calendar units such as months and years are not allowed.

May also be set with the UV_EXCLUDE_NEWER environment variable.

--exclude-newer-package exclude-newer-package

Limit candidate packages for specific packages to those that were uploaded prior to the given date.

-

Accepts package-date pairs in the format PACKAGE=DATE, where DATE is an RFC 3339 timestamp (e.g., 2006-12-02T02:07:43Z) or local date (e.g., 2006-12-02) in your system's configured time zone.

+

Accepts package-date pairs in the format PACKAGE=DATE, where DATE is an RFC 3339 timestamp (e.g., 2006-12-02T02:07:43Z), a local date in the same format (e.g., 2006-12-02) which use your system's configured time zone, or relative duration (e.g., 24 hours, 1 week, 30 days).

+

Relative durations do not respect semantics of the local time zone and are always resolved to a fixed number of seconds assuming that a day is 24 hours (i.e., DST transitions are ignored). Calendar units such as months and years are not allowed.

Can be provided multiple times for different packages.

--extra-index-url extra-index-url

(Deprecated: use --index instead) Extra URLs of package indexes to use, in addition to --index-url.

Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

@@ -1213,9 +1219,11 @@ uv version [OPTIONS] [VALUE]

May also be set with the UV_WORKING_DIRECTORY environment variable.

--dry-run

Don't write a new version to the pyproject.toml

Instead, the version will be displayed.

--exclude-newer exclude-newer

Limit candidate packages to those that were uploaded prior to the given date.

-

Accepts both RFC 3339 timestamps (e.g., 2006-12-02T02:07:43Z) and local dates in the same format (e.g., 2006-12-02) in your system's configured time zone.

+

Accepts RFC 3339 timestamps (e.g., 2006-12-02T02:07:43Z), local dates in the same format (e.g., 2006-12-02) which use your system's configured time zone, and relative durations (e.g., 24 hours, 1 week, 30 days).

+

Relative durations do not respect semantics of the local time zone and are always resolved to a fixed number of seconds assuming that a day is 24 hours (i.e., DST transitions are ignored). Calendar units such as months and years are not allowed.

May also be set with the UV_EXCLUDE_NEWER environment variable.

--exclude-newer-package exclude-newer-package

Limit candidate packages for specific packages to those that were uploaded prior to the given date.

-

Accepts package-date pairs in the format PACKAGE=DATE, where DATE is an RFC 3339 timestamp (e.g., 2006-12-02T02:07:43Z) or local date (e.g., 2006-12-02) in your system's configured time zone.

+

Accepts package-date pairs in the format PACKAGE=DATE, where DATE is an RFC 3339 timestamp (e.g., 2006-12-02T02:07:43Z), a local date in the same format (e.g., 2006-12-02) which use your system's configured time zone, or relative duration (e.g., 24 hours, 1 week, 30 days).

+

Relative durations do not respect semantics of the local time zone and are always resolved to a fixed number of seconds assuming that a day is 24 hours (i.e., DST transitions are ignored). Calendar units such as months and years are not allowed.

Can be provided multiple times for different packages.

--extra-index-url extra-index-url

(Deprecated: use --index instead) Extra URLs of package indexes to use, in addition to --index-url.

Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

@@ -1402,9 +1410,11 @@ uv sync [OPTIONS]

May also be set with the UV_WORKING_DIRECTORY environment variable.

--dry-run

Perform a dry run, without writing the lockfile or modifying the project environment.

In dry-run mode, uv will resolve the project's dependencies and report on the resulting changes to both the lockfile and the project environment, but will not modify either.

--exclude-newer exclude-newer

Limit candidate packages to those that were uploaded prior to the given date.

-

Accepts both RFC 3339 timestamps (e.g., 2006-12-02T02:07:43Z) and local dates in the same format (e.g., 2006-12-02) in your system's configured time zone.

+

Accepts RFC 3339 timestamps (e.g., 2006-12-02T02:07:43Z), local dates in the same format (e.g., 2006-12-02) which use your system's configured time zone, and relative durations (e.g., 24 hours, 1 week, 30 days).

+

Relative durations do not respect semantics of the local time zone and are always resolved to a fixed number of seconds assuming that a day is 24 hours (i.e., DST transitions are ignored). Calendar units such as months and years are not allowed.

May also be set with the UV_EXCLUDE_NEWER environment variable.

--exclude-newer-package exclude-newer-package

Limit candidate packages for specific packages to those that were uploaded prior to the given date.

-

Accepts package-date pairs in the format PACKAGE=DATE, where DATE is an RFC 3339 timestamp (e.g., 2006-12-02T02:07:43Z) or local date (e.g., 2006-12-02) in your system's configured time zone.

+

Accepts package-date pairs in the format PACKAGE=DATE, where DATE is an RFC 3339 timestamp (e.g., 2006-12-02T02:07:43Z), a local date in the same format (e.g., 2006-12-02) which use your system's configured time zone, or relative duration (e.g., 24 hours, 1 week, 30 days).

+

Relative durations do not respect semantics of the local time zone and are always resolved to a fixed number of seconds assuming that a day is 24 hours (i.e., DST transitions are ignored). Calendar units such as months and years are not allowed.

Can be provided multiple times for different packages.

--extra extra

Include optional dependencies from the specified extra name.

May be provided more than once.

@@ -1669,9 +1679,11 @@ uv lock [OPTIONS]

May also be set with the UV_WORKING_DIRECTORY environment variable.

--dry-run

Perform a dry run, without writing the lockfile.

In dry-run mode, uv will resolve the project's dependencies and report on the resulting changes, but will not write the lockfile to disk.

--exclude-newer exclude-newer

Limit candidate packages to those that were uploaded prior to the given date.

-

Accepts both RFC 3339 timestamps (e.g., 2006-12-02T02:07:43Z) and local dates in the same format (e.g., 2006-12-02) in your system's configured time zone.

-

May also be set with the UV_EXCLUDE_NEWER environment variable.

--exclude-newer-package exclude-newer-package

Limit candidate packages for a specific package to those that were uploaded prior to the given date.

-

Accepts package-date pairs in the format PACKAGE=DATE, where DATE is an RFC 3339 timestamp (e.g., 2006-12-02T02:07:43Z) or local date (e.g., 2006-12-02) in your system's configured time zone.

+

Accepts RFC 3339 timestamps (e.g., 2006-12-02T02:07:43Z), local dates in the same format (e.g., 2006-12-02) which use your system's configured time zone, and relative durations (e.g., 24 hours, 1 week, 30 days).

+

Relative durations do not respect semantics of the local time zone and are always resolved to a fixed number of seconds assuming that a day is 24 hours (i.e., DST transitions are ignored). Calendar units such as months and years are not allowed.

+

May also be set with the UV_EXCLUDE_NEWER environment variable.

--exclude-newer-package exclude-newer-package

Limit candidate packages for specific packages to those that were uploaded prior to the given date.

+

Accepts package-date pairs in the format PACKAGE=DATE, where DATE is an RFC 3339 timestamp (e.g., 2006-12-02T02:07:43Z), a local date in the same format (e.g., 2006-12-02) which use your system's configured time zone, or relative duration (e.g., 24 hours, 1 week, 30 days).

+

Relative durations do not respect semantics of the local time zone and are always resolved to a fixed number of seconds assuming that a day is 24 hours (i.e., DST transitions are ignored). Calendar units such as months and years are not allowed.

Can be provided multiple times for different packages.

--extra-index-url extra-index-url

(Deprecated: use --index instead) Extra URLs of package indexes to use, in addition to --index-url.

Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

@@ -1835,9 +1847,11 @@ uv export [OPTIONS]

Relative paths are resolved with the given directory as the base.

See --project to only change the project root directory.

May also be set with the UV_WORKING_DIRECTORY environment variable.

--exclude-newer exclude-newer

Limit candidate packages to those that were uploaded prior to the given date.

-

Accepts both RFC 3339 timestamps (e.g., 2006-12-02T02:07:43Z) and local dates in the same format (e.g., 2006-12-02) in your system's configured time zone.

-

May also be set with the UV_EXCLUDE_NEWER environment variable.

--exclude-newer-package exclude-newer-package

Limit candidate packages for a specific package to those that were uploaded prior to the given date.

-

Accepts package-date pairs in the format PACKAGE=DATE, where DATE is an RFC 3339 timestamp (e.g., 2006-12-02T02:07:43Z) or local date (e.g., 2006-12-02) in your system's configured time zone.

+

Accepts RFC 3339 timestamps (e.g., 2006-12-02T02:07:43Z), local dates in the same format (e.g., 2006-12-02) which use your system's configured time zone, and relative durations (e.g., 24 hours, 1 week, 30 days).

+

Relative durations do not respect semantics of the local time zone and are always resolved to a fixed number of seconds assuming that a day is 24 hours (i.e., DST transitions are ignored). Calendar units such as months and years are not allowed.

+

May also be set with the UV_EXCLUDE_NEWER environment variable.

--exclude-newer-package exclude-newer-package

Limit candidate packages for specific packages to those that were uploaded prior to the given date.

+

Accepts package-date pairs in the format PACKAGE=DATE, where DATE is an RFC 3339 timestamp (e.g., 2006-12-02T02:07:43Z), a local date in the same format (e.g., 2006-12-02) which use your system's configured time zone, or relative duration (e.g., 24 hours, 1 week, 30 days).

+

Relative durations do not respect semantics of the local time zone and are always resolved to a fixed number of seconds assuming that a day is 24 hours (i.e., DST transitions are ignored). Calendar units such as months and years are not allowed.

Can be provided multiple times for different packages.

--extra extra

Include optional dependencies from the specified extra name.

May be provided more than once.

@@ -2042,9 +2056,11 @@ uv tree [OPTIONS]

Relative paths are resolved with the given directory as the base.

See --project to only change the project root directory.

May also be set with the UV_WORKING_DIRECTORY environment variable.

--exclude-newer exclude-newer

Limit candidate packages to those that were uploaded prior to the given date.

-

Accepts both RFC 3339 timestamps (e.g., 2006-12-02T02:07:43Z) and local dates in the same format (e.g., 2006-12-02) in your system's configured time zone.

-

May also be set with the UV_EXCLUDE_NEWER environment variable.

--exclude-newer-package exclude-newer-package

Limit candidate packages for a specific package to those that were uploaded prior to the given date.

-

Accepts package-date pairs in the format PACKAGE=DATE, where DATE is an RFC 3339 timestamp (e.g., 2006-12-02T02:07:43Z) or local date (e.g., 2006-12-02) in your system's configured time zone.

+

Accepts RFC 3339 timestamps (e.g., 2006-12-02T02:07:43Z), local dates in the same format (e.g., 2006-12-02) which use your system's configured time zone, and relative durations (e.g., 24 hours, 1 week, 30 days).

+

Relative durations do not respect semantics of the local time zone and are always resolved to a fixed number of seconds assuming that a day is 24 hours (i.e., DST transitions are ignored). Calendar units such as months and years are not allowed.

+

May also be set with the UV_EXCLUDE_NEWER environment variable.

--exclude-newer-package exclude-newer-package

Limit candidate packages for specific packages to those that were uploaded prior to the given date.

+

Accepts package-date pairs in the format PACKAGE=DATE, where DATE is an RFC 3339 timestamp (e.g., 2006-12-02T02:07:43Z), a local date in the same format (e.g., 2006-12-02) which use your system's configured time zone, or relative duration (e.g., 24 hours, 1 week, 30 days).

+

Relative durations do not respect semantics of the local time zone and are always resolved to a fixed number of seconds assuming that a day is 24 hours (i.e., DST transitions are ignored). Calendar units such as months and years are not allowed.

Can be provided multiple times for different packages.

--extra-index-url extra-index-url

(Deprecated: use --index instead) Extra URLs of package indexes to use, in addition to --index-url.

Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

@@ -2394,9 +2410,11 @@ uv tool run [OPTIONS] [COMMAND]

May also be set with the UV_WORKING_DIRECTORY environment variable.

--env-file env-file

Load environment variables from a .env file.

Can be provided multiple times, with subsequent files overriding values defined in previous files.

May also be set with the UV_ENV_FILE environment variable.

--exclude-newer exclude-newer

Limit candidate packages to those that were uploaded prior to the given date.

-

Accepts both RFC 3339 timestamps (e.g., 2006-12-02T02:07:43Z) and local dates in the same format (e.g., 2006-12-02) in your system's configured time zone.

+

Accepts RFC 3339 timestamps (e.g., 2006-12-02T02:07:43Z), local dates in the same format (e.g., 2006-12-02) which use your system's configured time zone, and relative durations (e.g., 24 hours, 1 week, 30 days).

+

Relative durations do not respect semantics of the local time zone and are always resolved to a fixed number of seconds assuming that a day is 24 hours (i.e., DST transitions are ignored). Calendar units such as months and years are not allowed.

May also be set with the UV_EXCLUDE_NEWER environment variable.

--exclude-newer-package exclude-newer-package

Limit candidate packages for specific packages to those that were uploaded prior to the given date.

-

Accepts package-date pairs in the format PACKAGE=DATE, where DATE is an RFC 3339 timestamp (e.g., 2006-12-02T02:07:43Z) or local date (e.g., 2006-12-02) in your system's configured time zone.

+

Accepts package-date pairs in the format PACKAGE=DATE, where DATE is an RFC 3339 timestamp (e.g., 2006-12-02T02:07:43Z), a local date in the same format (e.g., 2006-12-02) which use your system's configured time zone, or relative duration (e.g., 24 hours, 1 week, 30 days).

+

Relative durations do not respect semantics of the local time zone and are always resolved to a fixed number of seconds assuming that a day is 24 hours (i.e., DST transitions are ignored). Calendar units such as months and years are not allowed.

Can be provided multiple times for different packages.

--extra-index-url extra-index-url

(Deprecated: use --index instead) Extra URLs of package indexes to use, in addition to --index-url.

Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

@@ -2624,9 +2642,11 @@ uv tool install [OPTIONS]

See --project to only change the project root directory.

May also be set with the UV_WORKING_DIRECTORY environment variable.

--editable, -e

Install the target package in editable mode, such that changes in the package's source directory are reflected without reinstallation

--exclude-newer exclude-newer

Limit candidate packages to those that were uploaded prior to the given date.

-

Accepts both RFC 3339 timestamps (e.g., 2006-12-02T02:07:43Z) and local dates in the same format (e.g., 2006-12-02) in your system's configured time zone.

+

Accepts RFC 3339 timestamps (e.g., 2006-12-02T02:07:43Z), local dates in the same format (e.g., 2006-12-02) which use your system's configured time zone, and relative durations (e.g., 24 hours, 1 week, 30 days).

+

Relative durations do not respect semantics of the local time zone and are always resolved to a fixed number of seconds assuming that a day is 24 hours (i.e., DST transitions are ignored). Calendar units such as months and years are not allowed.

May also be set with the UV_EXCLUDE_NEWER environment variable.

--exclude-newer-package exclude-newer-package

Limit candidate packages for specific packages to those that were uploaded prior to the given date.

-

Accepts package-date pairs in the format PACKAGE=DATE, where DATE is an RFC 3339 timestamp (e.g., 2006-12-02T02:07:43Z) or local date (e.g., 2006-12-02) in your system's configured time zone.

+

Accepts package-date pairs in the format PACKAGE=DATE, where DATE is an RFC 3339 timestamp (e.g., 2006-12-02T02:07:43Z), a local date in the same format (e.g., 2006-12-02) which use your system's configured time zone, or relative duration (e.g., 24 hours, 1 week, 30 days).

+

Relative durations do not respect semantics of the local time zone and are always resolved to a fixed number of seconds assuming that a day is 24 hours (i.e., DST transitions are ignored). Calendar units such as months and years are not allowed.

Can be provided multiple times for different packages.

--excludes, --exclude excludes

Exclude packages from resolution using the given requirements files.

Excludes files are requirements.txt-like files that specify packages to exclude from the resolution. When a package is excluded, it will be omitted from the dependency list entirely and its own dependencies will be ignored during the resolution phase. Excludes are unconditional in that requirement specifiers and markers are ignored; any package listed in the provided file will be omitted from all resolved environments.

@@ -2849,9 +2869,11 @@ uv tool upgrade [OPTIONS] ...

Relative paths are resolved with the given directory as the base.

See --project to only change the project root directory.

May also be set with the UV_WORKING_DIRECTORY environment variable.

--exclude-newer exclude-newer

Limit candidate packages to those that were uploaded prior to the given date.

-

Accepts both RFC 3339 timestamps (e.g., 2006-12-02T02:07:43Z) and local dates in the same format (e.g., 2006-12-02) in your system's configured time zone.

+

Accepts RFC 3339 timestamps (e.g., 2006-12-02T02:07:43Z), local dates in the same format (e.g., 2006-12-02) which use your system's configured time zone, and relative durations (e.g., 24 hours, 1 week, 30 days).

+

Relative durations do not respect semantics of the local time zone and are always resolved to a fixed number of seconds assuming that a day is 24 hours (i.e., DST transitions are ignored). Calendar units such as months and years are not allowed.

May also be set with the UV_EXCLUDE_NEWER environment variable.

--exclude-newer-package exclude-newer-package

Limit candidate packages for specific packages to those that were uploaded prior to the given date.

-

Accepts package-date pairs in the format PACKAGE=DATE, where DATE is an RFC 3339 timestamp (e.g., 2006-12-02T02:07:43Z) or local date (e.g., 2006-12-02) in your system's configured time zone.

+

Accepts package-date pairs in the format PACKAGE=DATE, where DATE is an RFC 3339 timestamp (e.g., 2006-12-02T02:07:43Z), a local date in the same format (e.g., 2006-12-02) which use your system's configured time zone, or relative duration (e.g., 24 hours, 1 week, 30 days).

+

Relative durations do not respect semantics of the local time zone and are always resolved to a fixed number of seconds assuming that a day is 24 hours (i.e., DST transitions are ignored). Calendar units such as months and years are not allowed.

Can be provided multiple times for different packages.

--extra-index-url extra-index-url

(Deprecated: use --index instead) Extra URLs of package indexes to use, in addition to --index-url.

Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

@@ -4069,9 +4091,11 @@ uv pip compile [OPTIONS] >
--emit-index-annotation

Include comment annotations indicating the index used to resolve each package (e.g., # from https://pypi.org/simple)

--emit-index-url

Include --index-url and --extra-index-url entries in the generated output file

--exclude-newer exclude-newer

Limit candidate packages to those that were uploaded prior to the given date.

-

Accepts both RFC 3339 timestamps (e.g., 2006-12-02T02:07:43Z) and local dates in the same format (e.g., 2006-12-02) in your system's configured time zone.

-

May also be set with the UV_EXCLUDE_NEWER environment variable.

--exclude-newer-package exclude-newer-package

Limit candidate packages for a specific package to those that were uploaded prior to the given date.

-

Accepts package-date pairs in the format PACKAGE=DATE, where DATE is an RFC 3339 timestamp (e.g., 2006-12-02T02:07:43Z) or local date (e.g., 2006-12-02) in your system's configured time zone.

+

Accepts RFC 3339 timestamps (e.g., 2006-12-02T02:07:43Z), local dates in the same format (e.g., 2006-12-02) which use your system's configured time zone, and relative durations (e.g., 24 hours, 1 week, 30 days).

+

Relative durations do not respect semantics of the local time zone and are always resolved to a fixed number of seconds assuming that a day is 24 hours (i.e., DST transitions are ignored). Calendar units such as months and years are not allowed.

+

May also be set with the UV_EXCLUDE_NEWER environment variable.

--exclude-newer-package exclude-newer-package

Limit candidate packages for specific packages to those that were uploaded prior to the given date.

+

Accepts package-date pairs in the format PACKAGE=DATE, where DATE is an RFC 3339 timestamp (e.g., 2006-12-02T02:07:43Z), a local date in the same format (e.g., 2006-12-02) which use your system's configured time zone, or relative duration (e.g., 24 hours, 1 week, 30 days).

+

Relative durations do not respect semantics of the local time zone and are always resolved to a fixed number of seconds assuming that a day is 24 hours (i.e., DST transitions are ignored). Calendar units such as months and years are not allowed.

Can be provided multiple times for different packages.

--excludes, --exclude excludes

Exclude packages from resolution using the given requirements files.

Excludes files are requirements.txt-like files that specify packages to exclude from the resolution. When a package is excluded, it will be omitted from the dependency list entirely and its own dependencies will be ignored during the resolution phase. Excludes are unconditional in that requirement specifiers and markers are ignored; any package listed in the provided file will be omitted from all resolved environments.

@@ -4390,9 +4414,11 @@ uv pip sync [OPTIONS] ...

See --project to only change the project root directory.

May also be set with the UV_WORKING_DIRECTORY environment variable.

--dry-run

Perform a dry run, i.e., don't actually install anything but resolve the dependencies and print the resulting plan

--exclude-newer exclude-newer

Limit candidate packages to those that were uploaded prior to the given date.

-

Accepts both RFC 3339 timestamps (e.g., 2006-12-02T02:07:43Z) and local dates in the same format (e.g., 2006-12-02) in your system's configured time zone.

+

Accepts RFC 3339 timestamps (e.g., 2006-12-02T02:07:43Z), local dates in the same format (e.g., 2006-12-02) which use your system's configured time zone, and relative durations (e.g., 24 hours, 1 week, 30 days).

+

Relative durations do not respect semantics of the local time zone and are always resolved to a fixed number of seconds assuming that a day is 24 hours (i.e., DST transitions are ignored). Calendar units such as months and years are not allowed.

May also be set with the UV_EXCLUDE_NEWER environment variable.

--exclude-newer-package exclude-newer-package

Limit candidate packages for specific packages to those that were uploaded prior to the given date.

-

Accepts package-date pairs in the format PACKAGE=DATE, where DATE is an RFC 3339 timestamp (e.g., 2006-12-02T02:07:43Z) or local date (e.g., 2006-12-02) in your system's configured time zone.

+

Accepts package-date pairs in the format PACKAGE=DATE, where DATE is an RFC 3339 timestamp (e.g., 2006-12-02T02:07:43Z), a local date in the same format (e.g., 2006-12-02) which use your system's configured time zone, or relative duration (e.g., 24 hours, 1 week, 30 days).

+

Relative durations do not respect semantics of the local time zone and are always resolved to a fixed number of seconds assuming that a day is 24 hours (i.e., DST transitions are ignored). Calendar units such as months and years are not allowed.

Can be provided multiple times for different packages.

--extra extra

Include optional dependencies from the specified extra name; may be provided more than once.

Only applies to pylock.toml, pyproject.toml, setup.py, and setup.cfg sources.

@@ -4666,9 +4692,11 @@ uv pip install [OPTIONS] |--editable
--exact

Perform an exact sync, removing extraneous packages.

By default, installing will make the minimum necessary changes to satisfy the requirements. When enabled, uv will update the environment to exactly match the requirements, removing packages that are not included in the requirements.

--exclude-newer exclude-newer

Limit candidate packages to those that were uploaded prior to the given date.

-

Accepts both RFC 3339 timestamps (e.g., 2006-12-02T02:07:43Z) and local dates in the same format (e.g., 2006-12-02) in your system's configured time zone.

+

Accepts RFC 3339 timestamps (e.g., 2006-12-02T02:07:43Z), local dates in the same format (e.g., 2006-12-02) which use your system's configured time zone, and relative durations (e.g., 24 hours, 1 week, 30 days).

+

Relative durations do not respect semantics of the local time zone and are always resolved to a fixed number of seconds assuming that a day is 24 hours (i.e., DST transitions are ignored). Calendar units such as months and years are not allowed.

May also be set with the UV_EXCLUDE_NEWER environment variable.

--exclude-newer-package exclude-newer-package

Limit candidate packages for specific packages to those that were uploaded prior to the given date.

-

Accepts package-date pairs in the format PACKAGE=DATE, where DATE is an RFC 3339 timestamp (e.g., 2006-12-02T02:07:43Z) or local date (e.g., 2006-12-02) in your system's configured time zone.

+

Accepts package-date pairs in the format PACKAGE=DATE, where DATE is an RFC 3339 timestamp (e.g., 2006-12-02T02:07:43Z), a local date in the same format (e.g., 2006-12-02) which use your system's configured time zone, or relative duration (e.g., 24 hours, 1 week, 30 days).

+

Relative durations do not respect semantics of the local time zone and are always resolved to a fixed number of seconds assuming that a day is 24 hours (i.e., DST transitions are ignored). Calendar units such as months and years are not allowed.

Can be provided multiple times for different packages.

--excludes, --exclude excludes

Exclude packages from resolution using the given requirements files.

Excludes files are requirements.txt-like files that specify packages to exclude from the resolution. When a package is excluded, it will be omitted from the dependency list entirely and its own dependencies will be ignored during the resolution phase. Excludes are unconditional in that requirement specifiers and markers are ignored; any package listed in the provided file will be omitted from all resolved environments.

@@ -5113,7 +5141,8 @@ uv pip list [OPTIONS]
--exclude exclude

Exclude the specified package(s) from the output

--exclude-editable

Exclude any editable packages from output

--exclude-newer exclude-newer

Limit candidate packages to those that were uploaded prior to the given date.

-

Accepts both RFC 3339 timestamps (e.g., 2006-12-02T02:07:43Z) and local dates in the same format (e.g., 2006-12-02) in your system's configured time zone.

+

Accepts RFC 3339 timestamps (e.g., 2006-12-02T02:07:43Z), local dates in the same format (e.g., 2006-12-02) which use your system's configured time zone, and relative durations (e.g., 24 hours, 1 week, 30 days).

+

Relative durations do not respect semantics of the local time zone and are always resolved to a fixed number of seconds assuming that a day is 24 hours (i.e., DST transitions are ignored). Calendar units such as months and years are not allowed.

May also be set with the UV_EXCLUDE_NEWER environment variable.

--extra-index-url extra-index-url

(Deprecated: use --index instead) Extra URLs of package indexes to use, in addition to --index-url.

Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

All indexes provided via this flag take priority over the index specified by --index-url (which defaults to PyPI). When multiple --extra-index-url flags are provided, earlier values take priority.

@@ -5293,7 +5322,8 @@ uv pip tree [OPTIONS]

Relative paths are resolved with the given directory as the base.

See --project to only change the project root directory.

May also be set with the UV_WORKING_DIRECTORY environment variable.

--exclude-newer exclude-newer

Limit candidate packages to those that were uploaded prior to the given date.

-

Accepts both RFC 3339 timestamps (e.g., 2006-12-02T02:07:43Z) and local dates in the same format (e.g., 2006-12-02) in your system's configured time zone.

+

Accepts RFC 3339 timestamps (e.g., 2006-12-02T02:07:43Z), local dates in the same format (e.g., 2006-12-02) which use your system's configured time zone, and relative durations (e.g., 24 hours, 1 week, 30 days).

+

Relative durations do not respect semantics of the local time zone and are always resolved to a fixed number of seconds assuming that a day is 24 hours (i.e., DST transitions are ignored). Calendar units such as months and years are not allowed.

May also be set with the UV_EXCLUDE_NEWER environment variable.

--extra-index-url extra-index-url

(Deprecated: use --index instead) Extra URLs of package indexes to use, in addition to --index-url.

Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

All indexes provided via this flag take priority over the index specified by --index-url (which defaults to PyPI). When multiple --extra-index-url flags are provided, earlier values take priority.

@@ -5539,9 +5569,11 @@ uv venv [OPTIONS] [PATH]

Relative paths are resolved with the given directory as the base.

See --project to only change the project root directory.

May also be set with the UV_WORKING_DIRECTORY environment variable.

--exclude-newer exclude-newer

Limit candidate packages to those that were uploaded prior to the given date.

-

Accepts both RFC 3339 timestamps (e.g., 2006-12-02T02:07:43Z) and local dates in the same format (e.g., 2006-12-02) in your system's configured time zone.

+

Accepts RFC 3339 timestamps (e.g., 2006-12-02T02:07:43Z), local dates in the same format (e.g., 2006-12-02) which use your system's configured time zone, and relative durations (e.g., 24 hours, 1 week, 30 days).

+

Relative durations do not respect semantics of the local time zone and are always resolved to a fixed number of seconds assuming that a day is 24 hours (i.e., DST transitions are ignored). Calendar units such as months and years are not allowed.

May also be set with the UV_EXCLUDE_NEWER environment variable.

--exclude-newer-package exclude-newer-package

Limit candidate packages for a specific package to those that were uploaded prior to the given date.

-

Accepts package-date pairs in the format PACKAGE=DATE, where DATE is an RFC 3339 timestamp (e.g., 2006-12-02T02:07:43Z) or local date (e.g., 2006-12-02) in your system's configured time zone.

+

Accepts package-date pairs in the format PACKAGE=DATE, where DATE is an RFC 3339 timestamp (e.g., 2006-12-02T02:07:43Z), a local date in the same format (e.g., 2006-12-02) which use your system's configured time zone, or relative duration (e.g., 24 hours, 1 week, 30 days).

+

Relative durations do not respect semantics of the local time zone and are always resolved to a fixed number of seconds assuming that a day is 24 hours (i.e., DST transitions are ignored). Calendar units such as months and years are not allowed.

Can be provided multiple times for different packages.

--extra-index-url extra-index-url

(Deprecated: use --index instead) Extra URLs of package indexes to use, in addition to --index-url.

Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

@@ -5687,9 +5719,11 @@ uv build [OPTIONS] [SRC]

Relative paths are resolved with the given directory as the base.

See --project to only change the project root directory.

May also be set with the UV_WORKING_DIRECTORY environment variable.

--exclude-newer exclude-newer

Limit candidate packages to those that were uploaded prior to the given date.

-

Accepts both RFC 3339 timestamps (e.g., 2006-12-02T02:07:43Z) and local dates in the same format (e.g., 2006-12-02) in your system's configured time zone.

-

May also be set with the UV_EXCLUDE_NEWER environment variable.

--exclude-newer-package exclude-newer-package

Limit candidate packages for a specific package to those that were uploaded prior to the given date.

-

Accepts package-date pairs in the format PACKAGE=DATE, where DATE is an RFC 3339 timestamp (e.g., 2006-12-02T02:07:43Z) or local date (e.g., 2006-12-02) in your system's configured time zone.

+

Accepts RFC 3339 timestamps (e.g., 2006-12-02T02:07:43Z), local dates in the same format (e.g., 2006-12-02) which use your system's configured time zone, and relative durations (e.g., 24 hours, 1 week, 30 days).

+

Relative durations do not respect semantics of the local time zone and are always resolved to a fixed number of seconds assuming that a day is 24 hours (i.e., DST transitions are ignored). Calendar units such as months and years are not allowed.

+

May also be set with the UV_EXCLUDE_NEWER environment variable.

--exclude-newer-package exclude-newer-package

Limit candidate packages for specific packages to those that were uploaded prior to the given date.

+

Accepts package-date pairs in the format PACKAGE=DATE, where DATE is an RFC 3339 timestamp (e.g., 2006-12-02T02:07:43Z), a local date in the same format (e.g., 2006-12-02) which use your system's configured time zone, or relative duration (e.g., 24 hours, 1 week, 30 days).

+

Relative durations do not respect semantics of the local time zone and are always resolved to a fixed number of seconds assuming that a day is 24 hours (i.e., DST transitions are ignored). Calendar units such as months and years are not allowed.

Can be provided multiple times for different packages.

--extra-index-url extra-index-url

(Deprecated: use --index instead) Extra URLs of package indexes to use, in addition to --index-url.

Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

diff --git a/uv.schema.json b/uv.schema.json index 7cd16427aab85..c9606c0109fb3 100644 --- a/uv.schema.json +++ b/uv.schema.json @@ -892,7 +892,7 @@ } }, "ExcludeNewerTimestamp": { - "description": "Exclude distributions uploaded after the given timestamp.\n\nAccepts both RFC 3339 timestamps (e.g., `2006-12-02T02:07:43Z`) and local dates in the same format (e.g., `2006-12-02`).", + "description": "Exclude distributions uploaded after the given timestamp.\n\nAccepts both RFC 3339 timestamps (e.g., `2006-12-02T02:07:43Z`) and local dates in the same format (e.g., `2006-12-02`), as well as relative durations (e.g., `1 week`, `30 days`, `6 months`). Relative durations are resolved to an absolute timestamp at lock time.", "type": "string", "pattern": "^\\d{4}-\\d{2}-\\d{2}(T\\d{2}:\\d{2}:\\d{2}(Z|[+-]\\d{2}:\\d{2}))?$" },