Skip to content
Draft
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
60 changes: 59 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

107 changes: 102 additions & 5 deletions crates/uv-cli/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
use std::ffi::OsString;
use std::ops::{Deref, DerefMut};
use std::path::PathBuf;
use std::str::FromStr;

use anyhow::{Result, anyhow};
use clap::builder::Styles;
use clap::builder::styling::{AnsiColor, Effects, Style};
use clap::{Args, Parser, Subcommand};
use std::ffi::OsString;
use std::ops::{Deref, DerefMut};
use std::path::PathBuf;
use std::str::FromStr;

use uv_auth::Service;
use uv_cache::CacheArgs;
Expand All @@ -31,6 +30,7 @@ use uv_resolver::{
use uv_settings::PythonInstallMirrors;
use uv_static::EnvVars;
use uv_torch::TorchMode;
use uv_workspace::pyproject::DependencyType;
use uv_workspace::pyproject_mut::AddBoundsKind;

pub mod comma;
Expand Down Expand Up @@ -656,6 +656,62 @@ pub struct VersionArgs {
pub python: Option<Maybe<String>>,
}

#[derive(Args)]
pub struct UpgradeProjectArgs {
/// Run without performing the upgrades.
#[arg(long)]
pub dry_run: bool,

/// Search recursively for pyproject.toml files.
#[arg(long, env = EnvVars::UV_UPGRADE_RECURSIVE)]
pub recursive: bool,

/// Only search specific tables in pyproject.toml: `prod,dev,optional,groups`.
#[arg(
long,
env = EnvVars::UV_UPGRADE_TYPES,
value_delimiter = ',',
value_parser = parse_dependency_type,
)]
pub types: Vec<Maybe<DependencyType>>,

/// Allow only some version digits to change, others will be skipped:
/// `1,2,3,4` (major, minor, patch, build number).
#[arg(
long,
env = EnvVars::UV_UPGRADE_TYPES,
value_delimiter = ',',
value_parser = parse_version_digit,
)]
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
/// metadata when there are not wheels.
///
/// The interpreter is also used as the fallback value for the minimum Python version if
/// `requires-python` is not set.
///
/// See `uv help python` for details on Python discovery and supported request formats.
#[arg(
long,
short,
env = EnvVars::UV_PYTHON,
verbatim_doc_comment,
help_heading = "Python options",
value_parser = parse_maybe_string,
)]
pub python: Option<Maybe<String>>,

/// Upgrade only the given requirements (i.e. `uv<0.5`) instead of pyproject.toml files.
#[arg(required = false, value_parser = parse_requirement)]
pub requirements: Vec<Maybe<Requirement>>,
}

// Note that the ordering of the variants is significant, as when given a list of operations
// to perform, we sort them and apply them in order, so users don't have to think too hard about it.
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, clap::ValueEnum)]
Expand Down Expand Up @@ -975,6 +1031,8 @@ pub enum ProjectCommand {
Remove(RemoveArgs),
/// Read or update the project's version.
Version(VersionArgs),
/// Upgrade the project's dependency constraints.
Upgrade(UpgradeProjectArgs),
/// Update the project's environment.
///
/// Syncing ensures that all project dependencies are installed and up-to-date with the
Expand Down Expand Up @@ -1172,6 +1230,45 @@ fn parse_insecure_host(input: &str) -> Result<Maybe<TrustedHost>, String> {
}
}

/// Parse a string into an [`DependencyType`], mapping the empty string to `None`.
fn parse_dependency_type(input: &str) -> Result<Maybe<DependencyType>, String> {
if input.is_empty() {
Ok(Maybe::None)
} else {
match DependencyType::from_str(input) {
Ok(table) => Ok(Maybe::Some(table)),
Err(err) => Err(err.to_string()),
}
}
}

