Skip to content

Commit b5a3d09

Browse files
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.
1 parent 0b5a061 commit b5a3d09

File tree

4 files changed

+138
-21
lines changed

4 files changed

+138
-21
lines changed

crates/uv-cli/src/comma.rs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
use std::str::FromStr;
2+
3+
/// A comma-separated string of requirements, e.g., `"flask,anyio"`, that takes extras into account
4+
/// (i.e., treats `"psycopg[binary,pool]"` as a single requirement).
5+
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
6+
pub struct CommaSeparatedRequirements(Vec<String>);
7+
8+
impl IntoIterator for CommaSeparatedRequirements {
9+
type Item = String;
10+
type IntoIter = std::vec::IntoIter<Self::Item>;
11+
12+
fn into_iter(self) -> Self::IntoIter {
13+
self.0.into_iter()
14+
}
15+
}
16+
17+
impl FromStr for CommaSeparatedRequirements {
18+
type Err = String;
19+
20+
fn from_str(input: &str) -> Result<Self, Self::Err> {
21+
// Split on commas _outside_ of brackets.
22+
let mut requirements = Vec::new();
23+
let mut depth = 0usize;
24+
let mut start = 0usize;
25+
for (i, c) in input.char_indices() {
26+
match c {
27+
'[' => {
28+
depth = depth.saturating_add(1);
29+
}
30+
']' => {
31+
depth = depth.saturating_sub(1);
32+
}
33+
',' if depth == 0 => {
34+
let requirement = input[start..i].trim().to_string();
35+
if !requirement.is_empty() {
36+
requirements.push(requirement);
37+
}
38+
start = i + ','.len_utf8();
39+
}
40+
_ => {}
41+
}
42+
}
43+
let requirement = input[start..].trim().to_string();
44+
if !requirement.is_empty() {
45+
requirements.push(requirement);
46+
}
47+
Ok(Self(requirements))
48+
}
49+
}
50+
51+
#[cfg(test)]
52+
mod tests;

crates/uv-cli/src/comma/tests.rs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
use super::CommaSeparatedRequirements;
2+
use std::str::FromStr;
3+
4+
#[test]
5+
fn single() {
6+
assert_eq!(
7+
CommaSeparatedRequirements::from_str("flask").unwrap(),
8+
CommaSeparatedRequirements(vec!["flask".to_string()])
9+
);
10+
}
11+
12+
#[test]
13+
fn double() {
14+
assert_eq!(
15+
CommaSeparatedRequirements::from_str("flask,anyio").unwrap(),
16+
CommaSeparatedRequirements(vec!["flask".to_string(), "anyio".to_string()])
17+
);
18+
}
19+
20+
#[test]
21+
fn empty() {
22+
assert_eq!(
23+
CommaSeparatedRequirements::from_str("flask,,anyio").unwrap(),
24+
CommaSeparatedRequirements(vec!["flask".to_string(), "anyio".to_string()])
25+
);
26+
}
27+
28+
#[test]
29+
fn single_extras() {
30+
assert_eq!(
31+
CommaSeparatedRequirements::from_str("psycopg[binary,pool]").unwrap(),
32+
CommaSeparatedRequirements(vec!["psycopg[binary,pool]".to_string()])
33+
);
34+
}
35+
36+
#[test]
37+
fn double_extras() {
38+
assert_eq!(
39+
CommaSeparatedRequirements::from_str("psycopg[binary,pool], flask").unwrap(),
40+
CommaSeparatedRequirements(vec![
41+
"psycopg[binary,pool]".to_string(),
42+
"flask".to_string()
43+
])
44+
);
45+
}

crates/uv-cli/src/lib.rs

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ use uv_python::{PythonDownloads, PythonPreference, PythonVersion};
2222
use uv_resolver::{AnnotationStyle, ExcludeNewer, PrereleaseMode, ResolutionMode};
2323
use uv_static::EnvVars;
2424

