Add `--marker` flag to `uv add` (#12012)

## Summary

Add a `--marker` flag to `uv add` which applies a marker to all given
requirements.

Example:

```
$ uv-debug add --marker "platform_machine == 'x86_64'" \
    "anyio>=2.31.0" \
    "iniconfig>=2; sys_platform != 'win32'" \
    "numpy>1.19; sys_platform == 'win32'"
```

```toml
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.12.0"
dependencies = [
    "anyio>=2.31.0 ; platform_machine == 'x86_64'",
    "iniconfig>=2 ; platform_machine == 'x86_64' and sys_platform != 'win32'",
    "numpy>1.19 ; platform_machine == 'x86_64' and sys_platform == 'win32'",
]
```

Fixes https://github.com/astral-sh/uv/issues/11987


## Test Plan

Added snapshot tests

---------

Co-authored-by: konstin <konstin@mailbox.org>
This commit is contained in:
justin 2025-03-11 09:29:36 -06:00 committed by GitHub
parent d660882b8d
commit c48af312ae
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 119 additions and 5 deletions

View File

@ -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<PathBuf>,
/// Apply this marker to all added packages.
#[arg(long, short, value_parser = MarkerTree::from_str)]
pub marker: Option<MarkerTree>,
/// Add the requirements to the development dependency group.
///
/// This option is an alias for `--group dev`.

View File

@ -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<bool>,
no_sync: bool,
requirements: Vec<RequirementsSource>,
marker: Option<MarkerTree>,
editable: Option<bool>,
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<MarkerTree>,
) -> 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 {

View File

@ -1647,6 +1647,7 @@ async fn run_project(
args.active,
args.no_sync,
requirements,
args.marker,
args.editable,
args.dependency_type,
args.raw_sources,

View File

@ -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<String>,
pub(crate) requirements: Vec<PathBuf>,
pub(crate) marker: Option<MarkerTree>,
pub(crate) dependency_type: DependencyType,
pub(crate) editable: Option<bool>,
pub(crate) extras: Vec<ExtraName>,
@ -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,

View File

@ -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<()> {

View File

@ -951,6 +951,8 @@ uv add [OPTIONS] <PACKAGES|--requirements <REQUIREMENTS>>
<p>Requires that the lockfile is up-to-date. If the lockfile is missing or needs to be updated, uv will exit with an error.</p>
<p>May also be set with the <code>UV_LOCKED</code> environment variable.</p>
</dd><dt id="uv-add--marker"><a href="#uv-add--marker"><code>--marker</code></a>, <code>-m</code> <i>marker</i></dt><dd><p>Apply this marker to all added packages</p>
</dd><dt id="uv-add--native-tls"><a href="#uv-add--native-tls"><code>--native-tls</code></a></dt><dd><p>Whether to load TLS certificates from the platform&#8217;s native certificate store.</p>
<p>By default, uv loads certificates from the bundled <code>webpki-roots</code> crate. The <code>webpki-roots</code> are a reliable set of trust roots from Mozilla, and including them in uv improves portability and performance (especially on macOS).</p>