feature: --types=prod,dev,extra,group

This commit is contained in:
Rene Leonhardt 2025-06-10 18:59:54 +02:00
parent a654845891
commit eb9ea71069
No known key found for this signature in database
GPG key ID: 8C95C84F75AB1E8E
6 changed files with 216 additions and 47 deletions

View file

@ -22,6 +22,7 @@ use uv_redacted::DisplaySafeUrl;
use uv_resolver::{AnnotationStyle, ExcludeNewer, ForkStrategy, PrereleaseMode, ResolutionMode};
use uv_static::EnvVars;
use uv_torch::TorchMode;
use uv_workspace::pyproject::DependencyType;
use uv_workspace::pyproject_mut::AddBoundsKind;
pub mod comma;
@ -618,6 +619,21 @@ pub struct UpgradeProjectArgs {
#[arg(long, env = EnvVars::UV_UPGRADE_RECURSIVE)]
pub recursive: bool,
/// Only search specific tables in pyproject.toml (case-insensitive).
/// * `prod,dev,optional,groups` (default)
/// * `prod,dev,opt,group` or `p,d,o,g` (abbreviated)
/// * `prd,dev,extra,group` or `p,d,e,g` (optional / extra)
#[arg(
long,
env = EnvVars::UV_UPGRADE_TYPES,
value_delimiter = ',',
value_parser = parse_dependency_type,
)]
pub types: Vec<Maybe<DependencyType>>,
#[arg(long, short, alias = "build-constraint", env = EnvVars::UV_BUILD_CONSTRAINT, value_delimiter = ' ', value_parser = parse_maybe_file_path)]
pub build_constraints: Vec<Maybe<PathBuf>>,
/// The Python interpreter to use during resolution (overrides pyproject.toml).
///
/// A Python interpreter is required for building source distributions to determine package
@ -1085,6 +1101,18 @@ 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 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> {

View file

@ -717,6 +717,17 @@ impl EnvVars {
#[attr_hidden]
pub const UV_UPGRADE_RECURSIVE: &'static str = "UV_UPGRADE_RECURSIVE";
/// Which pyproject.toml tables should `uv upgrade` search?
///
/// Default `prod,dev,optional,groups`.
///
/// * prod: `project.dependencies`
/// * dev: `tool.uv.dev-dependencies`
/// * optional: `project.optional-dependencies`
/// * groups: `dependency-groups`
#[attr_hidden]
pub const UV_UPGRADE_TYPES: &'static str = "UV_UPGRADE_TYPES";
/// Overrides terminal width used for wrapping. This variable is not read by uv directly.
///
/// This is a quasi-standard variable, described, e.g., in `ncurses(3x)`.

View file

@ -1686,6 +1686,40 @@ pub enum DependencyType {
Group(GroupName),
}
impl DependencyType {
pub fn iter() -> [Self; 4] {
[
Self::Production,
Self::Dev,
Self::Optional(ExtraName::from_str("e").ok().unwrap()),
Self::Group(GroupName::from_str("g").ok().unwrap()),
]
}
}
impl FromStr for DependencyType {
type Err = DependencyTypeError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
// case-insensitive, allow abbreviations
match s.to_lowercase().as_str() {
"prod" | "prd" | "p" => Ok(Self::Production),
"dev" | "d" => Ok(Self::Dev),
"optional" | "opt" | "o" | "extra" | "e" => {
Ok(Self::Optional(ExtraName::from_str("e").ok().unwrap()))
}
"groups" | "group" | "g" => Ok(Self::Group(GroupName::from_str("g").ok().unwrap())),
_ => Err(DependencyTypeError::Unknown(s.to_string())),
}
}
}
#[derive(Debug, Error)]
pub enum DependencyTypeError {
#[error("unknown value for: `{0}` (allowed: `prod,dev,optional,groups`)")]
Unknown(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(test, derive(Serialize))]
pub struct BuildBackendSettingsSchema;

View file

@ -8,7 +8,6 @@ use thiserror::Error;
use toml_edit::{
Array, ArrayOfTables, DocumentMut, Formatted, Item, RawString, Table, TomlError, Value,
};
use uv_cache_key::CanonicalUrl;
use uv_distribution_types::Index;
use uv_fs::PortablePath;
@ -1122,31 +1121,54 @@ impl PyProjectTomlMut {
>(
&mut self,
find_latest: &F,
tables: &[DependencyType],
) -> (
usize,
Vec<(usize, String, Requirement, Requirement, Version, bool)>,
Vec<(
usize,
String,
Requirement,
Requirement,
Version,
bool,
DependencyType,
)>,
) {
let mut all_upgrades = Vec::new();
let mut found = 0;
// Check `project.dependencies`
if let Some(item) = self
.project_mut()
.ok()
if let Some(item) = tables
.contains(&DependencyType::Production)
.then(|| {
self.project_mut()
.ok()
.flatten()
.and_then(|p| p.get_mut("dependencies"))
})
.flatten()
.and_then(|p| p.get_mut("dependencies"))
{
found += item.as_array().map_or(0, Array::len);
Self::replace_dependencies(find_latest, &mut all_upgrades, item).await;
Self::replace_dependencies(
find_latest,
&mut all_upgrades,
item,
&DependencyType::Production,
)
.await;
}
// Check `project.optional-dependencies`
if let Some(groups) = self
.project_mut()
.ok()
.flatten()
.and_then(|p| p.get_mut("optional-dependencies"))
.and_then(Item::as_table_like_mut)
if let Some(groups) = tables
.iter()
.find(|t| matches!(t, DependencyType::Optional(_)))
.and_then(|_| {
self.project_mut()
.ok()
.flatten()
.and_then(|p| p.get_mut("optional-dependencies"))
.and_then(Item::as_table_like_mut)
})
{
for (extra, item) in groups
.iter_mut()
@ -1154,16 +1176,26 @@ impl PyProjectTomlMut {
{
if let Some(_extra) = extra {
found += item.as_array().map_or(0, Array::len);
Self::replace_dependencies(find_latest, &mut all_upgrades, item).await;
Self::replace_dependencies(
find_latest,
&mut all_upgrades,
item,
&DependencyType::Optional(_extra),
)
.await;
}
}
}
// Check `dependency-groups`.
if let Some(groups) = self
.doc
.get_mut("dependency-groups")
.and_then(Item::as_table_like_mut)
if let Some(groups) = tables
.iter()
.find(|t| matches!(t, DependencyType::Group(_)))
.and_then(|_| {
self.doc
.get_mut("dependency-groups")
.and_then(Item::as_table_like_mut)
})
{
for (group, item) in groups
.iter_mut()
@ -1171,22 +1203,33 @@ impl PyProjectTomlMut {
{
if let Some(_group) = group {
found += item.as_array().map_or(0, Array::len);
Self::replace_dependencies(find_latest, &mut all_upgrades, item).await;
Self::replace_dependencies(
find_latest,
&mut all_upgrades,
item,
&DependencyType::Group(_group),
)
.await;
}
}
}
// Check `tool.uv.dev-dependencies`
if let Some(item) = self
.doc
.get_mut("tool")
.and_then(Item::as_table_mut)
.and_then(|tool| tool.get_mut("uv"))
.and_then(Item::as_table_mut)
.and_then(|uv| uv.get_mut("dev-dependencies"))
if let Some(item) = tables
.contains(&DependencyType::Dev)
.then(|| {
self.doc
.get_mut("tool")
.and_then(Item::as_table_mut)
.and_then(|tool| tool.get_mut("uv"))
.and_then(Item::as_table_mut)
.and_then(|uv| uv.get_mut("dev-dependencies"))
})
.flatten()
{
found += item.as_array().map_or(0, Array::len);
Self::replace_dependencies(find_latest, &mut all_upgrades, item).await;
Self::replace_dependencies(find_latest, &mut all_upgrades, item, &DependencyType::Dev)
.await;
}
(found, all_upgrades)
@ -1194,19 +1237,45 @@ impl PyProjectTomlMut {
async fn replace_dependencies<Fut: Future<Output = Option<Version>>, F: Fn(String) -> Fut>(
find_latest: &F,
all_upgrades: &mut Vec<(usize, String, Requirement, Requirement, Version, bool)>,
all_upgrades: &mut Vec<(
usize,
String,
Requirement,
Requirement,
Version,
bool,
DependencyType,
)>,
item: &mut Item,
dependency_type: &DependencyType,
) {
if let Some(dependencies) = item.as_array_mut().filter(|d| !d.is_empty()) {
Self::replace_upgrades(find_latest, all_upgrades, dependencies).await;
Self::replace_upgrades(find_latest, all_upgrades, dependencies, dependency_type).await;
}
}
async fn find_upgrades<Fut: Future<Output = Option<Version>>, F: Fn(String) -> Fut>(
find_latest: F,
dependencies: &mut Array,
all_upgrades: &[(usize, String, Requirement, Requirement, Version, bool)],
) -> Vec<(usize, String, Requirement, Requirement, Version, bool)> {
all_upgrades: &[(
usize,
String,
Requirement,
Requirement,
Version,
bool,
DependencyType,
)],
dependency_type: &DependencyType,
) -> Vec<(
usize,
String,
Requirement,
Requirement,
Version,
bool,
DependencyType,
)> {
let mut upgrades = Vec::new();
for (i, dep) in dependencies.iter().enumerate() {
let Some(mut req) = dep.as_str().and_then(try_parse_requirement) else {
@ -1220,9 +1289,9 @@ impl PyProjectTomlMut {
};
if let Some(upgrade) = match all_upgrades
.iter()
.find(|(_, _, _, r, _, _)| r.name == req.name)
.find(|(_, _, _, r, _, _, _)| r.name == req.name)
{
Some((_, _, _, _, v, _)) => Some(v.clone()), // reuse cached upgrade
Some((_, _, _, _, v, _, _)) => Some(v.clone()), // reuse cached upgrade
_ => find_latest(req.name.to_string())
.await
.filter(|latest| !version_specifiers.contains(latest)),
@ -1237,6 +1306,7 @@ impl PyProjectTomlMut {
req,
upgrade,
upgraded,
dependency_type.clone(),
));
}
}
@ -1246,11 +1316,21 @@ impl PyProjectTomlMut {
async fn replace_upgrades<Fut: Future<Output = Option<Version>>, F: Fn(String) -> Fut>(
find_latest: F,
all_upgrades: &mut Vec<(usize, String, Requirement, Requirement, Version, bool)>,
all_upgrades: &mut Vec<(
usize,
String,
Requirement,
Requirement,
Version,
bool,
DependencyType,
)>,
dependencies: &mut Array,
dependency_type: &DependencyType,
) {
let upgrades = Self::find_upgrades(find_latest, dependencies, all_upgrades).await;
for (i, _dep, _old, new, _upgrade, _upgraded) in &upgrades {
let upgrades =
Self::find_upgrades(find_latest, dependencies, all_upgrades, dependency_type).await;
for (i, _dep, _old, new, _upgrade, _upgraded, _) in &upgrades {
let string = new.to_string();
dependencies.replace(*i, toml_edit::Value::from(string));
}

View file

@ -5,6 +5,9 @@ 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;
use anyhow::Result;
use owo_colors::OwoColorize;
use prettytable::format::FormatBuilder;
@ -21,17 +24,23 @@ use uv_pep440::{Version, VersionSpecifiers};
use uv_pep508::{PackageName, Requirement};
use uv_resolver::{PrereleaseMode, RequiresPython};
use uv_warnings::warn_user;
use uv_workspace::pyproject::DependencyType;
use uv_workspace::pyproject_mut::{DependencyTarget, PyProjectTomlMut};
use walkdir::WalkDir;
use crate::commands::ExitStatus;
use crate::commands::pip::latest::LatestClient;
use crate::printer::Printer;
/// 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> {
let tables: Vec<_> = match args
.types
.iter()
.filter_map(|t| t.clone().into_option())
.collect::<Vec<_>>()
{
tables if !tables.is_empty() => tables,
_ => DependencyType::iter().to_vec(),
};
let tomls = match args
.recursive
.then(|| search_pyproject_tomls(Path::new(".")))
@ -131,7 +140,7 @@ pub(crate) async fn upgrade_project_dependencies(args: UpgradeProjectArgs) -> Re
format!("{}/", &toml_dir[2..])
};
let subpath = format!("{relative}pyproject.toml");
let (found, upgrades) = toml.upgrade_all_dependencies(&find_latest).await;
let (found, upgrades) = toml.upgrade_all_dependencies(&find_latest, &tables).await;
let bumped = upgrades.len();
all_found += found;
all_bumped += bumped;
@ -158,7 +167,7 @@ pub(crate) async fn upgrade_project_dependencies(args: UpgradeProjectArgs) -> Re
"upgraded {subpath}{}",
if args.dry_run { " (dry run)" } else { "" }
);
table.add_row(row![r->"#", rb->"name", Fr->"-old", bFg->"+new", "latest", dry_run]); // diff-like
table.add_row(row![r->"#", rb->"name", Fr->"-old", bFg->"+new", "latest", "type", dry_run]); // diff-like
let remove_spaces = |v: &Requirement| {
v.clone()
.version_or_url
@ -169,12 +178,18 @@ pub(crate) async fn upgrade_project_dependencies(args: UpgradeProjectArgs) -> Re
upgrades
.iter()
.enumerate()
.for_each(|(i, (_, _dep, old, new, version, upgraded))| {
.for_each(|(i, (_, _dep, old, new, version, upgraded, dependency_type))| {
let from = remove_spaces(old);
let to = remove_spaces(new);
let upordown = if *upgraded { "✅ up" } else { "❌ down" };
let _type = match dependency_type {
DependencyType::Production => "prod".into(),
DependencyType::Dev => "dev".into(),
DependencyType::Optional(extra) => format!("{extra} [extra]"),
DependencyType::Group(group) => format!("{group} [group]"),
};
table.add_row(
row![r->i + 1, rb->old.name, Fr->from, bFg->to, version.to_string(), upordown],
row![r->i + 1, rb->old.name, Fr->from, bFg->to, version.to_string(), _type, upordown],
);
});
table.printstd();
@ -219,7 +234,7 @@ fn search_pyproject_tomls(root: &Path) -> Result<Vec<String>, anyhow::Error> {
// Hint: Doesn't skip special folders like `build`, `dist` or `target`
let is_hidden_or_not_pyproject = |path: &Path| {
path.file_name().and_then(OsStr::to_str).is_some_and(|s| {
s.starts_with('.') || s.starts_with('_') || path.is_file() && s != "pyproject.toml"
s.starts_with('.') || s.starts_with('_') || s == "target" || path.is_file() && s != "pyproject.toml"
})
};

View file

@ -951,7 +951,7 @@ uv upgrade [OPTIONS]
<p>Can be provided multiple times.</p>
<p>Expects to receive either a hostname (e.g., <code>localhost</code>), a host-port pair (e.g., <code>localhost:8080</code>), or a URL (e.g., <code>https://localhost</code>).</p>
<p>WARNING: Hosts included in this list will not be verified against the system's certificate store. Only use <code>--allow-insecure-host</code> in a secure network with verified sources, as it bypasses SSL verification and could expose you to MITM attacks.</p>
<p>May also be set with the <code>UV_INSECURE_HOST</code> environment variable.</p></dd><dt id="uv-upgrade--cache-dir"><a href="#uv-upgrade--cache-dir"><code>--cache-dir</code></a> <i>cache-dir</i></dt><dd><p>Path to the cache directory.</p>
<p>May also be set with the <code>UV_INSECURE_HOST</code> environment variable.</p></dd><dt id="uv-upgrade--build-constraints"><a href="#uv-upgrade--build-constraints"><code>--build-constraints</code></a>, <code>--build-constraint</code>, <code>-b</code> <i>build-constraints</i></dt><dt id="uv-upgrade--cache-dir"><a href="#uv-upgrade--cache-dir"><code>--cache-dir</code></a> <i>cache-dir</i></dt><dd><p>Path to the cache directory.</p>
<p>Defaults to <code>$XDG_CACHE_HOME/uv</code> or <code>$HOME/.cache/uv</code> on macOS and Linux, and <code>%LOCALAPPDATA%\uv\cache</code> on Windows.</p>
<p>To view the location of the cache directory, run <code>uv cache dir</code>.</p>
<p>May also be set with the <code>UV_CACHE_DIR</code> environment variable.</p></dd><dt id="uv-upgrade--color"><a href="#uv-upgrade--color"><code>--color</code></a> <i>color-choice</i></dt><dd><p>Control the use of color in output.</p>
@ -997,7 +997,8 @@ 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--verbose"><a href="#uv-upgrade--verbose"><code>--verbose</code></a>, <code>-v</code></dt><dd><p>Use verbose output.</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 (case-insensitive). * <code>prod,dev,optional,groups</code> (default) * <code>prod,dev,opt,group</code> or <code>p,d,o,g</code> (abbreviated) * <code>prd,dev,extra,group</code> or <code>p,d,e,g</code> (optional / extra)</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>