Add exclude-newer to [[tool.uv.index]] (#18839)

## Summary

This PR enables users to set an `exclude-newer` override on a per-index
basis.

The priority is such that global `exclude-newer-package` has highest
priority, followed by `exclude-newer` on an index, followed by global
`exclude-newer`.

Closes https://github.com/astral-sh/uv/issues/16813.
This commit is contained in:
Charlie Marsh
2026-04-08 14:52:40 -04:00
committed by GitHub
parent 7924ba5b14
commit 39b83c30e0
22 changed files with 607 additions and 58 deletions
+76
View File
@@ -10,6 +10,7 @@ use uv_auth::{AuthPolicy, Credentials};
use uv_redacted::DisplaySafeUrl;
use uv_small_str::SmallString;
use crate::exclude_newer::ExcludeNewerOverride;
use crate::index_name::{IndexName, IndexNameError};
use crate::origin::Origin;
use crate::{IndexStatusCodeStrategy, IndexUrl, IndexUrlError, SerializableStatusCode};
@@ -229,6 +230,29 @@ pub struct Index {
/// ```
#[serde(default)]
pub cache_control: Option<IndexCacheControl>,
/// An index-specific `exclude-newer` cutoff.
///
/// Accepts the same date, timestamp, and duration values as the global `exclude-newer`
/// setting. Set this to `false` to disable `exclude-newer` for this index entirely.
///
/// When set to a value, packages resolved from this index will use that cutoff instead of the
/// globally-specified value, unless a package-specific `exclude-newer-package` override is
/// present.
///
/// This option is in preview and may change in any future release.
///
/// ```toml
/// [tool.uv]
/// exclude-newer = "2025-01-01T00:00:00Z"
///
/// [[tool.uv.index]]
/// name = "internal"
/// url = "https://internal.example.com/simple"
/// exclude-newer = "7 days"
/// ```
#[serde(default, skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "schemars", schemars(with = "ExcludeNewerOverride"))]
pub exclude_newer: Option<ExcludeNewerOverride>,
}
impl PartialEq for Index {
@@ -244,6 +268,7 @@ impl PartialEq for Index {
authenticate,
ignore_error_codes,
cache_control,
exclude_newer,
} = self;
*url == other.url
&& *name == other.name
@@ -254,6 +279,7 @@ impl PartialEq for Index {
&& *authenticate == other.authenticate
&& *ignore_error_codes == other.ignore_error_codes
&& *cache_control == other.cache_control
&& *exclude_newer == other.exclude_newer
}
}
@@ -278,6 +304,7 @@ impl Ord for Index {
authenticate,
ignore_error_codes,
cache_control,
exclude_newer,
} = self;
url.cmp(&other.url)
.then_with(|| name.cmp(&other.name))
@@ -288,6 +315,7 @@ impl Ord for Index {
.then_with(|| authenticate.cmp(&other.authenticate))
.then_with(|| ignore_error_codes.cmp(&other.ignore_error_codes))
.then_with(|| cache_control.cmp(&other.cache_control))
.then_with(|| exclude_newer.cmp(&other.exclude_newer))
}
}
@@ -304,6 +332,7 @@ impl std::hash::Hash for Index {
authenticate,
ignore_error_codes,
cache_control,
exclude_newer,
} = self;
url.hash(state);
name.hash(state);
@@ -314,6 +343,7 @@ impl std::hash::Hash for Index {
authenticate.hash(state);
ignore_error_codes.hash(state);
cache_control.hash(state);
exclude_newer.hash(state);
}
}
@@ -354,6 +384,7 @@ impl Index {
authenticate: AuthPolicy::default(),
ignore_error_codes: None,
cache_control: None,
exclude_newer: None,
}
}
@@ -370,6 +401,7 @@ impl Index {
authenticate: AuthPolicy::default(),
ignore_error_codes: None,
cache_control: None,
exclude_newer: None,
}
}
@@ -386,6 +418,7 @@ impl Index {
authenticate: AuthPolicy::default(),
ignore_error_codes: None,
cache_control: None,
exclude_newer: None,
}
}
@@ -478,6 +511,11 @@ impl Index {
.and_then(|cache_control| cache_control.api.clone())
.or_else(|| IndexCacheControl::simple_api_cache_control(self.url.url()))
}
/// Return the `exclude-newer` setting for this index.
pub fn exclude_newer(&self) -> Option<&ExcludeNewerOverride> {
self.exclude_newer.as_ref()
}
}
impl From<IndexUrl> for Index {
@@ -493,6 +531,7 @@ impl From<IndexUrl> for Index {
authenticate: AuthPolicy::default(),
ignore_error_codes: None,
cache_control: None,
exclude_newer: None,
}
}
}
@@ -517,6 +556,7 @@ impl FromStr for Index {
authenticate: AuthPolicy::default(),
ignore_error_codes: None,
cache_control: None,
exclude_newer: None,
});
}
}
@@ -534,6 +574,7 @@ impl FromStr for Index {
authenticate: AuthPolicy::default(),
ignore_error_codes: None,
cache_control: None,
exclude_newer: None,
})
}
}
@@ -638,6 +679,8 @@ struct IndexWire {
ignore_error_codes: Option<Vec<SerializableStatusCode>>,
#[serde(default)]
cache_control: Option<IndexCacheControl>,
#[serde(default)]
exclude_newer: Option<ExcludeNewerOverride>,
}
impl<'de> Deserialize<'de> for Index {
@@ -665,6 +708,7 @@ impl<'de> Deserialize<'de> for Index {
authenticate: wire.authenticate,
ignore_error_codes: wire.ignore_error_codes,
cache_control: wire.cache_control,
exclude_newer: wire.exclude_newer,
})
}
}
@@ -697,6 +741,7 @@ mod tests {
let index: Index = toml::from_str(toml_str).unwrap();
assert_eq!(index.name.as_ref().unwrap().as_ref(), "test-index");
assert!(index.cache_control.is_some());
assert_eq!(index.exclude_newer, None);
let cache_control = index.cache_control.as_ref().unwrap();
assert_eq!(
cache_control.api,
@@ -719,6 +764,7 @@ mod tests {
let index: Index = toml::from_str(toml_str).unwrap();
assert_eq!(index.name.as_ref().unwrap().as_ref(), "test-index");
assert_eq!(index.cache_control, None);
assert_eq!(index.exclude_newer, None);
}
#[test]
@@ -733,6 +779,7 @@ mod tests {
let index: Index = toml::from_str(toml_str).unwrap();
assert_eq!(index.name.as_ref().unwrap().as_ref(), "test-index");
assert!(index.cache_control.is_some());
assert_eq!(index.exclude_newer, None);
let cache_control = index.cache_control.as_ref().unwrap();
assert_eq!(
cache_control.api,
@@ -770,4 +817,33 @@ mod tests {
.contains("`cache-control.files` must be a valid HTTP header value")
);
}
#[test]
fn test_index_exclude_newer_disable() {
let toml_str = r#"
name = "internal"
url = "https://internal.example.com/simple"
exclude-newer = false
"#;
let index: Index = toml::from_str(toml_str).unwrap();
assert_eq!(index.name.as_ref().unwrap().as_ref(), "internal");
assert_eq!(index.exclude_newer, Some(ExcludeNewerOverride::Disabled));
}
#[test]
fn test_index_exclude_newer_relative() {
let toml_str = r#"
name = "internal"
url = "https://internal.example.com/simple"
exclude-newer = "7 days"
"#;
let index: Index = toml::from_str(toml_str).unwrap();
assert_eq!(index.name.as_ref().unwrap().as_ref(), "internal");
assert!(matches!(
index.exclude_newer,
Some(ExcludeNewerOverride::Enabled(_))
));
}
}
+16 -1
View File
@@ -15,7 +15,7 @@ use uv_pep508::{Scheme, VerbatimUrl, VerbatimUrlError, split_scheme};
use uv_redacted::DisplaySafeUrl;
use uv_warnings::warn_user;
use crate::{Index, IndexStatusCodeStrategy, Verbatim};
use crate::{ExcludeNewerOverride, Index, IndexStatusCodeStrategy, Verbatim};
static PYPI_URL: LazyLock<DisplaySafeUrl> =
LazyLock::new(|| DisplaySafeUrl::parse("https://pypi.org/simple").unwrap());
@@ -458,6 +458,16 @@ impl<'a> IndexLocations {
}
None
}
/// Return the `exclude-newer` setting for a given index, if the index is configured.
pub fn exclude_newer_for(&self, url: &IndexUrl) -> Option<&ExcludeNewerOverride> {
for index in &self.indexes {
if is_same_index(index.url(), url) {
return index.exclude_newer();
}
}
None
}
}
impl From<&IndexLocations> for uv_auth::Indexes {
@@ -754,6 +764,7 @@ mod tests {
publish_url: None,
authenticate: uv_auth::AuthPolicy::default(),
ignore_error_codes: None,
exclude_newer: None,
},
Index {
name: Some(IndexName::from_str("index2").unwrap()),
@@ -766,6 +777,7 @@ mod tests {
publish_url: None,
authenticate: uv_auth::AuthPolicy::default(),
ignore_error_codes: None,
exclude_newer: None,
},
];
@@ -804,6 +816,7 @@ mod tests {
publish_url: None,
authenticate: uv_auth::AuthPolicy::default(),
ignore_error_codes: None,
exclude_newer: None,
}];
let index_urls = IndexUrls::from_indexes(indexes.clone());
@@ -850,6 +863,7 @@ mod tests {
publish_url: None,
authenticate: uv_auth::AuthPolicy::default(),
ignore_error_codes: None,
exclude_newer: None,
}];
let index_urls = IndexUrls::from_indexes(indexes.clone());
@@ -892,6 +906,7 @@ mod tests {
publish_url: None,
authenticate: uv_auth::AuthPolicy::default(),
ignore_error_codes: None,
exclude_newer: None,
}];
let index_urls = IndexUrls::from_indexes(indexes.clone());
+7
View File
@@ -194,6 +194,7 @@ pub enum PreviewFeature {
PublishRequireNormalized = 1 << 25,
Audit = 1 << 26,
ProjectDirectoryMustExist = 1 << 27,
IndexExcludeNewer = 1 << 28,
}
impl PreviewFeature {
@@ -228,6 +229,7 @@ impl PreviewFeature {
Self::PublishRequireNormalized => "publish-require-normalized",
Self::Audit => "audit",
Self::ProjectDirectoryMustExist => "project-directory-must-exist",
Self::IndexExcludeNewer => "index-exclude-newer",
}
}
}
@@ -275,6 +277,7 @@ impl FromStr for PreviewFeature {
"publish-require-normalized" => Self::PublishRequireNormalized,
"audit" => Self::Audit,
"project-directory-must-exist" => Self::ProjectDirectoryMustExist,
"index-exclude-newer" => Self::IndexExcludeNewer,
_ => return Err(PreviewFeatureParseError),
})
}
@@ -524,6 +527,10 @@ mod tests {
PreviewFeature::ProjectDirectoryMustExist.as_str(),
"project-directory-must-exist"
);
assert_eq!(
PreviewFeature::IndexExcludeNewer.as_str(),
"index-exclude-newer"
);
}
#[test]
+65
View File
@@ -8,6 +8,19 @@ use rustc_hash::FxHashMap;
use serde::ser::SerializeMap;
use uv_distribution_types::{ExcludeNewerOverride, ExcludeNewerSpan, ExcludeNewerValue};
use uv_normalize::PackageName;
use uv_preview::PreviewFeature;
use uv_warnings::warn_user_once;
/// The configuration layer that supplied the effective `exclude-newer` cutoff for a package.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub(crate) enum EffectiveExcludeNewerSource {
/// The global `exclude-newer` setting.
Global,
/// A package-specific `exclude-newer-package` override.
Package,
/// An index-specific `[[tool.uv.index]].exclude-newer` override.
Index,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ExcludeNewerValueChange {
@@ -433,6 +446,15 @@ impl ExcludeNewer {
}
}
fn warn_index_exclude_newer_preview() {
if !uv_preview::is_enabled(PreviewFeature::IndexExcludeNewer) {
warn_user_once!(
"Setting `exclude-newer` on configured indexes is experimental and may change without warning. Pass `--preview-features {}` to disable this warning.",
PreviewFeature::IndexExcludeNewer
);
}
}
/// Create a new exclude newer configuration.
pub fn new(global: Option<ExcludeNewerValue>, package: ExcludeNewerPackage) -> Self {
Self { global, package }
@@ -460,6 +482,49 @@ impl ExcludeNewer {
}
}
/// Returns the effective exclude-newer value for a package resolved from a specific index.
pub fn exclude_newer_package_for_index(
&self,
package_name: &PackageName,
index: Option<&ExcludeNewerOverride>,
) -> Option<ExcludeNewerValue> {
self.exclude_newer_package_for_index_with_source(package_name, index)
.map(|(exclude_newer, _)| exclude_newer)
}
/// Returns the effective exclude-newer value and its source for a package resolved from a
/// specific index.
pub(crate) fn exclude_newer_package_for_index_with_source(
&self,
package_name: &PackageName,
index: Option<&ExcludeNewerOverride>,
) -> Option<(ExcludeNewerValue, EffectiveExcludeNewerSource)> {
match self.package.get(package_name) {
Some(ExcludeNewerOverride::Enabled(timestamp)) => Some((
timestamp.as_ref().clone(),
EffectiveExcludeNewerSource::Package,
)),
Some(ExcludeNewerOverride::Disabled) => None,
None => match index {
Some(ExcludeNewerOverride::Disabled) => {
Self::warn_index_exclude_newer_preview();
None
}
Some(ExcludeNewerOverride::Enabled(timestamp)) => Some((
{
Self::warn_index_exclude_newer_preview();
ExcludeNewerValue::from(timestamp.timestamp())
},
EffectiveExcludeNewerSource::Index,
)),
None => self
.global
.clone()
.map(|timestamp| (timestamp, EffectiveExcludeNewerSource::Global)),
},
}
}
/// 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()
+62 -36
View File
@@ -20,6 +20,7 @@ use uv_platform_tags::{AbiTag, IncompatibleTag, LanguageTag, PlatformTag, Tags};
use crate::candidate_selector::CandidateSelector;
use crate::error::{ErrorTree, PrefixMatch};
use crate::exclude_newer::EffectiveExcludeNewerSource;
use crate::fork_indexes::ForkIndexes;
use crate::fork_urls::ForkUrls;
use crate::prerelease::AllowPrerelease;
@@ -641,7 +642,28 @@ impl PubGrubReportFormatter<'_> {
output_hints,
);
if let Some(exclude_newer) = options.exclude_newer.exclude_newer_package(name) {
let exclude_newer = if let Some(index) = fork_indexes.get(name) {
options
.exclude_newer
.exclude_newer_package_for_index_with_source(
name,
index_locations.exclude_newer_for(index.url()),
)
} else {
options
.exclude_newer
.exclude_newer_package(name)
.map(|exclude_newer| {
let source = if options.exclude_newer.package.contains_key(name) {
EffectiveExcludeNewerSource::Package
} else {
EffectiveExcludeNewerSource::Global
};
(exclude_newer, source)
})
};
if let Some((exclude_newer, source)) = exclude_newer {
if self
.available_versions
.get(name)
@@ -650,7 +672,7 @@ impl PubGrubReportFormatter<'_> {
{
output_hints.insert(PubGrubHint::ExcludeNewer {
package: name.clone(),
per_package: options.exclude_newer.package.contains_key(name),
source,
exclude_newer,
});
}
@@ -1248,7 +1270,7 @@ pub(crate) enum PubGrubHint {
/// All versions of a package were excluded by `exclude-newer`.
ExcludeNewer {
package: PackageName,
per_package: bool,
source: EffectiveExcludeNewerSource,
// excluded from `PartialEq` and `Hash`
exclude_newer: ExcludeNewerValue,
},
@@ -1337,7 +1359,7 @@ enum PubGrubHintCore {
},
ExcludeNewer {
package: PackageName,
per_package: bool,
source: EffectiveExcludeNewerSource,
},
DisjointPythonVersion,
DisjointEnvironment,
@@ -1408,13 +1430,8 @@ impl From<PubGrubHint> for PubGrubHintCore {
PubGrubHint::AbiTags { package, .. } => Self::AbiTags { package },
PubGrubHint::PlatformTags { package, .. } => Self::PlatformTags { package },
PubGrubHint::ExcludeNewer {
package,
per_package,
..
} => Self::ExcludeNewer {
package,
per_package,
},
package, source, ..
} => Self::ExcludeNewer { package, source },
PubGrubHint::DisjointPythonVersion { .. } => Self::DisjointPythonVersion,
PubGrubHint::DisjointEnvironment => Self::DisjointEnvironment,
}
@@ -1849,34 +1866,43 @@ impl std::fmt::Display for PubGrubHint {
}
Self::ExcludeNewer {
package,
per_package,
source,
exclude_newer,
} => {
if *per_package {
write!(
f,
"{}{} `{}` was filtered by `{}` to only include packages uploaded \
} => match source {
EffectiveExcludeNewerSource::Package => write!(
f,
"{}{} `{}` was filtered by `{}` to only include packages uploaded \
before {}. Consider removing the setting or updating it to a later date.",
"hint".bold().cyan(),
":".bold(),
package.cyan(),
"exclude-newer-package".green(),
exclude_newer.cyan(),
)
} else {
write!(
f,
"{}{} `{}` was filtered by `{}` to only include packages uploaded \
"hint".bold().cyan(),
":".bold(),
package.cyan(),
"exclude-newer-package".green(),
exclude_newer.cyan(),
),
EffectiveExcludeNewerSource::Global => write!(
f,
"{}{} `{}` was filtered by `{}` to only include packages uploaded \
before {}. Consider using `{}` to override the cutoff for this package.",
"hint".bold().cyan(),
":".bold(),
package.cyan(),
"exclude-newer".green(),
exclude_newer.cyan(),
"exclude-newer-package".green(),
)
}
}
"hint".bold().cyan(),
":".bold(),
package.cyan(),
"exclude-newer".green(),
exclude_newer.cyan(),
"exclude-newer-package".green(),
),
EffectiveExcludeNewerSource::Index => write!(
f,
"{}{} `{}` was filtered by the index-specific `{}` setting to only include \
packages uploaded before {}. Consider updating that index's cutoff, setting \
it to `false`, or using `{}` to override the cutoff for this package.",
"hint".bold().cyan(),
":".bold(),
package.cyan(),
"exclude-newer".green(),
exclude_newer.cyan(),
"exclude-newer-package".green(),
),
},
Self::DisjointPythonVersion { python_version } => {
write!(
f,
+3 -9
View File
@@ -78,10 +78,7 @@ use crate::resolver::system::SystemDependency;
pub(crate) use crate::resolver::urls::Urls;
use crate::universal_marker::{ConflictMarker, UniversalMarker};
use crate::yanks::AllowedYanks;
use crate::{
DependencyMode, ExcludeNewer, Exclusions, FlatIndex, Options, ResolutionMode, VersionMap,
marker,
};
use crate::{DependencyMode, Exclusions, FlatIndex, Options, ResolutionMode, VersionMap, marker};
pub(crate) use provider::MetadataUnavailable;
mod availability;
@@ -185,6 +182,7 @@ impl<'a, Context: BuildContext, InstalledPackages: InstalledPackagesProvider>
AllowedYanks::from_manifest(&manifest, &env, options.dependency_mode),
hasher,
options.exclude_newer.clone(),
build_context.locations(),
build_context.build_options(),
build_context.capabilities(),
);
@@ -372,7 +370,6 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
state.fork_indexes,
state.env,
self.current_environment.clone(),
Some(&self.options.exclude_newer),
&visited,
));
}
@@ -2693,7 +2690,6 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
fork_indexes: ForkIndexes,
env: ResolverEnvironment,
current_environment: MarkerEnvironment,
exclude_newer: Option<&ExcludeNewer>,
visited: &FxHashSet<PackageName>,
) -> ResolveError {
err = NoSolutionError::collapse_local_version_segments(NoSolutionError::collapse_proxies(
@@ -2752,9 +2748,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
for (version, dists) in version_map.iter(&Ranges::full()) {
// Don't show versions removed by excluded-newer in hints.
if let Some(exclude_newer) =
exclude_newer.and_then(|en| en.exclude_newer_package(name))
{
if let Some(exclude_newer) = version_map.exclude_newer() {
let Some(prioritized_dist) = dists.prioritized_dist() else {
continue;
};
+17 -3
View File
@@ -4,8 +4,8 @@ use uv_client::MetadataFormat;
use uv_configuration::BuildOptions;
use uv_distribution::{ArchiveMetadata, DistributionDatabase, Reporter};
use uv_distribution_types::{
Dist, IndexCapabilities, IndexMetadata, IndexMetadataRef, InstalledDist, RequestedDist,
RequiresPython,
Dist, IndexCapabilities, IndexLocations, IndexMetadata, IndexMetadataRef, InstalledDist,
RequestedDist, RequiresPython,
};
use uv_normalize::PackageName;
use uv_pep440::{Version, VersionSpecifiers};
@@ -117,6 +117,7 @@ pub struct DefaultResolverProvider<'a, Context: BuildContext> {
allowed_yanks: AllowedYanks,
hasher: HashStrategy,
exclude_newer: ExcludeNewer,
index_locations: &'a IndexLocations,
build_options: &'a BuildOptions,
capabilities: &'a IndexCapabilities,
}
@@ -131,6 +132,7 @@ impl<'a, Context: BuildContext> DefaultResolverProvider<'a, Context> {
allowed_yanks: AllowedYanks,
hasher: &'a HashStrategy,
exclude_newer: ExcludeNewer,
index_locations: &'a IndexLocations,
build_options: &'a BuildOptions,
capabilities: &'a IndexCapabilities,
) -> Self {
@@ -142,10 +144,22 @@ impl<'a, Context: BuildContext> DefaultResolverProvider<'a, Context> {
allowed_yanks,
hasher: hasher.clone(),
exclude_newer,
index_locations,
build_options,
capabilities,
}
}
fn effective_exclude_newer(
&self,
package_name: &PackageName,
index: &uv_distribution_types::IndexUrl,
) -> Option<crate::ExcludeNewerValue> {
self.exclude_newer.exclude_newer_package_for_index(
package_name,
self.index_locations.exclude_newer_for(index),
)
}
}
impl<Context: BuildContext> ResolverProvider for DefaultResolverProvider<'_, Context> {
@@ -184,7 +198,7 @@ impl<Context: BuildContext> ResolverProvider for DefaultResolverProvider<'_, Con
&self.requires_python,
&self.allowed_yanks,
&self.hasher,
Some(&self.exclude_newer),
self.effective_exclude_newer(package_name, index),
flat_index
.and_then(|flat_index| flat_index.get(package_name))
.cloned(),
+11 -3
View File
@@ -23,7 +23,7 @@ use uv_types::HashStrategy;
use uv_warnings::warn_user_once;
use crate::flat_index::FlatDistributions;
use crate::{ExcludeNewer, ExcludeNewerValue, yanks::AllowedYanks};
use crate::{ExcludeNewerValue, yanks::AllowedYanks};
/// A map from versions to distributions.
#[derive(Debug)]
@@ -51,7 +51,7 @@ impl VersionMap {
requires_python: &RequiresPython,
allowed_yanks: &AllowedYanks,
hasher: &HashStrategy,
exclude_newer: Option<&ExcludeNewer>,
exclude_newer: Option<ExcludeNewerValue>,
flat_index: Option<FlatDistributions>,
build_options: &BuildOptions,
) -> Self {
@@ -127,7 +127,7 @@ impl VersionMap {
allowed_yanks: allowed_yanks.clone(),
hasher: hasher.clone(),
requires_python: requires_python.clone(),
exclude_newer: exclude_newer.and_then(|en| en.exclude_newer_package(package_name)),
exclude_newer,
}),
}
}
@@ -188,6 +188,14 @@ impl VersionMap {
}
}
/// Return the effective `exclude-newer` cutoff for this version map, if any.
pub(crate) fn exclude_newer(&self) -> Option<&ExcludeNewerValue> {
match &self.inner {
VersionMapInner::Eager(_) => None,
VersionMapInner::Lazy(lazy) => lazy.exclude_newer.as_ref(),
}
}
/// Return an iterator over the versions and distributions.
///
/// Note that the value returned in this iterator is a [`VersionMapDist`],
+17 -4
View File
@@ -3,7 +3,9 @@ use tracing::debug;
use uv_client::{MetadataFormat, RegistryClient, VersionFiles};
use uv_distribution_filename::DistFilename;
use uv_distribution_types::{IndexCapabilities, IndexMetadataRef, IndexUrl, RequiresPython};
use uv_distribution_types::{
IndexCapabilities, IndexLocations, IndexMetadataRef, IndexUrl, RequiresPython,
};
use uv_normalize::PackageName;
use uv_platform_tags::Tags;
use uv_resolver::{ExcludeNewer, PrereleaseMode};
@@ -19,11 +21,21 @@ pub(crate) struct LatestClient<'env> {
pub(crate) capabilities: &'env IndexCapabilities,
pub(crate) prerelease: PrereleaseMode,
pub(crate) exclude_newer: &'env ExcludeNewer,
pub(crate) index_locations: &'env IndexLocations,
pub(crate) tags: Option<&'env Tags>,
pub(crate) requires_python: Option<&'env RequiresPython>,
}
impl LatestClient<'_> {
fn effective_exclude_newer(
&self,
package: &PackageName,
index: &IndexUrl,
) -> Option<uv_resolver::ExcludeNewerValue> {
self.exclude_newer
.exclude_newer_package_for_index(package, self.index_locations.exclude_newer_for(index))
}
/// Find the latest version of a package from an index.
pub(crate) async fn find_latest(
&self,
@@ -55,10 +67,11 @@ impl LatestClient<'_> {
};
let mut latest: Option<DistFilename> = None;
for (_, archive) in archives {
for (index, archive) in archives {
let MetadataFormat::Simple(archive) = archive else {
continue;
};
let exclude_newer = self.effective_exclude_newer(package, index);
for datum in archive.iter().rev() {
// Find the first compatible distribution.
@@ -70,7 +83,7 @@ impl LatestClient<'_> {
for (filename, file) in files.all() {
// Skip distributions uploaded after the cutoff.
if let Some(exclude_newer) = self.exclude_newer.exclude_newer_package(package) {
if let Some(exclude_newer) = &exclude_newer {
match file.upload_time_utc_ms.as_ref() {
Some(&upload_time)
if upload_time >= exclude_newer.timestamp_millis() =>
@@ -81,7 +94,7 @@ impl LatestClient<'_> {
warn_user_once!(
"{} is missing an upload date, but user provided: {}",
file.filename,
self.exclude_newer
exclude_newer
);
}
_ => {}
+2
View File
@@ -107,6 +107,7 @@ pub(crate) async fn pip_list(
let capabilities = IndexCapabilities::default();
let client_builder = client_builder.clone().keyring(keyring_provider);
let latest_index_locations = index_locations.clone();
// Initialize the registry client.
let client = RegistryClientBuilder::new(
@@ -132,6 +133,7 @@ pub(crate) async fn pip_list(
capabilities: &capabilities,
prerelease,
exclude_newer: &exclude_newer,
index_locations: &latest_index_locations,
tags: Some(tags),
requires_python: Some(&requires_python),
};
+2
View File
@@ -91,6 +91,7 @@ pub(crate) async fn pip_tree(
let capabilities = IndexCapabilities::default();
let client_builder = client_builder.keyring(keyring_provider);
let latest_index_locations = index_locations.clone();
// Initialize the registry client.
let client = RegistryClientBuilder::new(
@@ -116,6 +117,7 @@ pub(crate) async fn pip_tree(
capabilities: &capabilities,
prerelease,
exclude_newer: &exclude_newer,
index_locations: &latest_index_locations,
tags: Some(tags),
requires_python: Some(&requires_python),
};
+1
View File
@@ -240,6 +240,7 @@ pub(crate) async fn tree(
capabilities: &capabilities,
prerelease: lock.prerelease_mode(),
exclude_newer: &exclude_newer,
index_locations,
requires_python: Some(lock.requires_python()),
tags: None,
};
+1
View File
@@ -282,6 +282,7 @@ pub(crate) async fn install(
capabilities: &capabilities,
prerelease: settings.resolver.prerelease,
exclude_newer: &settings.resolver.exclude_newer,
index_locations: &settings.resolver.index_locations,
tags: None,
requires_python: None,
};
+1
View File
@@ -159,6 +159,7 @@ pub(crate) async fn list(
capabilities: &capabilities,
prerelease: settings.resolver.prerelease,
exclude_newer: &settings.resolver.exclude_newer,
index_locations: &settings.resolver.index_locations,
tags: None,
requires_python: Some(&requires_python),
};
+1
View File
@@ -938,6 +938,7 @@ async fn get_or_create_environment(
capabilities: &capabilities,
prerelease: settings.resolver.prerelease,
exclude_newer: &settings.resolver.exclude_newer,
index_locations: &settings.resolver.index_locations,
tags: None,
requires_python: None,
};
+183
View File
@@ -33694,6 +33694,189 @@ fn lock_exclude_newer_hint() -> Result<()> {
Ok(())
}
/// Test that `exclude-newer` can be disabled for a specific index.
///
/// Regression test for:
/// - <https://github.com/astral-sh/uv/issues/16813>
/// - <https://github.com/astral-sh/uv/issues/18799>
#[tokio::test]
async fn lock_exclude_newer_index_disable() -> Result<()> {
let context = uv_test::test_context!("3.12");
let proxy = crate::pypi_proxy::start().await;
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(&format!(
r#"
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["iniconfig>=2"]
[tool.uv]
exclude-newer = "2025-01-01T00:00:00Z"
[tool.uv.sources]
iniconfig = {{ index = "internal" }}
[[tool.uv.index]]
name = "internal"
url = "{proxy_uri}/no-upload-time/simple"
explicit = true
"#,
proxy_uri = proxy.uri()
))?;
uv_snapshot!(context.filters(), context.lock(), @"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
warning: iniconfig-2.0.0.tar.gz is missing an upload date, but user provided: 2024-03-25T00:00:00Z
warning: iniconfig-2.0.0-py3-none-any.whl is missing an upload date, but user provided: 2024-03-25T00:00:00Z
× No solution found when resolving dependencies:
Because there are no versions of iniconfig and your project depends on iniconfig>=2, we can conclude that your project's requirements are unsatisfiable.
hint: `iniconfig` was filtered by `exclude-newer` to only include packages uploaded before 2024-03-25T00:00:00Z. Consider using `exclude-newer-package` to override the cutoff for this package.
");
pyproject_toml.write_str(&format!(
r#"
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["iniconfig>=2"]
[tool.uv]
exclude-newer = "2025-01-01T00:00:00Z"
[tool.uv.sources]
iniconfig = {{ index = "internal" }}
[[tool.uv.index]]
name = "internal"
url = "{proxy_uri}/no-upload-time/simple"
explicit = true
exclude-newer = false
"#,
proxy_uri = proxy.uri()
))?;
uv_snapshot!(context.filters(), context.lock(), @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
warning: Setting `exclude-newer` on configured indexes is experimental and may change without warning. Pass `--preview-features index-exclude-newer` to disable this warning.
Resolved 2 packages in [TIME]
");
let lock = context.read("uv.lock");
assert!(lock.contains(&format!(
"source = {{ registry = \"{}/no-upload-time/simple\" }}",
proxy.uri()
)));
Ok(())
}
/// Test that an index can set its own `exclude-newer` value, and package overrides still win.
#[tokio::test]
async fn lock_exclude_newer_index_value() -> Result<()> {
let context = uv_test::test_context!("3.12");
let proxy = crate::pypi_proxy::start().await;
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(&format!(
r#"
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["iniconfig>=2"]
[tool.uv.sources]
iniconfig = {{ index = "internal" }}
[[tool.uv.index]]
name = "internal"
url = "{proxy_uri}/no-upload-time/simple"
explicit = true
exclude-newer = "2025-01-01T00:00:00Z"
"#,
proxy_uri = proxy.uri()
))?;
uv_snapshot!(context.filters(), context.lock(), @"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
warning: Setting `exclude-newer` on configured indexes is experimental and may change without warning. Pass `--preview-features index-exclude-newer` to disable this warning.
warning: iniconfig-2.0.0.tar.gz is missing an upload date, but user provided: 2025-01-01T00:00:00Z
warning: iniconfig-2.0.0-py3-none-any.whl is missing an upload date, but user provided: 2025-01-01T00:00:00Z
× No solution found when resolving dependencies:
Because there are no versions of iniconfig and your project depends on iniconfig>=2, we can conclude that your project's requirements are unsatisfiable.
hint: `iniconfig` was filtered by the index-specific `exclude-newer` setting to only include packages uploaded before 2025-01-01T00:00:00Z. Consider updating that index's cutoff, setting it to `false`, or using `exclude-newer-package` to override the cutoff for this package.
");
uv_snapshot!(context.filters(), context
.lock()
.arg("--preview-features")
.arg("index-exclude-newer"), @"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
warning: iniconfig-2.0.0.tar.gz is missing an upload date, but user provided: 2025-01-01T00:00:00Z
warning: iniconfig-2.0.0-py3-none-any.whl is missing an upload date, but user provided: 2025-01-01T00:00:00Z
× No solution found when resolving dependencies:
Because there are no versions of iniconfig and your project depends on iniconfig>=2, we can conclude that your project's requirements are unsatisfiable.
hint: `iniconfig` was filtered by the index-specific `exclude-newer` setting to only include packages uploaded before 2025-01-01T00:00:00Z. Consider updating that index's cutoff, setting it to `false`, or using `exclude-newer-package` to override the cutoff for this package.
");
pyproject_toml.write_str(&format!(
r#"
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["iniconfig>=2"]
[tool.uv]
exclude-newer-package = {{ iniconfig = false }}
[tool.uv.sources]
iniconfig = {{ index = "internal" }}
[[tool.uv.index]]
name = "internal"
url = "{proxy_uri}/no-upload-time/simple"
explicit = true
exclude-newer = "2025-01-01T00:00:00Z"
"#,
proxy_uri = proxy.uri()
))?;
uv_snapshot!(context.filters(), context.lock(), @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 2 packages in [TIME]
");
Ok(())
}
/// Test that lockfile validation includes explicit indexes from path dependencies.
/// <https://github.com/astral-sh/uv/issues/11419>
#[tokio::test]
+48
View File
@@ -19,6 +19,7 @@
//! | `/basic-auth-heron/files/…` | `public:heron` | 302 redirect → `files.pythonhosted.org` |
//! | `/basic-auth-eagle/simple/{pkg}/` | `public:eagle` | Same, different password |
//! | `/basic-auth-eagle/files/…` | `public:eagle` | 302 redirect → `files.pythonhosted.org` |
//! | `/no-upload-time/simple/{pkg}/` | No | Simple API JSON without `upload-time` |
use std::collections::HashMap;
use std::collections::hash_map::DefaultHasher;
@@ -220,6 +221,41 @@ fn build_simple_api_response(
})
}
/// Build the JSON Simple API response for a package without `upload-time`.
fn build_simple_api_response_without_upload_time(
package_name: &str,
entries: &[PackageEntry],
file_url_prefix: &str,
) -> serde_json::Value {
let files: Vec<serde_json::Value> = entries
.iter()
.map(|entry| {
let rewritten_url = entry.url.replace(
"https://files.pythonhosted.org/",
&format!("{file_url_prefix}/"),
);
let mut file_obj = json!({
"filename": entry.filename,
"url": rewritten_url,
"hashes": {
"sha256": entry.sha256
},
"size": entry.size,
});
if let Some(rp) = entry.requires_python {
file_obj["requires-python"] = json!(rp);
}
file_obj
})
.collect();
json!({
"meta": { "api-version": "1.1" },
"name": package_name,
"files": files,
})
}
/// Build the JSON Simple API response for a package with relative file URLs.
///
/// File URLs are relative paths like `../../../files/packages/...`
@@ -312,6 +348,7 @@ impl PypiProxy {
/// - `/basic-auth-eagle/simple/{pkg}/` — authenticated Simple API (public:eagle)
/// - `/relative/simple/{pkg}/` — unauthenticated Simple API with relative file links
/// - `/basic-auth/relative/simple/{pkg}/` — authenticated Simple API with relative file links
/// - `/no-upload-time/simple/{pkg}/` — unauthenticated Simple API without `upload-time`
/// - `/files/…` — unauthenticated file redirect to `files.pythonhosted.org`
/// - `/basic-auth/files/…` — authenticated file redirect (public:heron)
/// - `/basic-auth-heron/files/…` — authenticated file redirect (public:heron)
@@ -481,6 +518,17 @@ pub(crate) async fn start() -> PypiProxy {
return ResponseTemplate::new(404);
}
// Route: /no-upload-time/simple/{pkg}/ (unauthenticated)
if let Some(pkg) = extract_package_name(path, "/no-upload-time/simple/") {
if let Some(entries) = db.get(pkg) {
let file_prefix = "https://files.pythonhosted.org";
let body =
build_simple_api_response_without_upload_time(pkg, entries, file_prefix);
return simple_api_response(&body);
}
return ResponseTemplate::new(404);
}
// Route: /simple/{pkg}/ (unauthenticated)
// Unlike authenticated routes, file URLs point directly to files.pythonhosted.org
// (matching the behavior of the original fly.dev proxy).
+32
View File
@@ -152,6 +152,7 @@ fn resolve_uv_toml() -> anyhow::Result<()> {
authenticate: Auto,
ignore_error_codes: None,
cache_control: None,
exclude_newer: None,
},
],
flat_index: [],
@@ -364,6 +365,7 @@ fn resolve_uv_toml() -> anyhow::Result<()> {
authenticate: Auto,
ignore_error_codes: None,
cache_control: None,
exclude_newer: None,
},
],
flat_index: [],
@@ -577,6 +579,7 @@ fn resolve_uv_toml() -> anyhow::Result<()> {
authenticate: Auto,
ignore_error_codes: None,
cache_control: None,
exclude_newer: None,
},
],
flat_index: [],
@@ -822,6 +825,7 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> {
authenticate: Auto,
ignore_error_codes: None,
cache_control: None,
exclude_newer: None,
},
],
flat_index: [],
@@ -1221,6 +1225,7 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> {
authenticate: Auto,
ignore_error_codes: None,
cache_control: None,
exclude_newer: None,
},
],
flat_index: [],
@@ -1458,6 +1463,7 @@ fn resolve_index_url() -> anyhow::Result<()> {
authenticate: Auto,
ignore_error_codes: None,
cache_control: None,
exclude_newer: None,
},
Index {
name: None,
@@ -1492,6 +1498,7 @@ fn resolve_index_url() -> anyhow::Result<()> {
authenticate: Auto,
ignore_error_codes: None,
cache_control: None,
exclude_newer: None,
},
],
flat_index: [],
@@ -1706,6 +1713,7 @@ fn resolve_index_url() -> anyhow::Result<()> {
authenticate: Auto,
ignore_error_codes: None,
cache_control: None,
exclude_newer: None,
},
Index {
name: None,
@@ -1740,6 +1748,7 @@ fn resolve_index_url() -> anyhow::Result<()> {
authenticate: Auto,
ignore_error_codes: None,
cache_control: None,
exclude_newer: None,
},
Index {
name: None,
@@ -1774,6 +1783,7 @@ fn resolve_index_url() -> anyhow::Result<()> {
authenticate: Auto,
ignore_error_codes: None,
cache_control: None,
exclude_newer: None,
},
],
flat_index: [],
@@ -2010,6 +2020,7 @@ fn resolve_find_links() -> anyhow::Result<()> {
authenticate: Auto,
ignore_error_codes: None,
cache_control: None,
exclude_newer: None,
},
],
no_index: true,
@@ -2435,6 +2446,7 @@ fn resolve_top_level() -> anyhow::Result<()> {
authenticate: Auto,
ignore_error_codes: None,
cache_control: None,
exclude_newer: None,
},
Index {
name: None,
@@ -2469,6 +2481,7 @@ fn resolve_top_level() -> anyhow::Result<()> {
authenticate: Auto,
ignore_error_codes: None,
cache_control: None,
exclude_newer: None,
},
],
flat_index: [],
@@ -2679,6 +2692,7 @@ fn resolve_top_level() -> anyhow::Result<()> {
authenticate: Auto,
ignore_error_codes: None,
cache_control: None,
exclude_newer: None,
},
Index {
name: None,
@@ -2713,6 +2727,7 @@ fn resolve_top_level() -> anyhow::Result<()> {
authenticate: Auto,
ignore_error_codes: None,
cache_control: None,
exclude_newer: None,
},
],
flat_index: [],
@@ -4113,6 +4128,7 @@ fn resolve_both() -> anyhow::Result<()> {
authenticate: Auto,
ignore_error_codes: None,
cache_control: None,
exclude_newer: None,
},
],
flat_index: [],
@@ -4371,6 +4387,7 @@ fn resolve_both_special_fields() -> anyhow::Result<()> {
authenticate: Auto,
ignore_error_codes: None,
cache_control: None,
exclude_newer: None,
},
],
flat_index: [],
@@ -4706,6 +4723,7 @@ fn resolve_config_file() -> anyhow::Result<()> {
authenticate: Auto,
ignore_error_codes: None,
cache_control: None,
exclude_newer: None,
},
],
flat_index: [],
@@ -5603,6 +5621,7 @@ fn index_priority() -> anyhow::Result<()> {
authenticate: Auto,
ignore_error_codes: None,
cache_control: None,
exclude_newer: None,
},
Index {
name: None,
@@ -5639,6 +5658,7 @@ fn index_priority() -> anyhow::Result<()> {
authenticate: Auto,
ignore_error_codes: None,
cache_control: None,
exclude_newer: None,
},
],
flat_index: [],
@@ -5851,6 +5871,7 @@ fn index_priority() -> anyhow::Result<()> {
authenticate: Auto,
ignore_error_codes: None,
cache_control: None,
exclude_newer: None,
},
Index {
name: None,
@@ -5887,6 +5908,7 @@ fn index_priority() -> anyhow::Result<()> {
authenticate: Auto,
ignore_error_codes: None,
cache_control: None,
exclude_newer: None,
},
],
flat_index: [],
@@ -6105,6 +6127,7 @@ fn index_priority() -> anyhow::Result<()> {
authenticate: Auto,
ignore_error_codes: None,
cache_control: None,
exclude_newer: None,
},
Index {
name: None,
@@ -6141,6 +6164,7 @@ fn index_priority() -> anyhow::Result<()> {
authenticate: Auto,
ignore_error_codes: None,
cache_control: None,
exclude_newer: None,
},
],
flat_index: [],
@@ -6354,6 +6378,7 @@ fn index_priority() -> anyhow::Result<()> {
authenticate: Auto,
ignore_error_codes: None,
cache_control: None,
exclude_newer: None,
},
Index {
name: None,
@@ -6390,6 +6415,7 @@ fn index_priority() -> anyhow::Result<()> {
authenticate: Auto,
ignore_error_codes: None,
cache_control: None,
exclude_newer: None,
},
],
flat_index: [],
@@ -6610,6 +6636,7 @@ fn index_priority() -> anyhow::Result<()> {
authenticate: Auto,
ignore_error_codes: None,
cache_control: None,
exclude_newer: None,
},
Index {
name: None,
@@ -6646,6 +6673,7 @@ fn index_priority() -> anyhow::Result<()> {
authenticate: Auto,
ignore_error_codes: None,
cache_control: None,
exclude_newer: None,
},
],
flat_index: [],
@@ -6859,6 +6887,7 @@ fn index_priority() -> anyhow::Result<()> {
authenticate: Auto,
ignore_error_codes: None,
cache_control: None,
exclude_newer: None,
},
Index {
name: None,
@@ -6895,6 +6924,7 @@ fn index_priority() -> anyhow::Result<()> {
authenticate: Auto,
ignore_error_codes: None,
cache_control: None,
exclude_newer: None,
},
],
flat_index: [],
@@ -8134,6 +8164,7 @@ fn preview_features() {
PublishRequireNormalized,
Audit,
ProjectDirectoryMustExist,
IndexExcludeNewer,
],
},
python_preference: Managed,
@@ -8405,6 +8436,7 @@ fn preview_features() {
PublishRequireNormalized,
Audit,
ProjectDirectoryMustExist,
IndexExcludeNewer,
],
},
python_preference: Managed,
+25
View File
@@ -284,6 +284,31 @@ that otherwise disable caching, often unintentionally. We typically recommend fo
approach to caching headers, i.e., setting `api = "max-age=600"` and
`files = "max-age=365000000, immutable"`.
### Configuring `exclude-newer` for an index
If you're using [`exclude-newer`](./resolution.md#reproducible-resolutions), you can configure a
different cutoff for a specific index:
```toml
[[tool.uv.index]]
name = "internal"
url = "https://internal.example.com/simple"
exclude-newer = "7 days"
```
Index-specific values only affect packages served from that index. Package-specific
`exclude-newer-package` overrides still take precedence.
If an index does not provide `upload-time` metadata, you can disable the cutoff for that index
entirely:
```toml
[[tool.uv.index]]
name = "internal"
url = "https://internal.example.com/simple"
exclude-newer = false
```
## "Flat" indexes
By default, `[[tool.uv.index]]` entries are assumed to be PyPI-style registries that implement the
+1
View File
@@ -63,6 +63,7 @@ The following preview features are available:
- `python-install-default`: Allows
[installing `python` and `python3` executables](./python-versions.md#installing-python-executables).
- `format`: Allows using `uv format`.
- `index-exclude-newer`: Allows setting `exclude-newer` on configured package indexes.
- `native-auth`: Enables storage of credentials in a
[system-native location](../concepts/authentication/http.md#the-uv-credentials-store).
- `workspace-metadata`: Allows using `uv workspace metadata`.
+28 -2
View File
@@ -693,7 +693,9 @@ 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 unless the package is opted out
via `--exclude-newer-package <package>=false`. PyPI provides `upload-time` for all packages.
via `--exclude-newer-package <package>=false`, or the index is configured with its own
`exclude-newer` value, or the index is opted out via `[[tool.uv.index]] exclude-newer = 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
@@ -734,7 +736,31 @@ exclude-newer-package = { setuptools = false }
This is useful to temporarily use a newer version of package or to allow resolving a package from an
index that does not publish upload times.
Package-specific values will take precedence over global values.
Package-specific values will take precedence over both global and index-specific values.
Likewise, an individual index can override the global cutoff:
```pyproject.toml
[tool.uv]
exclude-newer = "2006-12-02T02:07:43Z"
[[tool.uv.index]]
name = "internal"
url = "https://internal.example.com/simple"
exclude-newer = "7 days"
```
Or disable it entirely for that index:
```pyproject.toml
[[tool.uv.index]]
name = "internal"
url = "https://internal.example.com/simple"
exclude-newer = false
```
This is useful for private indexes that don't publish `upload-time`, or for applying a different
reproducibility window to a specific index while preserving the global behavior elsewhere.
## Dependency cooldowns
+8
View File
@@ -971,6 +971,14 @@
"type": "boolean",
"default": false
},
"exclude-newer": {
"description": "An index-specific `exclude-newer` cutoff.\n\nAccepts the same date, timestamp, and duration values as the global `exclude-newer`\nsetting. Set this to `false` to disable `exclude-newer` for this index entirely.\n\nWhen set to a value, packages resolved from this index will use that cutoff instead of the\nglobally-specified value, unless a package-specific `exclude-newer-package` override is\npresent.\n\nThis option is in preview and may change in any future release.\n\n```toml\n[tool.uv]\nexclude-newer = \"2025-01-01T00:00:00Z\"\n\n[[tool.uv.index]]\nname = \"internal\"\nurl = \"https://internal.example.com/simple\"\nexclude-newer = \"7 days\"\n```",
"allOf": [
{
"$ref": "#/definitions/ExcludeNewerOverride"
}
]
},
"explicit": {
"description": "Mark the index as explicit.\n\nExplicit indexes will _only_ be used when explicitly requested via a `[tool.uv.sources]`\ndefinition, as in:\n\n```toml\n[[tool.uv.index]]\nname = \"pytorch\"\nurl = \"https://download.pytorch.org/whl/cu121\"\nexplicit = true\n\n[tool.uv.sources]\ntorch = { index = \"pytorch\" }\n```",
"type": "boolean",