Skip to content

Commit

Permalink
Add support for requirements files, constraints, and overrides in `uv…
Browse files Browse the repository at this point in the history
… run`
  • Loading branch information
zanieb committed Jul 11, 2024
1 parent 528a5a5 commit 4d81e5c
Show file tree
Hide file tree
Showing 9 changed files with 475 additions and 42 deletions.
31 changes: 31 additions & 0 deletions crates/uv-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1781,6 +1781,37 @@ pub struct RunArgs {
/// - `/home/ferris/.local/bin/python3.10` uses the exact Python at the given path.
#[arg(long, short, env = "UV_PYTHON", verbatim_doc_comment)]
pub python: Option<String>,

/// Require all packages listed in the given `requirements.txt` files.
///
/// If a `pyproject.toml`, `setup.py`, or `setup.cfg` file is provided, `uv` will
/// extract the requirements for the relevant project.
///
/// If `-` is provided, then requirements will be read from stdin.
#[arg(long, short, group = "sources", value_parser = parse_file_path)]
pub requirement: Vec<PathBuf>,

/// Constrain versions using the given requirements files.
///
/// Constraints files are `requirements.txt`-like files that only control the _version_ of a
/// requirement that's installed. However, including a package in a constraints file will _not_
/// trigger the installation of that package.
///
/// This is equivalent to pip's `--constraint` option.
#[arg(long, short, env = "UV_CONSTRAINT", value_delimiter = ' ', value_parser = parse_maybe_file_path)]
pub constraint: Vec<Maybe<PathBuf>>,

/// Override versions using the given requirements files.
///
/// Overrides files are `requirements.txt`-like files that force a specific version of a
/// requirement to be installed, regardless of the requirements declared by any constituent
/// package, and regardless of whether this would be considered an invalid resolution.
///
/// While constraints are _additive_, in that they're combined with the requirements of the
/// constituent packages, overrides are _absolute_, in that they completely replace the
/// requirements of the constituent packages.
#[arg(long, env = "UV_OVERRIDE", value_delimiter = ' ', value_parser = parse_maybe_file_path)]
pub r#override: Vec<Maybe<PathBuf>>,
}

#[derive(Args)]
Expand Down
5 changes: 5 additions & 0 deletions crates/uv-requirements/src/specification.rs
Original file line number Diff line number Diff line change
Expand Up @@ -336,4 +336,9 @@ impl RequirementsSpecification {
..Self::default()
}
}

