Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions crates/uv-resolver/src/exclude_newer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use jiff::{Span, Timestamp, ToSpan, Unit, tz::TimeZone};
use rustc_hash::FxHashMap;
use serde::Deserialize;
use serde::de::value::MapAccessDeserializer;
use serde::ser::SerializeMap;
use uv_normalize::PackageName;

#[derive(Debug, Clone, PartialEq, Eq)]
Expand Down Expand Up @@ -218,6 +219,24 @@ impl serde::Serialize for ExcludeNewerValue {
}
}

pub struct ExcludeNewerValueWithSpanRef<'a>(pub &'a ExcludeNewerValue);

impl serde::Serialize for ExcludeNewerValueWithSpanRef<'_> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
if let Some(span) = self.0.span() {
let mut map = serializer.serialize_map(Some(2))?;
map.serialize_entry("timestamp", &self.0.timestamp())?;
map.serialize_entry("span", span)?;
map.end()
} else {
self.0.timestamp().serialize(serializer)
}
}
}

impl<'de> serde::Deserialize<'de> for ExcludeNewerValue {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
Expand Down Expand Up @@ -620,6 +639,29 @@ impl serde::Serialize for PackageExcludeNewer {
}
}

pub fn serialize_exclude_newer_package_with_spans<S>(
value: &Option<ExcludeNewerPackage>,
serializer: S,
) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let Some(value) = value else {
return serializer.serialize_none();
};

