diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index d168adf50..af874e3e4 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -16,7 +16,7 @@ use uv_configuration::{ }; use uv_distribution_types::{Index, IndexUrl, Origin, PipExtraIndex, PipFindLinks, PipIndex}; use uv_normalize::{ExtraName, GroupName, PackageName}; -use uv_pep508::Requirement; +use uv_pep508::{MarkerTree, Requirement}; use uv_pypi_types::VerbatimParsedUrl; use uv_python::{PythonDownloads, PythonPreference, PythonVersion}; use uv_resolver::{AnnotationStyle, ExcludeNewer, ForkStrategy, PrereleaseMode, ResolutionMode}; @@ -3272,6 +3272,10 @@ pub struct AddArgs { #[arg(long, short, alias = "requirement", group = "sources", value_parser = parse_file_path)] pub requirements: Vec, + /// Apply this marker to all added packages. + #[arg(long, short, value_parser = MarkerTree::from_str)] + pub marker: Option, + /// Add the requirements to the development dependency group. /// /// This option is an alias for `--group dev`. diff --git a/crates/uv/src/commands/project/add.rs b/crates/uv/src/commands/project/add.rs index 3657bbcb4..b1f560e37 100644 --- a/crates/uv/src/commands/project/add.rs +++ b/crates/uv/src/commands/project/add.rs @@ -26,7 +26,7 @@ use uv_fs::Simplified; use uv_git::GIT_STORE; use uv_git_types::GitReference; use uv_normalize::{PackageName, DEV_DEPENDENCIES}; -use uv_pep508::{ExtraName, Requirement, UnnamedRequirement, VersionOrUrl}; +use uv_pep508::{ExtraName, MarkerTree, Requirement, UnnamedRequirement, VersionOrUrl}; use uv_pypi_types::{redact_credentials, ParsedUrl, RequirementSource, VerbatimParsedUrl}; use uv_python::{Interpreter, PythonDownloads, PythonEnvironment, PythonPreference, PythonRequest}; use uv_requirements::{NamedRequirementsResolver, RequirementsSource, RequirementsSpecification}; @@ -64,6 +64,7 @@ pub(crate) async fn add( active: Option, no_sync: bool, requirements: Vec, + marker: Option, editable: Option, dependency_type: DependencyType, raw_sources: bool, @@ -274,6 +275,7 @@ pub(crate) async fn add( rev.as_deref(), tag.as_deref(), branch.as_deref(), + marker, ) }) .partition_map(|requirement| match requirement { @@ -896,10 +898,17 @@ fn augment_requirement( rev: Option<&str>, tag: Option<&str>, branch: Option<&str>, + marker: Option, ) -> UnresolvedRequirement { match requirement { - UnresolvedRequirement::Named(requirement) => { + UnresolvedRequirement::Named(mut requirement) => { UnresolvedRequirement::Named(uv_pypi_types::Requirement { + marker: marker + .map(|marker| { + requirement.marker.and(marker); + requirement.marker + }) + .unwrap_or(requirement.marker), source: match requirement.source { RequirementSource::Git { git, @@ -926,8 +935,14 @@ fn augment_requirement( ..requirement }) } - UnresolvedRequirement::Unnamed(requirement) => { + UnresolvedRequirement::Unnamed(mut requirement) => { UnresolvedRequirement::Unnamed(UnnamedRequirement { + marker: marker + .map(|marker| { + requirement.marker.and(marker); + requirement.marker + }) + .unwrap_or(requirement.marker), url: match requirement.url.parsed_url { ParsedUrl::Git(mut git) => { let reference = if let Some(rev) = rev { diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index ccb0d8556..83803d338 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -1647,6 +1647,7 @@ async fn run_project( args.active, args.no_sync, requirements, + args.marker, args.editable, args.dependency_type, args.raw_sources, diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index f77f093dd..d98f0e918 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -31,7 +31,7 @@ use uv_configuration::{ use uv_distribution_types::{DependencyMetadata, Index, IndexLocations, IndexUrl}; use uv_install_wheel::LinkMode; use uv_normalize::PackageName; -use uv_pep508::{ExtraName, RequirementOrigin}; +use uv_pep508::{ExtraName, MarkerTree, RequirementOrigin}; use uv_pypi_types::{Requirement, SupportedEnvironments}; use uv_python::{Prefix, PythonDownloads, PythonPreference, PythonVersion, Target}; use uv_resolver::{ @@ -1167,6 +1167,7 @@ pub(crate) struct AddSettings { pub(crate) no_sync: bool, pub(crate) packages: Vec, pub(crate) requirements: Vec, + pub(crate) marker: Option, pub(crate) dependency_type: DependencyType, pub(crate) editable: Option, pub(crate) extras: Vec, @@ -1190,6 +1191,7 @@ impl AddSettings { let AddArgs { packages, requirements, + marker, dev, optional, group, @@ -1280,6 +1282,7 @@ impl AddSettings { no_sync, packages, requirements, + marker, dependency_type, raw_sources, rev, diff --git a/crates/uv/tests/it/edit.rs b/crates/uv/tests/it/edit.rs index 9d74e7120..6ac647229 100644 --- a/crates/uv/tests/it/edit.rs +++ b/crates/uv/tests/it/edit.rs @@ -4736,6 +4736,95 @@ fn add_requirements_file() -> Result<()> { Ok(()) } +/// Add requirements from a file with a marker flag. +/// +/// We test that: +/// * Adding requirements from a file applies the marker to all of them +/// * We combine the marker with existing markers +/// * We only sync the packages applicable under this marker +#[test] +fn add_requirements_file_with_marker_flag() -> Result<()> { + let context = TestContext::new("3.12"); + + let requirements_win_txt = context.temp_dir.child("requirements.win.txt"); + requirements_win_txt.write_str("anyio>=2.31.0\niniconfig>=2; sys_platform != 'fantasy_os'\nnumpy>1.9; sys_platform == 'fantasy_os'")?; + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + let base_pyproject_toml = indoc! {r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [] + "#}; + pyproject_toml.write_str(base_pyproject_toml)?; + + // Add dependencies with a marker that does not apply for the current target. + uv_snapshot!(context.filters(), context.add().arg("-r").arg("requirements.win.txt").arg("-m").arg("python_version == '3.11'"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Audited in [TIME] + "); + let edited_pyproject_toml = context.read("pyproject.toml"); + + // TODO(konsti): We should output `python_version == '3.12'` instead of lowering to + // `python_full_version`. + assert_snapshot!( + edited_pyproject_toml, @r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [ + "anyio>=2.31.0 ; python_full_version == '3.11.*'", + "iniconfig>=2 ; python_full_version == '3.11.*' and sys_platform != 'fantasy_os'", + "numpy>1.9 ; python_full_version == '3.11.*' and sys_platform == 'fantasy_os'", + ] + "# + ); + + // Reset the project. + pyproject_toml.write_str(base_pyproject_toml)?; + fs_err::remove_file(context.temp_dir.join("uv.lock"))?; + + // Add dependencies with a marker that applies for the current target. + uv_snapshot!(context.filters(), context.add().arg("-r").arg("requirements.win.txt").arg("-m").arg("python_version == '3.12'"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 6 packages in [TIME] + Prepared 4 packages in [TIME] + Installed 4 packages in [TIME] + + anyio==4.3.0 + + idna==3.6 + + iniconfig==2.0.0 + + sniffio==1.3.1 + "); + let edited_pyproject_toml = context.read("pyproject.toml"); + + assert_snapshot!( + edited_pyproject_toml, @r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [ + "anyio>=2.31.0 ; python_full_version == '3.12.*'", + "iniconfig>=2 ; python_full_version == '3.12.*' and sys_platform != 'fantasy_os'", + "numpy>1.9 ; python_full_version == '3.12.*' and sys_platform == 'fantasy_os'", + ] + "# + ); + + Ok(()) +} + /// Add a requirement to a dependency group. #[test] fn add_group() -> Result<()> { diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 79c90ae36..32473a03d 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -951,6 +951,8 @@ uv add [OPTIONS] >

Requires that the lockfile is up-to-date. If the lockfile is missing or needs to be updated, uv will exit with an error.

May also be set with the UV_LOCKED environment variable.

+
--marker, -m marker

Apply this marker to all added packages

+
--native-tls

Whether to load TLS certificates from the platform’s native certificate store.

By default, uv loads certificates from the bundled webpki-roots crate. The webpki-roots are a reliable set of trust roots from Mozilla, and including them in uv improves portability and performance (especially on macOS).