/// Return true if the specification does not include any requirements to install.
pub fn is_empty(&self) -> bool {
self.requirements.is_empty() && self.source_trees.is_empty() && self.overrides.is_empty()
}
}
2 changes: 1 addition & 1 deletion crates/uv/src/commands/project/add.rs
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@ pub(crate) async fn add(
&VirtualProject::Project(project),
&venv,
&lock,
extras,
&extras,
dev,
Modifications::Sufficient,
settings.as_ref().into(),
Expand Down
2 changes: 1 addition & 1 deletion crates/uv/src/commands/project/remove.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ pub(crate) async fn remove(
&VirtualProject::Project(project),
&venv,
&lock,
extras,
&extras,
dev,
Modifications::Exact,
settings.as_ref().into(),
Expand Down
95 changes: 59 additions & 36 deletions crates/uv/src/commands/project/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use std::ffi::OsString;
use std::fmt::Write;
use std::path::PathBuf;

use anyhow::{Context, Result};
use anyhow::{bail, Context, Result};
use itertools::Itertools;
use owo_colors::OwoColorize;
use tokio::process::Command;
Expand All @@ -23,10 +23,10 @@ use uv_python::{
PythonInstallation, PythonPreference, PythonRequest, VersionRequest,
};

use uv_requirements::{RequirementsSource, RequirementsSpecification};
use uv_requirements::RequirementsSource;
use uv_warnings::warn_user_once;

use crate::commands::pip::operations::Modifications;
use crate::commands::pip::operations::{self, Modifications};
use crate::commands::project::environment::CachedEnvironment;
use crate::commands::reporters::PythonDownloadReporter;
use crate::commands::{project, ExitStatus, SharedState};
Expand All @@ -37,6 +37,8 @@ use crate::settings::ResolverInstallerSettings;
pub(crate) async fn run(
command: ExternalCommand,
requirements: Vec<RequirementsSource>,
constraints: &[RequirementsSource],
overrides: &[RequirementsSource],
package: Option<PackageName>,
extras: ExtrasSpecification,
dev: bool,
Expand All @@ -56,6 +58,27 @@ pub(crate) async fn run(
warn_user_once!("`uv run` is experimental and may change without warning.");
}

// These cases seem quite complex because it changes the current package — let's just
// ban it entirely for now
if requirements
.iter()
.any(|source| matches!(source, RequirementsSource::PyprojectToml(_)))
{
bail!("Adding requirements from a `pyproject.toml` is not supported in `uv run`");
}
if requirements
.iter()
.any(|source| matches!(source, RequirementsSource::SetupCfg(_)))
{
bail!("Adding requirements from a `setup.cfg` is not supported in `uv run`");
}
if requirements
.iter()
.any(|source| matches!(source, RequirementsSource::SetupCfg(_)))
{
bail!("Adding requirements from a `setup.py` is not supported in `uv run`");
}

// Parse the input command.
let command = RunCommand::from(command);

Expand Down Expand Up @@ -207,7 +230,7 @@ pub(crate) async fn run(
&project,
&venv,
&lock,
extras,
&extras,
dev,
Modifications::Sufficient,
settings.as_ref().into(),
Expand Down Expand Up @@ -255,16 +278,22 @@ pub(crate) async fn run(
);
}

// Read the `--with` requirements.
let spec = if requirements.is_empty() {
// Read the requirements.
let spec = if requirements.is_empty() && constraints.is_empty() && overrides.is_empty() {
None
} else {
let client_builder = BaseClientBuilder::new()
.connectivity(connectivity)
.native_tls(native_tls);

let spec =
RequirementsSpecification::from_simple_sources(&requirements, &client_builder).await?;
let spec = operations::read_requirements(
&requirements,
constraints,
overrides,
&extras,
&client_builder,
)
.await?;

Some(spec)
};
Expand All @@ -285,6 +314,7 @@ pub(crate) async fn run(
return false;
}


match site_packages.satisfies(&spec.requirements, &spec.constraints, &spec.overrides) {
// If the requirements are already satisfied, we're done.
Ok(SatisfiesResult::Fresh {
Expand Down Expand Up @@ -355,35 +385,28 @@ pub(crate) async fn run(
false,
)?;

if requirements.is_empty() {
Some(venv)
} else {
debug!("Syncing ephemeral requirements");

let client_builder = BaseClientBuilder::new()
.connectivity(connectivity)
.native_tls(native_tls);

let spec =
RequirementsSpecification::from_simple_sources(&requirements, &client_builder)
.await?;

// Install the ephemeral requirements.
Some(
project::update_environment(
venv,
spec,
&settings,
&state,
preview,
connectivity,
concurrency,
native_tls,
cache,
printer,
match spec {
None => Some(venv),
Some(spec) if spec.is_empty() => Some(venv),
Some(spec) => {
debug!("Syncing ephemeral requirements");
// Install the ephemeral requirements.
Some(
project::update_environment(
venv,
spec,
&settings,
&state,
preview,
connectivity,
concurrency,
native_tls,
cache,
printer,
)
.await?,
)
.await?,
)
}
}
};

Expand Down
6 changes: 3 additions & 3 deletions crates/uv/src/commands/project/sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ pub(crate) async fn sync(
&project,
&venv,
&lock,
extras,
&extras,
dev,
modifications,
settings.as_ref().into(),
Expand All @@ -169,7 +169,7 @@ pub(super) async fn do_sync(
project: &VirtualProject,
venv: &PythonEnvironment,
lock: &Lock,
extras: ExtrasSpecification,
extras: &ExtrasSpecification,
dev: bool,
modifications: Modifications,
settings: InstallerSettingsRef<'_>,
Expand Down Expand Up @@ -215,7 +215,7 @@ pub(super) async fn do_sync(
let tags = venv.interpreter().tags()?;

// Read the lockfile.
let resolution = lock.to_resolution(project, markers, tags, &extras, &dev)?;
let resolution = lock.to_resolution(project, markers, tags, extras, &dev)?;

// Initialize the registry client.
let client = RegistryClientBuilder::new(cache.clone())
Expand Down
17 changes: 17 additions & 0 deletions crates/uv/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -813,11 +813,28 @@ async fn run_project(
.with
.into_iter()
.map(RequirementsSource::from_package)
.chain(
args.requirement
.into_iter()
.map(RequirementsSource::from_requirements_file),
)
.collect::<Vec<_>>();
let constraints = args
.constraint
.into_iter()
.map(RequirementsSource::from_constraints_txt)
.collect::<Vec<_>>();
let overrides = args
.r#override
.into_iter()
.map(RequirementsSource::from_overrides_txt)
.collect::<Vec<_>>();

commands::run(
args.command,
requirements,
&constraints,
&overrides,
args.package,
args.extras,
args.dev,
Expand Down
15 changes: 15 additions & 0 deletions crates/uv/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,9 @@ pub(crate) struct RunSettings {
pub(crate) with: Vec<String>,
pub(crate) package: Option<PackageName>,
pub(crate) python: Option<String>,
pub(crate) requirement: Vec<PathBuf>,
pub(crate) constraint: Vec<PathBuf>,
pub(crate) r#override: Vec<PathBuf>,
pub(crate) refresh: Refresh,
pub(crate) settings: ResolverInstallerSettings,
}
Expand All @@ -176,6 +179,9 @@ impl RunSettings {
refresh,
package,
python,
requirement,
constraint,
r#override,
} = args;

Self {
Expand All @@ -189,6 +195,15 @@ impl RunSettings {
package,
python,
refresh: Refresh::from(refresh),
requirement,
constraint: constraint
.into_iter()
.filter_map(Maybe::into_option)
.collect(),
r#override: r#override
.into_iter()
.filter_map(Maybe::into_option)
.collect(),
settings: ResolverInstallerSettings::combine(
resolver_installer_options(installer, build),
filesystem,
Expand Down
Loading

0 comments on commit 4d81e5c

Please sign in to comment.