/// Parse a string like `uv<0.5` into an [`Requirement`], mapping the empty string to `None`.
fn parse_requirement(input: &str) -> Result<Maybe<Requirement>, String> {
if input.is_empty() {
Ok(Maybe::None)
} else {
match Requirement::from_str(input) {
Ok(table) => Ok(Maybe::Some(table)),
Err(err) => Err(err.to_string()),
}
}
}

/// Parse a string into an [`usize`], mapping the empty string or unknown digits to `None`.
///
/// Allowed: 1, 2, 3 or 4.
fn parse_version_digit(input: &str) -> Result<Maybe<usize>, String> {
if input.is_empty() {
Ok(Maybe::None)
} else {
match usize::from_str(input) {
Ok(digit) if (1..=4).contains(&digit) => Ok(Maybe::Some(digit)),
Ok(_) => Ok(Maybe::None),
Err(err) => Err(err.to_string()),
}
}
}

/// Parse a string into a [`PathBuf`]. The string can represent a file, either as a path or a
/// `file://` URL.
fn parse_file_path(input: &str) -> Result<PathBuf, String> {
Expand Down
2 changes: 1 addition & 1 deletion crates/uv-distribution-types/src/index_url.rs
Original file line number Diff line number Diff line change
Expand Up @@ -376,7 +376,7 @@ impl<'a> IndexLocations {
}

/// Clone the index locations into a [`IndexUrls`] instance.
pub fn index_urls(&'a self) -> IndexUrls {
pub fn index_urls(&self) -> IndexUrls {
IndexUrls {
indexes: self.indexes.clone(),
no_index: self.no_index,
Expand Down
17 changes: 15 additions & 2 deletions crates/uv-distribution-types/src/requires_python.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
use std::collections::Bound;

use std::str::FromStr;
use version_ranges::Ranges;

use uv_distribution_filename::WheelFilename;
use uv_pep440::{
LowerBound, UpperBound, Version, VersionSpecifier, VersionSpecifiers,
release_specifiers_to_ranges,
VersionSpecifiersParseError, release_specifiers_to_ranges,
};
use uv_pep508::{MarkerExpression, MarkerTree, MarkerValueVersion};
use uv_platform_tags::{AbiTag, LanguageTag};
Expand Down Expand Up @@ -502,6 +502,19 @@ impl RequiresPython {
}
})
}

/// Remove trailing zeroes from all specifiers
pub fn remove_zeroes(&self) -> String {
self.specifiers().remove_zeroes().to_string()
}
}

impl FromStr for RequiresPython {
type Err = VersionSpecifiersParseError;

fn from_str(s: &str) -> Result<Self, Self::Err> {
VersionSpecifiers::from_str(s).map(|v| Self::from_specifiers(&v))
}
}

impl std::fmt::Display for RequiresPython {
Expand Down
2 changes: 1 addition & 1 deletion crates/uv-fs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -412,7 +412,7 @@ pub async fn persist_with_retry(
// So every time we fail, we need to reset the `NamedTempFile` to try again.
//
// Every time we (re)try we call this outer closure (`let persist = ...`), so it needs to
// be at least a `FnMut` (as opposed to `Fnonce`). However the closure needs to return a
// be at least a `FnMut` (as opposed to `FnOnce`). However the closure needs to return a
// totally owned `Future` (so effectively it returns a `FnOnce`).
//
// But if the `Future` is totally owned it *necessarily* can't write back the `NamedTempFile`
Expand Down
4 changes: 2 additions & 2 deletions crates/uv-pep440/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ pub use version_ranges::{
pub use {
version::{
BumpCommand, LocalSegment, LocalVersion, LocalVersionSlice, MIN_VERSION, Operator,
OperatorParseError, Prerelease, PrereleaseKind, Version, VersionParseError, VersionPattern,
VersionPatternParseError,
OperatorParseError, Prerelease, PrereleaseKind, Version, VersionDigit, VersionParseError,
VersionPattern, VersionPatternParseError,
},
version_specifier::{
TildeVersionSpecifier, VersionSpecifier, VersionSpecifierBuildError, VersionSpecifiers,
Expand Down
Loading
Loading