let mut map = serializer.serialize_map(Some(value.len()))?;
for (name, setting) in value {
match setting {
PackageExcludeNewer::Disabled => map.serialize_entry(name, &false)?,
PackageExcludeNewer::Enabled(value) => {
map.serialize_entry(name, &ExcludeNewerValueWithSpanRef(value.as_ref()))?;
}
}
}
map.end()
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PackageExcludeNewerChange {
Disabled { was: ExcludeNewerValue },
Expand Down
5 changes: 3 additions & 2 deletions crates/uv-resolver/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ pub use dependency_mode::DependencyMode;
pub use error::{ErrorTree, NoSolutionError, NoSolutionHeader, ResolveError, SentinelRange};
pub use exclude_newer::{
ExcludeNewer, ExcludeNewerChange, ExcludeNewerPackage, ExcludeNewerPackageChange,
ExcludeNewerPackageEntry, ExcludeNewerValue, ExcludeNewerValueChange, PackageExcludeNewer,
PackageExcludeNewerChange,
ExcludeNewerPackageEntry, ExcludeNewerSpan, ExcludeNewerValue, ExcludeNewerValueChange,
ExcludeNewerValueWithSpanRef, PackageExcludeNewer, PackageExcludeNewerChange,
serialize_exclude_newer_package_with_spans,
};
pub use exclusions::Exclusions;
pub use flat_index::{FlatDistributions, FlatIndex};
Expand Down
142 changes: 140 additions & 2 deletions crates/uv-settings/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ use uv_pypi_types::{SupportedEnvironments, VerbatimParsedUrl};
use uv_python::{PythonDownloads, PythonPreference, PythonVersion};
use uv_redacted::DisplaySafeUrl;
use uv_resolver::{
AnnotationStyle, ExcludeNewer, ExcludeNewerPackage, ExcludeNewerValue, ForkStrategy,
PrereleaseMode, ResolutionMode,
AnnotationStyle, ExcludeNewer, ExcludeNewerPackage, ExcludeNewerSpan, ExcludeNewerValue,
ForkStrategy, PrereleaseMode, ResolutionMode, serialize_exclude_newer_package_with_spans,
};
use uv_torch::TorchMode;
use uv_workspace::pyproject::ExtraBuildDependencies;
Expand Down Expand Up @@ -499,6 +499,25 @@ pub struct ResolverInstallerOptions {
pub no_binary_package: Option<Vec<PackageName>>,
}

impl ResolverInstallerOptions {
/// Recompute any relative exclude-newer values against the current time.
#[must_use]
pub fn recompute_exclude_newer(mut self) -> Self {
let exclude_newer = ExcludeNewer::new(
self.exclude_newer.take(),
self.exclude_newer_package.take().unwrap_or_default(),
)
.recompute();
self.exclude_newer = exclude_newer.global;
self.exclude_newer_package = if exclude_newer.package.is_empty() {
None
} else {
Some(exclude_newer.package)
};
self
}
}

impl From<ResolverInstallerSchema> for ResolverInstallerOptions {
fn from(value: ResolverInstallerSchema) -> Self {
let ResolverInstallerSchema {
Expand Down Expand Up @@ -2130,6 +2149,41 @@ pub struct ToolOptions {
pub torch_backend: Option<TorchMode>,
}

/// The on-disk representation of [`ToolOptions`] in a tool receipt.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub struct ToolOptionsWire {
pub index: Option<Vec<Index>>,
pub index_url: Option<PipIndex>,
pub extra_index_url: Option<Vec<PipExtraIndex>>,
pub no_index: Option<bool>,
pub find_links: Option<Vec<PipFindLinks>>,
pub index_strategy: Option<IndexStrategy>,
pub keyring_provider: Option<KeyringProviderType>,
pub resolution: Option<ResolutionMode>,
pub prerelease: Option<PrereleaseMode>,
pub fork_strategy: Option<ForkStrategy>,
pub dependency_metadata: Option<Vec<StaticMetadata>>,
pub config_settings: Option<ConfigSettings>,
pub config_settings_package: Option<PackageConfigSettings>,
pub build_isolation: Option<BuildIsolation>,
pub extra_build_dependencies: Option<ExtraBuildDependencies>,
pub extra_build_variables: Option<ExtraBuildVariables>,
pub exclude_newer: Option<ExcludeNewerValue>,
pub exclude_newer_span: Option<ExcludeNewerSpan>,
#[serde(serialize_with = "serialize_exclude_newer_package_with_spans")]
pub exclude_newer_package: Option<ExcludeNewerPackage>,
pub link_mode: Option<LinkMode>,
pub compile_bytecode: Option<bool>,
pub no_sources: Option<bool>,
pub no_sources_package: Option<Vec<PackageName>>,
pub no_build: Option<bool>,
pub no_build_package: Option<Vec<PackageName>>,
pub no_binary: Option<bool>,
pub no_binary_package: Option<Vec<PackageName>>,
pub torch_backend: Option<TorchMode>,
}

impl From<ResolverInstallerOptions> for ToolOptions {
fn from(value: ResolverInstallerOptions) -> Self {
Self {
Expand Down Expand Up @@ -2169,6 +2223,90 @@ impl From<ResolverInstallerOptions> for ToolOptions {
}
}

impl From<ToolOptionsWire> for ToolOptions {
fn from(value: ToolOptionsWire) -> Self {
let exclude_newer = value.exclude_newer.map(|exclude_newer| {
if exclude_newer.span().is_none() {
ExcludeNewerValue::new(exclude_newer.timestamp(), value.exclude_newer_span)
} else {
exclude_newer
}
});

Self {
index: value.index,
index_url: value.index_url,
extra_index_url: value.extra_index_url,
no_index: value.no_index,
find_links: value.find_links,
index_strategy: value.index_strategy,
keyring_provider: value.keyring_provider,
resolution: value.resolution,
prerelease: value.prerelease,
fork_strategy: value.fork_strategy,
dependency_metadata: value.dependency_metadata,
config_settings: value.config_settings,
config_settings_package: value.config_settings_package,
build_isolation: value.build_isolation,
extra_build_dependencies: value.extra_build_dependencies,
extra_build_variables: value.extra_build_variables,
exclude_newer,
exclude_newer_package: value.exclude_newer_package,
link_mode: value.link_mode,
compile_bytecode: value.compile_bytecode,
no_sources: value.no_sources,
no_sources_package: value.no_sources_package,
no_build: value.no_build,
no_build_package: value.no_build_package,
no_binary: value.no_binary,
no_binary_package: value.no_binary_package,
torch_backend: value.torch_backend,
}
}
}

impl From<ToolOptions> for ToolOptionsWire {
fn from(value: ToolOptions) -> Self {
let (exclude_newer, exclude_newer_span) = value
.exclude_newer
.map(ExcludeNewerValue::into_parts)
.map_or((None, None), |(timestamp, span)| {
(Some(ExcludeNewerValue::from(timestamp)), span)
});

Self {
index: value.index,
index_url: value.index_url,
extra_index_url: value.extra_index_url,
no_index: value.no_index,
find_links: value.find_links,
index_strategy: value.index_strategy,
keyring_provider: value.keyring_provider,
resolution: value.resolution,
prerelease: value.prerelease,
fork_strategy: value.fork_strategy,
dependency_metadata: value.dependency_metadata,
config_settings: value.config_settings,
config_settings_package: value.config_settings_package,
build_isolation: value.build_isolation,
extra_build_dependencies: value.extra_build_dependencies,
extra_build_variables: value.extra_build_variables,
exclude_newer,
exclude_newer_span,
exclude_newer_package: value.exclude_newer_package,
link_mode: value.link_mode,
compile_bytecode: value.compile_bytecode,
no_sources: value.no_sources,
no_sources_package: value.no_sources_package,
no_build: value.no_build,
no_build_package: value.no_build_package,
no_binary: value.no_binary,
no_binary_package: value.no_binary_package,
torch_backend: value.torch_backend,
}
}
}

impl From<ToolOptions> for ResolverInstallerOptions {
fn from(value: ToolOptions) -> Self {
Self {
Expand Down
14 changes: 8 additions & 6 deletions crates/uv-tool/src/tool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use uv_fs::{PortablePath, Simplified};
use uv_normalize::PackageName;
use uv_pypi_types::VerbatimParsedUrl;
use uv_python::PythonRequest;
use uv_settings::ToolOptions;
use uv_settings::{ToolOptions, ToolOptionsWire};

/// A tool entry.
#[derive(Debug, Clone, Deserialize)]
Expand Down Expand Up @@ -49,7 +49,7 @@ struct ToolWire {
python: Option<PythonRequest>,
entrypoints: Vec<ToolEntrypoint>,
#[serde(default)]
options: ToolOptions,
options: ToolOptionsWire,
}

#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
Expand All @@ -76,7 +76,7 @@ impl From<Tool> for ToolWire {
build_constraint_dependencies: tool.build_constraints,
python: tool.python,
entrypoints: tool.entrypoints,
options: tool.options,
options: tool.options.into(),
}
}
}
Expand All @@ -100,7 +100,7 @@ impl TryFrom<ToolWire> for Tool {
build_constraints: tool.build_constraint_dependencies,
python: tool.python,
entrypoints: tool.entrypoints,
options: tool.options,
options: tool.options.into(),
})
}
}
Expand Down Expand Up @@ -333,8 +333,10 @@ impl Tool {
});

if self.options != ToolOptions::default() {
let serialized =
serde::Serialize::serialize(&self.options, toml_edit::ser::ValueSerializer::new())?;
let serialized = serde::Serialize::serialize(
&ToolOptionsWire::from(self.options.clone()),
toml_edit::ser::ValueSerializer::new(),
)?;
let Value::InlineTable(serialized) = serialized else {
return Err(toml_edit::ser::Error::Custom(
"Expected an inline table".to_string(),
Expand Down
10 changes: 7 additions & 3 deletions crates/uv/src/commands/tool/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,9 +130,13 @@ pub(crate) async fn list(
let filesystem = filesystem.clone();
async move {
let capabilities = IndexCapabilities::default();
let settings = ResolverInstallerSettings::from(args.combine(
ResolverInstallerOptions::from(tool.options().clone()).combine(filesystem),
));
let settings = ResolverInstallerSettings::from(
args.combine(
ResolverInstallerOptions::from(tool.options().clone())
.recompute_exclude_newer()
.combine(filesystem),
),
);
let interpreter = tool_env.environment().interpreter();

let client = RegistryClientBuilder::new(
Expand Down
1 change: 1 addition & 0 deletions crates/uv/src/commands/tool/upgrade.rs
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,7 @@ async fn upgrade_tool(
// Resolve the appropriate settings, preferring: CLI > receipt > user.
let options = args.clone().combine(
ResolverInstallerOptions::from(existing_tool_receipt.options().clone())
.recompute_exclude_newer()
.combine(filesystem.clone()),
);
let settings = ResolverInstallerSettings::from(options.clone());
Expand Down
37 changes: 37 additions & 0 deletions crates/uv/tests/it/tool_install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,43 @@ fn tool_install() {
});
}

#[test]
fn tool_install_relative_exclude_newer_receipt_preserves_span() {
let context = uv_test::test_context!("3.12").with_filtered_exe_suffix();
let tool_dir = context.temp_dir.child("tools");
let bin_dir = context.temp_dir.child("bin");

context
.tool_install()
.arg("black==24.2.0")
.arg("--exclude-newer")
.arg("3 weeks")
.env_remove(EnvVars::UV_EXCLUDE_NEWER)
.env(EnvVars::UV_TEST_CURRENT_TIMESTAMP, "2024-05-01T00:00:00Z")
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str())
.env(EnvVars::PATH, bin_dir.as_os_str())
.assert()
.success();

insta::with_settings!({
filters => context.filters(),
}, {
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv-receipt.toml")).unwrap(), @r#"
[tool]
requirements = [{ name = "black", specifier = "==24.2.0" }]
entrypoints = [
{ name = "black", install-path = "[TEMP_DIR]/bin/black", from = "black" },
{ name = "blackd", install-path = "[TEMP_DIR]/bin/blackd", from = "black" },
]

[tool.options]
exclude-newer = "2024-04-10T00:00:00Z"
exclude-newer-span = "P3W"
"#);
});
}

#[test]
fn tool_install_python_from_global_version_file() {
let context = uv_test::test_context_with_versions!(&["3.11", "3.12", "3.13"])
Expand Down
Loading
Loading