Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for -c constraints in uv add #12209

Merged
merged 1 commit into from
Mar 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions crates/uv-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3289,6 +3289,16 @@ pub struct AddArgs {
#[arg(long, short, alias = "requirement", group = "sources", value_parser = parse_file_path)]
pub requirements: Vec<PathBuf>,

/// Constrain versions using the given requirements files.
///
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably note these won't be added to the pyproject.toml and explain what final effect they have here?

/// Constraints files are `requirements.txt`-like files that only control the _version_ of a
/// requirement that's installed. The constraints will _not_ be added to the project's
/// `pyproject.toml` file, but _will_ be respected during dependency resolution.
///
/// This is equivalent to pip's `--constraint` option.
#[arg(long, short, alias = "constraint", env = EnvVars::UV_CONSTRAINT, value_delimiter = ' ', value_parser = parse_maybe_file_path)]
pub constraints: Vec<Maybe<PathBuf>>,

/// Apply this marker to all added packages.
#[arg(long, short, value_parser = MarkerTree::from_str)]
pub marker: Option<MarkerTree>,
Expand Down
31 changes: 24 additions & 7 deletions crates/uv/src/commands/project/add.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use std::collections::hash_map::Entry;
use std::collections::BTreeMap;
use std::fmt::Write;
use std::io;
use std::path::Path;
Expand All @@ -21,7 +22,9 @@ use uv_configuration::{
};
use uv_dispatch::BuildDispatch;
use uv_distribution::DistributionDatabase;
use uv_distribution_types::{Index, IndexName, IndexUrls, UnresolvedRequirement, VersionId};
use uv_distribution_types::{
Index, IndexName, IndexUrls, NameRequirementSpecification, UnresolvedRequirement, VersionId,
};
use uv_fs::Simplified;
use uv_git::GIT_STORE;
use uv_git_types::GitReference;
Expand Down Expand Up @@ -64,6 +67,7 @@ pub(crate) async fn add(
active: Option<bool>,
no_sync: bool,
requirements: Vec<RequirementsSource>,
constraints: Vec<RequirementsSource>,
marker: Option<MarkerTree>,
editable: Option<bool>,
dependency_type: DependencyType,
Expand Down Expand Up @@ -258,8 +262,18 @@ pub(crate) async fn add(
.allow_insecure_host(network_settings.allow_insecure_host.clone());

// Read the requirements.
let RequirementsSpecification { requirements, .. } =
RequirementsSpecification::from_simple_sources(&requirements, &client_builder).await?;
let RequirementsSpecification {
requirements,
constraints,
..
} = RequirementsSpecification::from_sources(
&requirements,
&constraints,
&[],
BTreeMap::default(),
&client_builder,
)
.await?;

// Initialize any shared state.
let state = PlatformState::default();
Expand Down Expand Up @@ -646,6 +660,7 @@ pub(crate) async fn add(
locked,
&dependency_type,
raw_sources,
constraints,
&settings,
&network_settings,
installer_metadata,
Expand Down Expand Up @@ -682,6 +697,7 @@ async fn lock_and_sync(
locked: bool,
dependency_type: &DependencyType,
raw_sources: bool,
constraints: Vec<NameRequirementSpecification>,
settings: &ResolverInstallerSettings,
network_settings: &NetworkSettings,
installer_metadata: bool,
Expand All @@ -690,13 +706,12 @@ async fn lock_and_sync(
printer: Printer,
preview: PreviewMode,
) -> Result<(), ProjectError> {
let mut lock = project::lock::do_safe_lock(
let mut lock = project::lock::LockOperation::new(
if locked {
LockMode::Locked(target.interpreter())
} else {
LockMode::Write(target.interpreter())
},
(&target).into(),
&settings.resolver,
network_settings,
&lock_state,
Expand All @@ -706,6 +721,8 @@ async fn lock_and_sync(
printer,
preview,
)
.with_constraints(constraints)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might need to propagate --marker to the constraints, wdyt? I think it's needed for importing a platform-specific lock (e.g., requirements-win.txt)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain the command that would be run and why we need to propagate?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pip-compile does not add markers to its single platform output. So pip-compile on Windows will produce a Windows specific lockfile without markers.

To import this, we need to gate the lock to win32, e.g.:

uv add -r requirements-win.in -c requirements-win.txt --marker "sys_platform == 'win32'"

I think in reality, this is actually more complicated :/ because you probably have a single requirements.in and multiple lockfiles. What you really need is:

uv add -r requirements.in \
    -c "requirements-win.txt; sys_platform == 'win32'" \
    -c "requirements-linux.txt; sys_platform == 'linux'"

--marker doesn't work since it's applied to the input requirements.

For a concrete example, here's what I have in my pip migration draft...

For example, take a simple dependency:

tqdm

On Linux, this compiles to:

tqdm==4.67.1
    # via -r requirements.in

While on Windows, this compiles to:

colorama==0.4.6
    # via tqdm
tqdm==4.67.1
    # via -r requirements.in

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if separating input requirements is common. I don't think we should focus on support for that.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm ok... We can add the marker... It should be harmless...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Followed-up synchronously because I'm not sure what's best here)

I think not respecting --marker to start is fine. I'm not sure what to do for that use-case yet.

.execute((&target).into())
.await?
.into_lock();

Expand Down Expand Up @@ -808,13 +825,12 @@ async fn lock_and_sync(

// If the file was modified, we have to lock again, though the only expected change is
// the addition of the minimum version specifiers.
lock = project::lock::do_safe_lock(
lock = project::lock::LockOperation::new(
if locked {
LockMode::Locked(target.interpreter())
} else {
LockMode::Write(target.interpreter())
},
(&target).into(),
&settings.resolver,
network_settings,
&lock_state,
Expand All @@ -824,6 +840,7 @@ async fn lock_and_sync(
printer,
preview,
)
.execute((&target).into())
.await?
.into_lock();
}
Expand Down
6 changes: 3 additions & 3 deletions crates/uv/src/commands/project/export.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ use uv_workspace::{DiscoveryOptions, MemberDiscovery, VirtualProject, Workspace,

use crate::commands::pip::loggers::DefaultResolveLogger;
use crate::commands::project::install_target::InstallTarget;
use crate::commands::project::lock::{do_safe_lock, LockMode};
use crate::commands::project::lock::{LockMode, LockOperation};
use crate::commands::project::lock_target::LockTarget;
use crate::commands::project::{
default_dependency_groups, detect_conflicts, ProjectError, ProjectInterpreter,
Expand Down Expand Up @@ -169,9 +169,8 @@ pub(crate) async fn export(
let state = UniversalState::default();

// Lock the project.
let lock = match do_safe_lock(
let lock = match LockOperation::new(
mode,
(&target).into(),
&settings,
&network_settings,
&state,
Expand All @@ -181,6 +180,7 @@ pub(crate) async fn export(
printer,
preview,
)
.execute((&target).into())
.await
{
Ok(result) => result.into_lock(),
Expand Down
Loading
Loading