Respect comma-separated extras in --with (#8946)
## Summary We need to treat `flask,anyio` as two requirements, but `psycopg[binary,pool]` as a single requirement. Closes #8918.
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
/// A comma-separated string of requirements, e.g., `"flask,anyio"`, that takes extras into account
|
||||
/// (i.e., treats `"psycopg[binary,pool]"` as a single requirement).
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct CommaSeparatedRequirements(Vec<String>);
|
||||
|
||||
impl IntoIterator for CommaSeparatedRequirements {
|
||||
type Item = String;
|
||||
type IntoIter = std::vec::IntoIter<Self::Item>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.0.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for CommaSeparatedRequirements {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||
// Split on commas _outside_ of brackets.
|
||||
let mut requirements = Vec::new();
|
||||
let mut depth = 0usize;
|
||||
let mut start = 0usize;
|
||||
for (i, c) in input.char_indices() {
|
||||
match c {
|
||||
'[' => {
|
||||
depth = depth.saturating_add(1);
|
||||
}
|
||||
']' => {
|
||||
depth = depth.saturating_sub(1);
|
||||
}
|
||||
',' if depth == 0 => {
|
||||
let requirement = input[start..i].trim().to_string();
|
||||
if !requirement.is_empty() {
|
||||
requirements.push(requirement);
|
||||
}
|
||||
start = i + ','.len_utf8();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let requirement = input[start..].trim().to_string();
|
||||
if !requirement.is_empty() {
|
||||
requirements.push(requirement);
|
||||
}
|
||||
Ok(Self(requirements))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
@@ -0,0 +1,45 @@
|
||||
use super::CommaSeparatedRequirements;
|
||||
use std::str::FromStr;
|
||||
|
||||
#[test]
|
||||
fn single() {
|
||||
assert_eq!(
|
||||
CommaSeparatedRequirements::from_str("flask").unwrap(),
|
||||
CommaSeparatedRequirements(vec!["flask".to_string()])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn double() {
|
||||
assert_eq!(
|
||||
CommaSeparatedRequirements::from_str("flask,anyio").unwrap(),
|
||||
CommaSeparatedRequirements(vec!["flask".to_string(), "anyio".to_string()])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty() {
|
||||
assert_eq!(
|
||||
CommaSeparatedRequirements::from_str("flask,,anyio").unwrap(),
|
||||
CommaSeparatedRequirements(vec!["flask".to_string(), "anyio".to_string()])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_extras() {
|
||||
assert_eq!(
|
||||
CommaSeparatedRequirements::from_str("psycopg[binary,pool]").unwrap(),
|
||||
CommaSeparatedRequirements(vec!["psycopg[binary,pool]".to_string()])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn double_extras() {
|
||||
assert_eq!(
|
||||
CommaSeparatedRequirements::from_str("psycopg[binary,pool], flask").unwrap(),
|
||||
CommaSeparatedRequirements(vec![
|
||||
"psycopg[binary,pool]".to_string(),
|
||||
"flask".to_string()
|
||||
])
|
||||
);
|
||||
}
|
||||
+15
-14
@@ -22,6 +22,7 @@ use uv_python::{PythonDownloads, PythonPreference, PythonVersion};
|
||||
use uv_resolver::{AnnotationStyle, ExcludeNewer, PrereleaseMode, ResolutionMode};
|
||||
use uv_static::EnvVars;
|
||||
|
||||
pub mod comma;
|
||||
pub mod compat;
|
||||
pub mod options;
|
||||
pub mod version;
|
||||
@@ -2674,16 +2675,16 @@ pub struct RunArgs {
|
||||
/// When used in a project, these dependencies will be layered on top of
|
||||
/// the project environment in a separate, ephemeral environment. These
|
||||
/// dependencies are allowed to conflict with those specified by the project.
|
||||
#[arg(long, value_delimiter = ',')]
|
||||
pub with: Vec<String>,
|
||||
#[arg(long)]
|
||||
pub with: Vec<comma::CommaSeparatedRequirements>,
|
||||
|
||||
/// Run with the given packages installed as editables.
|
||||
///
|
||||
/// When used in a project, these dependencies will be layered on top of
|
||||
/// the project environment in a separate, ephemeral environment. These
|
||||
/// dependencies are allowed to conflict with those specified by the project.
|
||||
#[arg(long, value_delimiter = ',')]
|
||||
pub with_editable: Vec<String>,
|
||||
#[arg(long)]
|
||||
pub with_editable: Vec<comma::CommaSeparatedRequirements>,
|
||||
|
||||
/// Run with all packages listed in the given `requirements.txt` files.
|
||||
///
|
||||
@@ -3620,16 +3621,16 @@ pub struct ToolRunArgs {
|
||||
pub from: Option<String>,
|
||||
|
||||
/// Run with the given packages installed.
|
||||
#[arg(long, value_delimiter = ',')]
|
||||
pub with: Vec<String>,
|
||||
#[arg(long)]
|
||||
pub with: Vec<comma::CommaSeparatedRequirements>,
|
||||
|
||||
/// Run with the given packages installed as editables
|
||||
///
|
||||
/// When used in a project, these dependencies will be layered on top of
|
||||
/// the uv tool's environment in a separate, ephemeral environment. These
|
||||
/// dependencies are allowed to conflict with those specified.
|
||||
#[arg(long, value_delimiter = ',')]
|
||||
pub with_editable: Vec<String>,
|
||||
#[arg(long)]
|
||||
pub with_editable: Vec<comma::CommaSeparatedRequirements>,
|
||||
|
||||
/// Run with all packages listed in the given `requirements.txt` files.
|
||||
#[arg(long, value_delimiter = ',', value_parser = parse_maybe_file_path)]
|
||||
@@ -3681,10 +3682,6 @@ pub struct ToolInstallArgs {
|
||||
#[arg(short, long)]
|
||||
pub editable: bool,
|
||||
|
||||
/// Include the given packages as editables.
|
||||
#[arg(long, value_delimiter = ',')]
|
||||
pub with_editable: Vec<String>,
|
||||
|
||||
/// The package to install commands from.
|
||||
///
|
||||
/// This option is provided for parity with `uv tool run`, but is redundant with `package`.
|
||||
@@ -3692,8 +3689,12 @@ pub struct ToolInstallArgs {
|
||||
pub from: Option<String>,
|
||||
|
||||
/// Include the following extra requirements.
|
||||
#[arg(long, value_delimiter = ',')]
|
||||
pub with: Vec<String>,
|
||||
#[arg(long)]
|
||||
pub with: Vec<comma::CommaSeparatedRequirements>,
|
||||
|
||||
/// Include the given packages as editables.
|
||||
#[arg(long)]
|
||||
pub with_editable: Vec<comma::CommaSeparatedRequirements>,
|
||||
|
||||
/// Run all requirements listed in the given `requirements.txt` files.
|
||||
#[arg(long, value_delimiter = ',', value_parser = parse_maybe_file_path)]
|
||||
|
||||
@@ -6,6 +6,7 @@ use std::str::FromStr;
|
||||
|
||||
use url::Url;
|
||||
use uv_cache::{CacheArgs, Refresh};
|
||||
use uv_cli::comma::CommaSeparatedRequirements;
|
||||
use uv_cli::{
|
||||
options::{flag, resolver_installer_options, resolver_options},
|
||||
AuthorFrom, BuildArgs, ExportArgs, PublishArgs, PythonDirArgs, ToolUpgradeArgs,
|
||||
@@ -314,8 +315,14 @@ impl RunSettings {
|
||||
dev, no_dev, only_dev, group, no_group, only_group,
|
||||
),
|
||||
editable: EditableMode::from_args(no_editable),
|
||||
with,
|
||||
with_editable,
|
||||
with: with
|
||||
.into_iter()
|
||||
.flat_map(CommaSeparatedRequirements::into_iter)
|
||||
.collect(),
|
||||
with_editable: with_editable
|
||||
.into_iter()
|
||||
.flat_map(CommaSeparatedRequirements::into_iter)
|
||||
.collect(),
|
||||
with_requirements: with_requirements
|
||||
.into_iter()
|
||||
.filter_map(Maybe::into_option)
|
||||
@@ -398,8 +405,14 @@ impl ToolRunSettings {
|
||||
Self {
|
||||
command,
|
||||
from,
|
||||
with,
|
||||
with_editable,
|
||||
with: with
|
||||
.into_iter()
|
||||
.flat_map(CommaSeparatedRequirements::into_iter)
|
||||
.collect(),
|
||||
with_editable: with_editable
|
||||
.into_iter()
|
||||
.flat_map(CommaSeparatedRequirements::into_iter)
|
||||
.collect(),
|
||||
with_requirements: with_requirements
|
||||
.into_iter()
|
||||
.filter_map(Maybe::into_option)
|
||||
@@ -463,8 +476,14 @@ impl ToolInstallSettings {
|
||||
Self {
|
||||
package,
|
||||
from,
|
||||
with,
|
||||
with_editable,
|
||||
with: with
|
||||
.into_iter()
|
||||
.flat_map(CommaSeparatedRequirements::into_iter)
|
||||
.collect(),
|
||||
with_editable: with_editable
|
||||
.into_iter()
|
||||
.flat_map(CommaSeparatedRequirements::into_iter)
|
||||
.collect(),
|
||||
with_requirements: with_requirements
|
||||
.into_iter()
|
||||
.filter_map(Maybe::into_option)
|
||||
@@ -2635,7 +2654,7 @@ pub(crate) struct PublishSettings {
|
||||
}
|
||||
|
||||
impl PublishSettings {
|
||||
/// Resolve the [`crate::settings::PublishSettings`] from the CLI and filesystem configuration.
|
||||
/// Resolve the [`PublishSettings`] from the CLI and filesystem configuration.
|
||||
pub(crate) fn resolve(args: PublishArgs, filesystem: Option<FilesystemOptions>) -> Self {
|
||||
let Options {
|
||||
publish, top_level, ..
|
||||
|
||||
Reference in New Issue
Block a user