support refresh args (default: cache for 10 minutes)

This commit is contained in:
Rene Leonhardt 2025-06-13 14:01:50 +02:00
parent 15978e285b
commit cc8c7218ba
No known key found for this signature in database
GPG key ID: 8C95C84F75AB1E8E
4 changed files with 117 additions and 78 deletions

View file

@ -637,6 +637,9 @@ pub struct UpgradeProjectArgs {
)]
pub allow: Vec<Maybe<usize>>,
#[command(flatten)]
pub refresh: RefreshArgs,
/// The Python interpreter to use during resolution (overrides pyproject.toml).
///
/// A Python interpreter is required for building source distributions to determine package

View file

@ -1,10 +1,3 @@
use std::env;
use std::ffi::OsStr;
use std::fmt::Write;
use std::io::ErrorKind;
use std::path::Path;
use std::str::FromStr;
use crate::commands::ExitStatus;
use crate::commands::pip::latest::LatestClient;
use crate::printer::Printer;
@ -15,9 +8,13 @@ use owo_colors::OwoColorize;
use prettytable::format::FormatBuilder;
use prettytable::row;
use rustc_hash::{FxHashMap, FxHashSet};
use std::ffi::OsStr;
use std::fmt::Write;
use std::io::ErrorKind;
use std::path::Path;
use std::str::FromStr;
use tokio::sync::Semaphore;
use uv_cache::{Cache, Refresh};
use uv_cache_info::Timestamp;
use uv_cache::Cache;
use uv_cli::{Maybe, UpgradeProjectArgs};
use uv_client::{BaseClientBuilder, RegistryClientBuilder};
use uv_configuration::Concurrency;
@ -34,7 +31,10 @@ use walkdir::WalkDir;
/// Upgrade all dependencies in the project requirements (pyproject.toml).
///
/// This doesn't read or modify uv.lock, only constraints like `<1.0` are bumped.
pub(crate) async fn upgrade_project_dependencies(args: UpgradeProjectArgs) -> Result<ExitStatus> {
pub(crate) async fn upgrade_project_dependencies(
args: UpgradeProjectArgs,
cache: Cache,
) -> Result<ExitStatus> {
let tables: Vec<_> = match args
.types
.iter()
@ -65,19 +65,15 @@ pub(crate) async fn upgrade_project_dependencies(args: UpgradeProjectArgs) -> Re
let printer = Printer::Default;
let info = format!("{}{}", "info".cyan().bold(), ":".bold());
#[allow(deprecated)]
let cache_dir = env::home_dir().unwrap().join(".cache/uv");
let cache = Cache::from_settings(false, Some(cache_dir))?.init()?;
let capabilities = IndexCapabilities::default();
let client_builder = BaseClientBuilder::new();
// Initialize the registry client.
let client = RegistryClientBuilder::try_from(client_builder)?
.cache(cache.clone().with_refresh(Refresh::All(Timestamp::now())))
.cache(cache)
.index_locations(&IndexLocations::default())
.build();
let concurrency = Concurrency::default();
let download_concurrency = Semaphore::new(concurrency.downloads);
let (mut item_written, mut all_found, mut all_bumped, mut all_skipped) =
(false, 0, 0, VersionDigit::default());
@ -86,37 +82,13 @@ pub(crate) async fn upgrade_project_dependencies(args: UpgradeProjectArgs) -> Re
for toml_dir in &tomls {
let pyproject_toml = Path::new(toml_dir).join("pyproject.toml");
let content = match fs_err::tokio::read_to_string(pyproject_toml.clone()).await {
Ok(content) => content,
Err(err) => {
if err.kind() == ErrorKind::NotFound {
warn_user!("No pyproject.toml found in current directory");
return Ok(ExitStatus::Error);
}
return Err(err.into());
}
};
let mut toml = match PyProjectTomlMut::from_toml(&content, DependencyTarget::PyProjectToml)
{
Ok(toml) => toml,
Err(err) => {
warn_user!("Couldn't read pyproject.toml: {}", err);
return Ok(ExitStatus::Error);
}
let mut toml = match read_pyproject_toml(&pyproject_toml).await {
Ok(value) => value,
Err(value) => return value,
};
let python = args
.python
.clone()
.and_then(Maybe::into_option)
.or_else(|| {
toml.get_requires_python()
.map(std::string::ToString::to_string)
});
let version_specifiers = python.and_then(|s| VersionSpecifiers::from_str(&s).ok());
let requires_python = version_specifiers
.map(|v| RequiresPython::from_specifiers(&v))
.unwrap_or_else(|| RequiresPython::greater_than_equal_version(&Version::new([4]))); // allow any by default
let python = get_python(&args, &toml);
let requires_python = create_requires_python(python);
// Initialize the client to fetch the latest version of each package.
let client = LatestClient {
@ -128,25 +100,6 @@ pub(crate) async fn upgrade_project_dependencies(args: UpgradeProjectArgs) -> Re
requires_python: &requires_python,
};
let find_latest = async |names: &FxHashSet<PackageName>| {
let mut fetches = futures::stream::iter(names.iter())
.map(async |name| {
let latest = client
.find_latest(name, None, &download_concurrency)
.await?;
Ok::<(&PackageName, Option<DistFilename>), uv_client::Error>((name, latest))
})
.buffer_unordered(concurrency.downloads);
let mut map = FxHashMap::default();
while let Ok(Some((package, version))) = fetches.next().await.transpose() {
if let Some(version) = version.as_ref() {
map.insert(package.clone(), version.clone().into_version());
}
}
map
};
let relative = if toml_dir == "." {
String::new()
} else {
@ -160,7 +113,7 @@ pub(crate) async fn upgrade_project_dependencies(args: UpgradeProjectArgs) -> Re
.into_iter()
.filter(|p| !all_latest_versions.contains_key(p))
.collect();
let latest_versions = find_latest(&query_versions).await;
let latest_versions = find_latest(&client, &query_versions, concurrency.downloads).await;
all_latest_versions.extend(latest_versions.clone());
let (found, upgrades) =
@ -292,6 +245,72 @@ pub(crate) async fn upgrade_project_dependencies(args: UpgradeProjectArgs) -> Re
Ok(ExitStatus::Success)
}
fn create_requires_python(python: Option<String>) -> RequiresPython {
let version_specifiers = python.and_then(|s| VersionSpecifiers::from_str(&s).ok());
version_specifiers
.map(|v| RequiresPython::from_specifiers(&v))
.unwrap_or_else(|| RequiresPython::greater_than_equal_version(&Version::new([4]))) // allow any by default
}
fn get_python(args: &UpgradeProjectArgs, toml: &PyProjectTomlMut) -> Option<String> {
let python = args
.python
.clone()
.and_then(Maybe::into_option)
.or_else(|| {
toml.get_requires_python()
.map(std::string::ToString::to_string)
});
python
}
async fn read_pyproject_toml(
pyproject_toml: &Path,
) -> Result<PyProjectTomlMut, Result<ExitStatus>> {
let content = match fs_err::tokio::read_to_string(pyproject_toml.to_path_buf()).await {
Ok(content) => content,
Err(err) => {
if err.kind() == ErrorKind::NotFound {
warn_user!("No pyproject.toml found in current directory");
return Err(Ok(ExitStatus::Error));
}
return Err(Err(err.into()));
}
};
let toml = match PyProjectTomlMut::from_toml(&content, DependencyTarget::PyProjectToml) {
Ok(toml) => toml,
Err(err) => {
warn_user!("Couldn't read pyproject.toml: {}", err);
return Err(Ok(ExitStatus::Error));
}
};
Ok(toml)
}
async fn find_latest(
client: &LatestClient<'_>,
names: &FxHashSet<PackageName>,
downloads: usize,
) -> FxHashMap<PackageName, Version> {
let download_concurrency = Semaphore::new(downloads);
let mut fetches = futures::stream::iter(names.iter())
.map(async |name| {
let latest = client
.find_latest(name, None, &download_concurrency)
.await?;
Ok::<(&PackageName, Option<DistFilename>), uv_client::Error>((name, latest))
})
.buffer_unordered(downloads);
let mut map = FxHashMap::default();
while let Ok(Some((package, version))) = fetches.next().await.transpose() {
if let Some(version) = version.as_ref() {
map.insert(package.clone(), version.clone().into_version());
}
}
map
}
/// Recursively search for pyproject.toml files.
fn search_pyproject_tomls(root: &Path) -> Result<Vec<String>, anyhow::Error> {
let metadata = match fs_err::symlink_metadata(root) {

View file

@ -1,15 +1,3 @@
use std::borrow::Cow;
use std::collections::BTreeMap;
use std::ffi::OsString;
use std::fmt::Write;
use std::io::stdout;
#[cfg(feature = "self-update")]
use std::ops::Bound;
use std::path::Path;
use std::process::ExitCode;
use std::str::FromStr;
use std::sync::atomic::Ordering;
use anstream::eprintln;
use anyhow::{Context, Result, bail};
use clap::error::{ContextKind, ContextValue};
@ -17,6 +5,19 @@ use clap::{CommandFactory, Parser};
use futures::FutureExt;
use owo_colors::OwoColorize;
use settings::PipTreeSettings;
use std::borrow::Cow;
use std::collections::BTreeMap;
use std::ffi::OsString;
use std::fmt::Write;
use std::io::stdout;
#[cfg(feature = "self-update")]
use std::ops::Bound;
use std::ops::Sub;
use std::path::Path;
use std::process::ExitCode;
use std::str::FromStr;
use std::sync::atomic::Ordering;
use std::time::{Duration, SystemTime};
use tokio::task::spawn_blocking;
use tracing::{debug, instrument};
@ -2072,7 +2073,21 @@ async fn run_project(
.await
}
ProjectCommand::Upgrade(args) => {
Box::pin(commands::upgrade_project_dependencies(args)).await
// --refresh -> now() -> uncached
// --no-refresh -> cached (overrides --refresh)
// otherwise: cache subsequent runs for 10 minutes
let timestamp = Timestamp::from(SystemTime::now().sub(Duration::from_secs(60 * 10)));
let refresh_package = args.refresh.refresh_package.clone();
let _refresh = if args.refresh.refresh || args.refresh.no_refresh {
Refresh::from_args(Some(!args.refresh.no_refresh), refresh_package)
} else if refresh_package.is_empty() {
Refresh::All(timestamp) // user didn't pass flags or package
} else {
Refresh::Packages(refresh_package, vec![], timestamp)
};
let cache = cache.init()?.with_refresh(_refresh);
Box::pin(commands::upgrade_project_dependencies(args, cache)).await
}
ProjectCommand::Tree(args) => {
// Resolve the settings from the command-line arguments and workspace configuration.

View file

@ -998,7 +998,9 @@ metadata when there are not wheels.</p>
<p>May also be set with the <code>UV_PYTHON</code> environment variable.</p></dd><dt id="uv-upgrade--quiet"><a href="#uv-upgrade--quiet"><code>--quiet</code></a>, <code>-q</code></dt><dd><p>Use quiet output.</p>
<p>Repeating this option, e.g., <code>-qq</code>, will enable a silent mode in which uv will write no output to stdout.</p>
</dd><dt id="uv-upgrade--recursive"><a href="#uv-upgrade--recursive"><code>--recursive</code></a></dt><dd><p>Search recursively for pyproject.toml files</p>
<p>May also be set with the <code>UV_UPGRADE_RECURSIVE</code> environment variable.</p></dd><dt id="uv-upgrade--types"><a href="#uv-upgrade--types"><code>--types</code></a> <i>types</i></dt><dd><p>Only search specific tables in pyproject.toml: <code>prod,dev,optional,groups</code></p>
<p>May also be set with the <code>UV_UPGRADE_RECURSIVE</code> environment variable.</p></dd><dt id="uv-upgrade--refresh"><a href="#uv-upgrade--refresh"><code>--refresh</code></a></dt><dd><p>Refresh all cached data</p>
</dd><dt id="uv-upgrade--refresh-package"><a href="#uv-upgrade--refresh-package"><code>--refresh-package</code></a> <i>refresh-package</i></dt><dd><p>Refresh cached data for a specific package</p>
</dd><dt id="uv-upgrade--types"><a href="#uv-upgrade--types"><code>--types</code></a> <i>types</i></dt><dd><p>Only search specific tables in pyproject.toml: <code>prod,dev,optional,groups</code></p>
<p>May also be set with the <code>UV_UPGRADE_TYPES</code> environment variable.</p></dd><dt id="uv-upgrade--verbose"><a href="#uv-upgrade--verbose"><code>--verbose</code></a>, <code>-v</code></dt><dd><p>Use verbose output.</p>
<p>You can configure fine-grained logging using the <code>RUST_LOG</code> environment variable. (<a href="https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#directives">https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#directives</a>)</p>
</dd></dl>