Skip to content

Commit

Permalink
Respect comma-separated extras in --with (#8946)
Browse files Browse the repository at this point in the history
## Summary

We need to treat `flask,anyio` as two requirements, but
`psycopg[binary,pool]` as a single requirement.

Closes #8918.
  • Loading branch information
charliermarsh authored Nov 8, 2024
1 parent 0b5a061 commit b5a3d09
Show file tree
Hide file tree
Showing 4 changed files with 138 additions and 21 deletions.
52 changes: 52 additions & 0 deletions crates/uv-cli/src/comma.rs
Original file line number Diff line number Diff line change
@@ -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 changes: 45 additions & 0 deletions crates/uv-cli/src/comma/tests.rs
Original file line number Diff line number Diff line change
@@ -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()
])
);
}
29 changes: 15 additions & 14 deletions crates/uv-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.
///
Expand Down Expand Up @@ -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)]
Expand Down Expand Up @@ -3681,19 +3682,19 @@ 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`.
#[arg(long, hide = true)]
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)]
Expand Down
33 changes: 26 additions & 7 deletions crates/uv/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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, ..
Expand Down

0 comments on commit b5a3d09

Please sign in to comment.