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:
Charlie Marsh
2024-11-08 15:13:30 -05:00
committed by GitHub
parent 0b5a06194b
commit b5a3d09958
4 changed files with 138 additions and 21 deletions
+52
View File
@@ -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;
+45
View File
@@ -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
View File
@@ -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)]
+26 -7
View File
@@ -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, ..