diff --git a/crates/uv-resolver/src/exclude_newer.rs b/crates/uv-resolver/src/exclude_newer.rs index 913b3f22d..6338f0094 100644 --- a/crates/uv-resolver/src/exclude_newer.rs +++ b/crates/uv-resolver/src/exclude_newer.rs @@ -7,6 +7,8 @@ use std::{ use jiff::{Span, Timestamp, ToSpan, Unit, tz::TimeZone}; use rustc_hash::FxHashMap; +use serde::Deserialize; +use serde::de::value::MapAccessDeserializer; use uv_normalize::PackageName; #[derive(Debug, Clone, PartialEq, Eq)] @@ -95,9 +97,9 @@ impl std::fmt::Display for ExcludeNewerChange { #[derive(Debug, Clone, PartialEq, Eq)] pub enum ExcludeNewerPackageChange { - PackageAdded(PackageName, ExcludeNewerValue), + PackageAdded(PackageName, PackageExcludeNewer), PackageRemoved(PackageName), - PackageChanged(PackageName, ExcludeNewerValueChange), + PackageChanged(PackageName, Box), } impl ExcludeNewerPackageChange { @@ -112,18 +114,23 @@ impl ExcludeNewerPackageChange { impl std::fmt::Display for ExcludeNewerPackageChange { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Self::PackageAdded(name, value) => { + Self::PackageAdded(name, PackageExcludeNewer::Enabled(value)) => { write!( f, - "addition of exclude newer `{value}` for package `{name}`" + "addition of exclude newer `{}` for package `{name}`", + value.as_ref() + ) + } + Self::PackageAdded(name, PackageExcludeNewer::Disabled) => { + write!( + f, + "addition of exclude newer exclusion for package `{name}`" ) } Self::PackageRemoved(name) => { write!(f, "removal of exclude newer for package `{name}`") } - Self::PackageChanged(name, change) => { - write!(f, "{change} for package `{name}`") - } + Self::PackageChanged(name, change) => write!(f, "{change} for package `{name}`"), } } } @@ -255,7 +262,7 @@ impl ExcludeNewerValue { self.span.as_ref() } - /// Create a new [`ExcludeNewerTimestamp`]. + /// Create a new [`ExcludeNewerValue`]. pub fn new(timestamp: Timestamp, span: Option) -> Self { Self { timestamp, span } } @@ -326,7 +333,7 @@ fn format_exclude_newer_error( impl FromStr for ExcludeNewerValue { type Err = String; - /// Parse an [`ExcludeNewerTimestamp`] from a string. + /// Parse an [`ExcludeNewerValue`] from a string. /// /// Accepts RFC 3339 timestamps (e.g., `2006-12-02T02:07:43Z`), local dates in the same format /// (e.g., `2006-12-02`), "friendly" durations (e.g., `1 week`, `30 days`), and ISO 8601 @@ -425,47 +432,170 @@ impl std::fmt::Display for ExcludeNewerValue { } } +/// Per-package exclude-newer setting. +/// +/// This enum represents whether exclude-newer should be disabled for a package, +/// or if a specific cutoff (absolute or relative) should be used. +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +pub enum PackageExcludeNewer { + /// Disable exclude-newer for this package (allow all versions regardless of upload date). + Disabled, + /// Enable exclude-newer with this cutoff for this package. + Enabled(Box), +} + /// A package-specific exclude-newer entry. #[derive(Debug, Clone, PartialEq, Eq)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub struct ExcludeNewerPackageEntry { pub package: PackageName, - pub timestamp: ExcludeNewerValue, + pub setting: PackageExcludeNewer, } impl FromStr for ExcludeNewerPackageEntry { type Err = String; - /// Parses a [`ExcludeNewerPackageEntry`] from a string in the format `PACKAGE=DATE`. + /// Parses a [`ExcludeNewerPackageEntry`] from a string in the format `PACKAGE=DATE` or `PACKAGE=false`. fn from_str(s: &str) -> Result { - let Some((package, date)) = s.split_once('=') else { + let Some((package, value)) = s.split_once('=') else { return Err(format!( - "Invalid `exclude-newer-package` value `{s}`: expected format `PACKAGE=DATE`" + "Invalid `exclude-newer-package` value `{s}`: expected format `PACKAGE=DATE` or `PACKAGE=false`" )); }; let package = PackageName::from_str(package).map_err(|err| { format!("Invalid `exclude-newer-package` package name `{package}`: {err}") })?; - let timestamp = ExcludeNewerValue::from_str(date) - .map_err(|err| format!("Invalid `exclude-newer-package` timestamp `{date}`: {err}"))?; - Ok(Self { package, timestamp }) + let setting = if value == "false" { + PackageExcludeNewer::Disabled + } else { + PackageExcludeNewer::Enabled(Box::new(ExcludeNewerValue::from_str(value).map_err( + |err| format!("Invalid `exclude-newer-package` value `{value}`: {err}"), + )?)) + }; + + Ok(Self { package, setting }) + } +} + +impl From<(PackageName, PackageExcludeNewer)> for ExcludeNewerPackageEntry { + fn from((package, setting): (PackageName, PackageExcludeNewer)) -> Self { + Self { package, setting } } } impl From<(PackageName, ExcludeNewerValue)> for ExcludeNewerPackageEntry { fn from((package, timestamp): (PackageName, ExcludeNewerValue)) -> Self { - Self { package, timestamp } + Self { + package, + setting: PackageExcludeNewer::Enabled(Box::new(timestamp)), + } + } +} + +impl<'de> serde::Deserialize<'de> for PackageExcludeNewer { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct Visitor; + + impl<'de> serde::de::Visitor<'de> for Visitor { + type Value = PackageExcludeNewer; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str( + "a date/timestamp/duration string, false to disable exclude-newer, or a table \ + with timestamp/span", + ) + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + ExcludeNewerValue::from_str(v) + .map(|ts| PackageExcludeNewer::Enabled(Box::new(ts))) + .map_err(|e| E::custom(format!("failed to parse exclude-newer value: {e}"))) + } + + fn visit_bool(self, v: bool) -> Result + where + E: serde::de::Error, + { + if v { + Err(E::custom( + "expected false to disable exclude-newer, got true", + )) + } else { + Ok(PackageExcludeNewer::Disabled) + } + } + + fn visit_map(self, map: A) -> Result + where + A: serde::de::MapAccess<'de>, + { + Ok(PackageExcludeNewer::Enabled(Box::new( + ExcludeNewerValue::deserialize(MapAccessDeserializer::new(map))?, + ))) + } + } + + deserializer.deserialize_any(Visitor) + } +} + +impl serde::Serialize for PackageExcludeNewer { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + match self { + Self::Enabled(timestamp) => timestamp.to_string().serialize(serializer), + Self::Disabled => serializer.serialize_bool(false), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum PackageExcludeNewerChange { + Disabled { was: ExcludeNewerValue }, + Enabled { now: ExcludeNewerValue }, + TimestampChanged(ExcludeNewerValueChange), +} + +impl PackageExcludeNewerChange { + pub fn is_relative_timestamp_change(&self) -> bool { + match self { + Self::Disabled { .. } | Self::Enabled { .. } => false, + Self::TimestampChanged(change) => change.is_relative_timestamp_change(), + } + } +} + +impl std::fmt::Display for PackageExcludeNewerChange { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Disabled { was } => { + write!(f, "add exclude newer exclusion (was `{was}`)") + } + Self::Enabled { now } => { + write!(f, "remove exclude newer exclusion (now `{now}`)") + } + Self::TimestampChanged(change) => write!(f, "{change}"), + } } } #[derive(Debug, Clone, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct ExcludeNewerPackage(FxHashMap); +pub struct ExcludeNewerPackage(FxHashMap); impl Deref for ExcludeNewerPackage { - type Target = FxHashMap; + type Target = FxHashMap; fn deref(&self) -> &Self::Target { &self.0 @@ -482,15 +612,15 @@ impl FromIterator for ExcludeNewerPackage { fn from_iter>(iter: T) -> Self { Self( iter.into_iter() - .map(|entry| (entry.package, entry.timestamp)) + .map(|entry| (entry.package, entry.setting)) .collect(), ) } } impl IntoIterator for ExcludeNewerPackage { - type Item = (PackageName, ExcludeNewerValue); - type IntoIter = std::collections::hash_map::IntoIter; + type Item = (PackageName, PackageExcludeNewer); + type IntoIter = std::collections::hash_map::IntoIter; fn into_iter(self) -> Self::IntoIter { self.0.into_iter() @@ -498,8 +628,8 @@ impl IntoIterator for ExcludeNewerPackage { } impl<'a> IntoIterator for &'a ExcludeNewerPackage { - type Item = (&'a PackageName, &'a ExcludeNewerValue); - type IntoIter = std::collections::hash_map::Iter<'a, PackageName, ExcludeNewerValue>; + type Item = (&'a PackageName, &'a PackageExcludeNewer); + type IntoIter = std::collections::hash_map::Iter<'a, PackageName, PackageExcludeNewer>; fn into_iter(self) -> Self::IntoIter { self.0.iter() @@ -508,20 +638,55 @@ impl<'a> IntoIterator for &'a ExcludeNewerPackage { impl ExcludeNewerPackage { /// Convert to the inner `HashMap`. - pub fn into_inner(self) -> FxHashMap { + pub fn into_inner(self) -> FxHashMap { self.0 } + /// Returns true if this map is empty (no package-specific settings). + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + pub fn compare(&self, other: &Self) -> Option { - for (package, timestamp) in self { - let Some(other_timestamp) = other.get(package) else { - return Some(ExcludeNewerPackageChange::PackageRemoved(package.clone())); - }; - if let Some(change) = timestamp.compare(other_timestamp) { - return Some(ExcludeNewerPackageChange::PackageChanged( - package.clone(), - change, - )); + for (package, setting) in self { + match (setting, other.get(package)) { + ( + PackageExcludeNewer::Enabled(self_timestamp), + Some(PackageExcludeNewer::Enabled(other_timestamp)), + ) => { + if let Some(change) = self_timestamp.compare(other_timestamp) { + return Some(ExcludeNewerPackageChange::PackageChanged( + package.clone(), + Box::new(PackageExcludeNewerChange::TimestampChanged(change)), + )); + } + } + ( + PackageExcludeNewer::Enabled(self_timestamp), + Some(PackageExcludeNewer::Disabled), + ) => { + return Some(ExcludeNewerPackageChange::PackageChanged( + package.clone(), + Box::new(PackageExcludeNewerChange::Disabled { + was: self_timestamp.as_ref().clone(), + }), + )); + } + ( + PackageExcludeNewer::Disabled, + Some(PackageExcludeNewer::Enabled(other_timestamp)), + ) => { + return Some(ExcludeNewerPackageChange::PackageChanged( + package.clone(), + Box::new(PackageExcludeNewerChange::Enabled { + now: other_timestamp.as_ref().clone(), + }), + )); + } + (PackageExcludeNewer::Disabled, Some(PackageExcludeNewer::Disabled)) => {} + (_, None) => { + return Some(ExcludeNewerPackageChange::PackageRemoved(package.clone())); + } } } @@ -574,12 +739,16 @@ impl ExcludeNewer { Self { global, package } } - /// Returns the timestamp for a specific package, falling back to the global timestamp if set. + /// Returns the exclude-newer value for a specific package, returning `Some(value)` if the + /// package has a package-specific setting or falls back to the global value if set, or `None` + /// if exclude-newer is explicitly disabled for the package (set to `false`) or if no + /// exclude-newer is configured. pub fn exclude_newer_package(&self, package_name: &PackageName) -> Option { - self.package - .get(package_name) - .cloned() - .or(self.global.clone()) + match self.package.get(package_name) { + Some(PackageExcludeNewer::Enabled(timestamp)) => Some(timestamp.as_ref().clone()), + Some(PackageExcludeNewer::Disabled) => None, + None => self.global.clone(), + } } /// Returns true if this has any configuration (global or per-package). @@ -615,11 +784,18 @@ impl std::fmt::Display for ExcludeNewer { } } let mut first = true; - for (name, timestamp) in &self.package { + for (name, setting) in &self.package { if !first { write!(f, ", ")?; } - write!(f, "{name}: {timestamp}")?; + match setting { + PackageExcludeNewer::Enabled(timestamp) => { + write!(f, "{name}: {}", timestamp.as_ref())?; + } + PackageExcludeNewer::Disabled => { + write!(f, "{name}: disabled")?; + } + } first = false; } Ok(()) diff --git a/crates/uv-resolver/src/lib.rs b/crates/uv-resolver/src/lib.rs index d57f8f097..756f2e57b 100644 --- a/crates/uv-resolver/src/lib.rs +++ b/crates/uv-resolver/src/lib.rs @@ -2,7 +2,8 @@ pub use dependency_mode::DependencyMode; pub use error::{ErrorTree, NoSolutionError, NoSolutionHeader, ResolveError, SentinelRange}; pub use exclude_newer::{ ExcludeNewer, ExcludeNewerChange, ExcludeNewerPackage, ExcludeNewerPackageChange, - ExcludeNewerPackageEntry, ExcludeNewerValue, ExcludeNewerValueChange, + ExcludeNewerPackageEntry, ExcludeNewerValue, ExcludeNewerValueChange, PackageExcludeNewer, + PackageExcludeNewerChange, }; pub use exclusions::Exclusions; pub use flat_index::{FlatDistributions, FlatIndex}; diff --git a/crates/uv-resolver/src/lock/mod.rs b/crates/uv-resolver/src/lock/mod.rs index 3ef583155..dc0d0f44a 100644 --- a/crates/uv-resolver/src/lock/mod.rs +++ b/crates/uv-resolver/src/lock/mod.rs @@ -61,7 +61,7 @@ use crate::resolution::{AnnotatedDist, ResolutionGraphNode}; use crate::universal_marker::{ConflictMarker, UniversalMarker}; use crate::{ ExcludeNewer, ExcludeNewerPackage, ExcludeNewerValue, InMemoryIndex, MetadataResponse, - PrereleaseMode, ResolutionMode, ResolverOutput, + PackageExcludeNewer, PrereleaseMode, ResolutionMode, ResolverOutput, }; mod export; @@ -1071,20 +1071,29 @@ impl Lock { // Serialize package-specific exclusions as a separate field if !exclude_newer.package.is_empty() { let mut package_table = toml_edit::Table::new(); - for (name, exclude_newer_value) in &exclude_newer.package { - if let Some(span) = exclude_newer_value.span() { - // Serialize as inline table with timestamp and span - let mut inline = toml_edit::InlineTable::new(); - inline.insert( - "timestamp", - exclude_newer_value.timestamp().to_string().into(), - ); - inline.insert("span", span.to_string().into()); - package_table.insert(name.as_ref(), Item::Value(inline.into())); - } else { - // Serialize as simple string - package_table - .insert(name.as_ref(), value(exclude_newer_value.to_string())); + for (name, setting) in &exclude_newer.package { + match setting { + PackageExcludeNewer::Enabled(exclude_newer_value) => { + if let Some(span) = exclude_newer_value.span() { + // Serialize as inline table with timestamp and span + let mut inline = toml_edit::InlineTable::new(); + inline.insert( + "timestamp", + exclude_newer_value.timestamp().to_string().into(), + ); + inline.insert("span", span.to_string().into()); + package_table.insert(name.as_ref(), Item::Value(inline.into())); + } else { + // Serialize as simple string + package_table.insert( + name.as_ref(), + value(exclude_newer_value.to_string()), + ); + } + } + PackageExcludeNewer::Disabled => { + package_table.insert(name.as_ref(), value(false)); + } } } options_table.insert("exclude-newer-package", Item::Table(package_table)); diff --git a/crates/uv-settings/src/combine.rs b/crates/uv-settings/src/combine.rs index 15a63e1dc..8e3f46152 100644 --- a/crates/uv-settings/src/combine.rs +++ b/crates/uv-settings/src/combine.rs @@ -228,9 +228,11 @@ impl Combine for ExcludeNewer { if self.package.is_empty() { self.package = other.package; } else { - // Merge package-specific timestamps, with self taking precedence - for (pkg, timestamp) in &other.package { - self.package.entry(pkg.clone()).or_insert(timestamp.clone()); + // Merge package-specific settings, with self taking precedence + for (pkg, setting) in &other.package { + self.package + .entry(pkg.clone()) + .or_insert_with(|| setting.clone()); } } } diff --git a/crates/uv/tests/it/lock.rs b/crates/uv/tests/it/lock.rs index 6c9bd7858..b608cd29a 100644 --- a/crates/uv/tests/it/lock.rs +++ b/crates/uv/tests/it/lock.rs @@ -31179,6 +31179,94 @@ fn test_tilde_equals_python_version() -> Result<()> { Ok(()) } +/// Test that exclude-newer-package can be disabled for specific packages using `false`. +#[test] +fn lock_exclude_newer_package_disable() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["idna", "iniconfig"] + "#, + )?; + + // Lock with global exclude-newer and disable it for idna + uv_snapshot!(context.filters(), context + .lock() + .env_remove(EnvVars::UV_EXCLUDE_NEWER) + .arg("--exclude-newer") + .arg("2022-04-04T12:00:00Z") + .arg("--exclude-newer-package") + .arg("idna=false"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 3 packages in [TIME] + "###); + + let lock = context.read("uv.lock"); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + lock, @r#" + version = 1 + revision = 3 + requires-python = ">=3.12" + + [options] + exclude-newer = "2022-04-04T12:00:00Z" + + [options.exclude-newer-package] + idna = false + + [[package]] + name = "idna" + version = "3.11" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } + wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, + ] + + [[package]] + name = "iniconfig" + version = "1.1.1" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/23/a2/97899f6bd0e873fed3a7e67ae8d3a08b21799430fb4da15cfedf10d6e2c2/iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32", size = 8104, upload-time = "2020-10-14T10:20:18.572Z" } + wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/dd/b3c12c6d707058fa947864b67f0c4e0c39ef8610988d7baea9578f3c48f3/iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3", size = 4990, upload-time = "2020-10-16T17:37:23.05Z" }, + ] + + [[package]] + name = "project" + version = "0.1.0" + source = { virtual = "." } + dependencies = [ + { name = "idna" }, + { name = "iniconfig" }, + ] + + [package.metadata] + requires-dist = [ + { name = "idna" }, + { name = "iniconfig" }, + ] + "# + ); + }); + + Ok(()) +} + /// Test that exclude-newer-package is properly serialized in the lockfile. #[test] fn lock_exclude_newer_package() -> Result<()> { diff --git a/crates/uv/tests/it/pip_compile.rs b/crates/uv/tests/it/pip_compile.rs index 9d01ae9b3..e22239fad 100644 --- a/crates/uv/tests/it/pip_compile.rs +++ b/crates/uv/tests/it/pip_compile.rs @@ -3565,7 +3565,7 @@ fn compile_exclude_newer_package_errors() -> Result<()> { ----- stdout ----- ----- stderr ----- - error: invalid value 'tqdm' for '--exclude-newer-package ': Invalid `exclude-newer-package` value `tqdm`: expected format `PACKAGE=DATE` + error: invalid value 'tqdm' for '--exclude-newer-package ': Invalid `exclude-newer-package` value `tqdm`: expected format `PACKAGE=DATE` or `PACKAGE=false` For more information, try '--help'. " @@ -3583,7 +3583,7 @@ fn compile_exclude_newer_package_errors() -> Result<()> { ----- stdout ----- ----- stderr ----- - error: invalid value 'tqdm=invalid-date' for '--exclude-newer-package ': Invalid `exclude-newer-package` timestamp `invalid-date`: `invalid-date` could not be parsed as a valid exclude-newer value (expected a date like `2024-01-01`, a timestamp like `2024-01-01T00:00:00Z`, or a duration like `3 days` or `P3D`) + error: invalid value 'tqdm=invalid-date' for '--exclude-newer-package ': Invalid `exclude-newer-package` value `invalid-date`: `invalid-date` could not be parsed as a valid exclude-newer value (expected a date like `2024-01-01`, a timestamp like `2024-01-01T00:00:00Z`, or a duration like `3 days` or `P3D`) For more information, try '--help'. " diff --git a/docs/concepts/resolution.md b/docs/concepts/resolution.md index 2f60f4b37..c26fc110a 100644 --- a/docs/concepts/resolution.md +++ b/docs/concepts/resolution.md @@ -658,8 +658,8 @@ configured time zone. The package index must support the `upload-time` field as specified in [`PEP 700`](https://peps.python.org/pep-0700/). If the field is not present for a given - distribution, the distribution will be treated as unavailable. PyPI provides `upload-time` for - all packages. + distribution, the distribution will be treated as unavailable unless the package is opted out + via `--exclude-newer-package =false`. PyPI provides `upload-time` for all packages. To ensure reproducibility, messages for unsatisfiable resolutions will not mention that distributions were excluded due to the `--exclude-newer` flag — newer distributions will be treated @@ -689,6 +689,9 @@ Values may also be specified for specific packages, e.g., exclude-newer-package = { setuptools = "2006-12-02T02:07:43Z" } ``` +The same flag also accepts `=false` to opt a package out of the `--exclude-newer` +restriction, e.g., to allow resolving packages from an index that does not publish upload times. + Package-specific values will take precedence over global values. ## Dependency cooldowns diff --git a/uv.schema.json b/uv.schema.json index 78784098a..58cf3d6be 100644 --- a/uv.schema.json +++ b/uv.schema.json @@ -899,7 +899,7 @@ "ExcludeNewerPackage": { "type": "object", "additionalProperties": { - "$ref": "#/definitions/ExcludeNewerTimestamp" + "$ref": "#/definitions/PackageExcludeNewer" } }, "ExcludeNewerTimestamp": { @@ -1213,6 +1213,29 @@ "$ref": "#/definitions/ConfigSettings" } }, + "PackageExcludeNewer": { + "description": "Per-package exclude-newer setting.\n\nThis enum represents whether exclude-newer should be disabled for a package,\nor if a specific cutoff (absolute or relative) should be used.", + "oneOf": [ + { + "description": "Disable exclude-newer for this package (allow all versions regardless of upload date).", + "type": "string", + "const": "Disabled" + }, + { + "description": "Enable exclude-newer with this cutoff for this package.", + "type": "object", + "properties": { + "Enabled": { + "$ref": "#/definitions/ExcludeNewerTimestamp" + } + }, + "additionalProperties": false, + "required": [ + "Enabled" + ] + } + ] + }, "PackageName": { "description": "The normalized name of a package.\n\nConverts the name to lowercase and collapses runs of `-`, `_`, and `.` down to a single `-`.\nFor example, `---`, `.`, and `__` are all converted to a single `-`.\n\nSee: ", "type": "string"