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:
@@ -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(_))
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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`],
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Generated
+8
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user