25+
pub mod comma;
2526
pub mod compat;
2627
pub mod options;
2728
pub mod version;
@@ -2674,16 +2675,16 @@ pub struct RunArgs {
26742675
/// When used in a project, these dependencies will be layered on top of
26752676
/// the project environment in a separate, ephemeral environment. These
26762677
/// dependencies are allowed to conflict with those specified by the project.
2677-
#[arg(long, value_delimiter = ',')]
2678-
pub with: Vec<String>,
2678+
#[arg(long)]
2679+
pub with: Vec<comma::CommaSeparatedRequirements>,
26792680

26802681
/// Run with the given packages installed as editables.
26812682
///
26822683
/// When used in a project, these dependencies will be layered on top of
26832684
/// the project environment in a separate, ephemeral environment. These
26842685
/// dependencies are allowed to conflict with those specified by the project.
2685-
#[arg(long, value_delimiter = ',')]
2686-
pub with_editable: Vec<String>,
2686+
#[arg(long)]
2687+
pub with_editable: Vec<comma::CommaSeparatedRequirements>,
26872688

26882689
/// Run with all packages listed in the given `requirements.txt` files.
26892690
///
@@ -3620,16 +3621,16 @@ pub struct ToolRunArgs {
36203621
pub from: Option<String>,
36213622

36223623
/// Run with the given packages installed.
3623-
#[arg(long, value_delimiter = ',')]
3624-
pub with: Vec<String>,
3624+
#[arg(long)]
3625+
pub with: Vec<comma::CommaSeparatedRequirements>,
36253626

36263627
/// Run with the given packages installed as editables
36273628
///
36283629
/// When used in a project, these dependencies will be layered on top of
36293630
/// the uv tool's environment in a separate, ephemeral environment. These
36303631
/// dependencies are allowed to conflict with those specified.
3631-
#[arg(long, value_delimiter = ',')]
3632-
pub with_editable: Vec<String>,
3632+
#[arg(long)]
3633+
pub with_editable: Vec<comma::CommaSeparatedRequirements>,
36333634

36343635
/// Run with all packages listed in the given `requirements.txt` files.
36353636
#[arg(long, value_delimiter = ',', value_parser = parse_maybe_file_path)]
@@ -3681,19 +3682,19 @@ pub struct ToolInstallArgs {
36813682
#[arg(short, long)]
36823683
pub editable: bool,
36833684

3684-
/// Include the given packages as editables.
3685-
#[arg(long, value_delimiter = ',')]
3686-
pub with_editable: Vec<String>,
3687-
36883685
/// The package to install commands from.
36893686
///
36903687
/// This option is provided for parity with `uv tool run`, but is redundant with `package`.
36913688
#[arg(long, hide = true)]
36923689
pub from: Option<String>,
36933690

36943691
/// Include the following extra requirements.
3695-
#[arg(long, value_delimiter = ',')]
3696-
pub with: Vec<String>,
3692+
#[arg(long)]
3693+
pub with: Vec<comma::CommaSeparatedRequirements>,
3694+
3695+
/// Include the given packages as editables.
3696+
#[arg(long)]
3697+
pub with_editable: Vec<comma::CommaSeparatedRequirements>,
36973698

36983699
/// Run all requirements listed in the given `requirements.txt` files.
36993700
#[arg(long, value_delimiter = ',', value_parser = parse_maybe_file_path)]

crates/uv/src/settings.rs

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use std::str::FromStr;
66

77
use url::Url;
88
use uv_cache::{CacheArgs, Refresh};
9+
use uv_cli::comma::CommaSeparatedRequirements;
910
use uv_cli::{
1011
options::{flag, resolver_installer_options, resolver_options},
1112
AuthorFrom, BuildArgs, ExportArgs, PublishArgs, PythonDirArgs, ToolUpgradeArgs,
@@ -314,8 +315,14 @@ impl RunSettings {
314315
dev, no_dev, only_dev, group, no_group, only_group,
315316
),
316317
editable: EditableMode::from_args(no_editable),
317-
with,
318-
with_editable,
318+
with: with
319+
.into_iter()
320+
.flat_map(CommaSeparatedRequirements::into_iter)
321+
.collect(),
322+
with_editable: with_editable
323+
.into_iter()
324+
.flat_map(CommaSeparatedRequirements::into_iter)
325+
.collect(),
319326
with_requirements: with_requirements
320327
.into_iter()
321328
.filter_map(Maybe::into_option)
@@ -398,8 +405,14 @@ impl ToolRunSettings {
398405
Self {
399406
command,
400407
from,
401-
with,
402-
with_editable,
408+
with: with
409+
.into_iter()
410+
.flat_map(CommaSeparatedRequirements::into_iter)
411+
.collect(),
412+
with_editable: with_editable
413+
.into_iter()
414+
.flat_map(CommaSeparatedRequirements::into_iter)
415+
.collect(),
403416
with_requirements: with_requirements
404417
.into_iter()
405418
.filter_map(Maybe::into_option)
@@ -463,8 +476,14 @@ impl ToolInstallSettings {
463476
Self {
464477
package,
465478
from,
466-
with,
467-
with_editable,
479+
with: with
480+
.into_iter()
481+
.flat_map(CommaSeparatedRequirements::into_iter)
482+
.collect(),
483+
with_editable: with_editable
484+
.into_iter()
485+
.flat_map(CommaSeparatedRequirements::into_iter)
486+
.collect(),
468487
with_requirements: with_requirements
469488
.into_iter()
470489
.filter_map(Maybe::into_option)
@@ -2635,7 +2654,7 @@ pub(crate) struct PublishSettings {
26352654
}
26362655

26372656
impl PublishSettings {
2638-
/// Resolve the [`crate::settings::PublishSettings`] from the CLI and filesystem configuration.
2657+
/// Resolve the [`PublishSettings`] from the CLI and filesystem configuration.
26392658
pub(crate) fn resolve(args: PublishArgs, filesystem: Option<FilesystemOptions>) -> Self {
26402659
let Options {
26412660
publish, top_level, ..

0 commit comments

Comments
 (0)