Skip to content

Commit

Permalink
Allow --constraints and --overrides in uvx
Browse files Browse the repository at this point in the history
  • Loading branch information
charliermarsh committed Dec 27, 2024
1 parent 1fb7f35 commit 5443d5b
Show file tree
Hide file tree
Showing 6 changed files with 204 additions and 36 deletions.
22 changes: 22 additions & 0 deletions crates/uv-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3773,6 +3773,28 @@ pub struct ToolRunArgs {
#[arg(long, value_delimiter = ',', value_parser = parse_maybe_file_path)]
pub with_requirements: Vec<Maybe<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, alias = "constraint", env = EnvVars::UV_CONSTRAINT, value_delimiter = ' ', value_parser = parse_maybe_file_path)]
pub constraints: 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, alias = "override", env = EnvVars::UV_OVERRIDE, value_delimiter = ' ', value_parser = parse_maybe_file_path)]
pub overrides: Vec<Maybe<PathBuf>>,

/// Run the tool in an isolated virtual environment, ignoring any already-installed tools.
#[arg(long)]
pub isolated: bool,
Expand Down
92 changes: 63 additions & 29 deletions crates/uv/src/commands/tool/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@ use uv_cli::ExternalCommand;
use uv_client::{BaseClientBuilder, Connectivity};
use uv_configuration::{Concurrency, PreviewMode, TrustedHost};
use uv_dispatch::SharedState;
use uv_distribution_types::{Name, UnresolvedRequirementSpecification};
use uv_installer::{SatisfiesResult, SitePackages};
use uv_distribution_types::{
Name, NameRequirementSpecification, UnresolvedRequirementSpecification,
};
use uv_installer::SitePackages;
use uv_normalize::PackageName;
use uv_pep440::{VersionSpecifier, VersionSpecifiers};
use uv_pep508::MarkerTree;
Expand All @@ -27,7 +29,7 @@ use uv_python::{
PythonPreference, PythonRequest,
};
use uv_requirements::{RequirementsSource, RequirementsSpecification};
use uv_settings::PythonInstallMirrors;
use uv_settings::{PythonInstallMirrors, ResolverInstallerOptions, ToolOptions};
use uv_static::EnvVars;
use uv_tool::{entrypoint_paths, InstalledTools};
use uv_warnings::warn_user;
Expand Down Expand Up @@ -69,9 +71,12 @@ pub(crate) async fn run(
command: Option<ExternalCommand>,
from: Option<String>,
with: &[RequirementsSource],
constraints: &[RequirementsSource],
overrides: &[RequirementsSource],
show_resolution: bool,
python: Option<String>,
install_mirrors: PythonInstallMirrors,
options: ResolverInstallerOptions,
settings: ResolverInstallerSettings,
invocation_source: ToolRunCommand,
isolated: bool,
Expand Down Expand Up @@ -115,9 +120,12 @@ pub(crate) async fn run(
let result = get_or_create_environment(
&target,
with,
constraints,
overrides,
show_resolution,
python.as_deref(),
install_mirrors,
options,
&settings,
isolated,
python_preference,
Expand Down Expand Up @@ -434,9 +442,12 @@ fn warn_executable_not_provided_by_package(
async fn get_or_create_environment(
target: &Target<'_>,
with: &[RequirementsSource],
constraints: &[RequirementsSource],
overrides: &[RequirementsSource],
show_resolution: bool,
python: Option<&str>,
install_mirrors: PythonInstallMirrors,
options: ResolverInstallerOptions,
settings: &ResolverInstallerSettings,
isolated: bool,
python_preference: PythonPreference,
Expand Down Expand Up @@ -477,6 +488,11 @@ async fn get_or_create_environment(
// Initialize any shared state.
let state = SharedState::default();

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

// Resolve the `--from` requirement.
let from = match target {
// Ex) `ruff`
Expand Down Expand Up @@ -540,13 +556,9 @@ async fn get_or_create_environment(
};

// Read the `--with` requirements.
let spec = {
let client_builder = BaseClientBuilder::new()
.connectivity(connectivity)
.native_tls(native_tls)
.allow_insecure_host(allow_insecure_host.to_vec());
RequirementsSpecification::from_simple_sources(with, &client_builder).await?
};
let spec =
RequirementsSpecification::from_sources(with, constraints, overrides, &client_builder)
.await?;

// Resolve the `--from` and `--with` requirements.
let requirements = {
Expand All @@ -571,6 +583,30 @@ async fn get_or_create_environment(
requirements
};

// Resolve the constraints.
let constraints = spec
.constraints
.clone()
.into_iter()
.map(|constraint| constraint.requirement)
.collect::<Vec<_>>();

// Resolve the overrides.
let overrides = resolve_names(
spec.overrides.clone(),
&interpreter,
settings,
&state,
connectivity,
concurrency,
native_tls,
allow_insecure_host,
cache,
printer,
preview,
)
.await?;

// Check if the tool is already installed in a compatible environment.
if !isolated && !target.is_latest() {
let installed_tools = InstalledTools::from_settings()?.init()?;
Expand All @@ -586,25 +622,15 @@ async fn get_or_create_environment(
});
if let Some(environment) = existing_environment {
// Check if the installed packages meet the requirements.
let site_packages = SitePackages::from_environment(&environment)?;

let requirements = requirements
.iter()
.cloned()
.map(UnresolvedRequirementSpecification::from)
.collect::<Vec<_>>();
let constraints = [];

if matches!(
site_packages.satisfies(
&requirements,
&constraints,
&interpreter.resolver_marker_environment()
),
Ok(SatisfiesResult::Fresh { .. })
) {
debug!("Using existing tool `{}`", from.name);
return Ok((from, environment));
if let Ok(Some(tool_receipt)) = installed_tools.get_tool_receipt(&from.name) {
if requirements == tool_receipt.requirements()
&& constraints == tool_receipt.constraints()
&& overrides == tool_receipt.overrides()
&& ToolOptions::from(options) == *tool_receipt.options()
{
debug!("Using existing tool `{}`", from.name);
return Ok((from, environment));
}
}
}
}
Expand All @@ -615,6 +641,14 @@ async fn get_or_create_environment(
.into_iter()
.map(UnresolvedRequirementSpecification::from)
.collect(),
constraints: constraints
.into_iter()
.map(NameRequirementSpecification::from)
.collect(),
overrides: overrides
.into_iter()
.map(UnresolvedRequirementSpecification::from)
.collect(),
..spec
};

Expand Down
13 changes: 13 additions & 0 deletions crates/uv/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -926,14 +926,27 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
.map(RequirementsSource::from_requirements_file),
)
.collect::<Vec<_>>();
let constraints = args
.constraints
.into_iter()
.map(RequirementsSource::from_constraints_txt)
.collect::<Vec<_>>();
let overrides = args
.overrides
.into_iter()
.map(RequirementsSource::from_overrides_txt)
.collect::<Vec<_>>();

commands::tool_run(
args.command,
args.from,
&requirements,
&constraints,
&overrides,
args.show_resolution || globals.verbose > 0,
args.python,
args.install_mirrors,
args.options,
args.settings,
invocation_source,
args.isolated,
Expand Down
35 changes: 28 additions & 7 deletions crates/uv/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -382,13 +382,16 @@ pub(crate) struct ToolRunSettings {
pub(crate) command: Option<ExternalCommand>,
pub(crate) from: Option<String>,
pub(crate) with: Vec<String>,
pub(crate) with_editable: Vec<String>,
pub(crate) with_requirements: Vec<PathBuf>,
pub(crate) with_editable: Vec<String>,
pub(crate) constraints: Vec<PathBuf>,
pub(crate) overrides: Vec<PathBuf>,
pub(crate) isolated: bool,
pub(crate) show_resolution: bool,
pub(crate) python: Option<String>,
pub(crate) install_mirrors: PythonInstallMirrors,
pub(crate) refresh: Refresh,
pub(crate) options: ResolverInstallerOptions,
pub(crate) settings: ResolverInstallerSettings,
}

Expand All @@ -406,6 +409,8 @@ impl ToolRunSettings {
with,
with_editable,
with_requirements,
constraints,
overrides,
isolated,
show_resolution,
installer,
Expand Down Expand Up @@ -433,11 +438,21 @@ impl ToolRunSettings {
}
}

let options = resolver_installer_options(installer, build).combine(
filesystem
.clone()
.map(FilesystemOptions::into_options)
.map(|options| options.top_level)
.unwrap_or_default(),
);

let install_mirrors = filesystem
.clone()
.map(|fs| fs.install_mirrors.clone())
.map(FilesystemOptions::into_options)
.map(|options| options.install_mirrors)
.unwrap_or_default();

let settings = ResolverInstallerSettings::from(options.clone());

Self {
command,
from,
Expand All @@ -453,14 +468,20 @@ impl ToolRunSettings {
.into_iter()
.filter_map(Maybe::into_option)
.collect(),
constraints: constraints
.into_iter()
.filter_map(Maybe::into_option)
.collect(),
overrides: overrides
.into_iter()
.filter_map(Maybe::into_option)
.collect(),
isolated,
show_resolution,
python: python.and_then(Maybe::into_option),
refresh: Refresh::from(refresh),
settings: ResolverInstallerSettings::combine(
resolver_installer_options(installer, build),
filesystem,
),
settings,
options,
install_mirrors,
}
}
Expand Down
64 changes: 64 additions & 0 deletions crates/uv/tests/it/tool_run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,70 @@ fn tool_run_from_version() {
"###);
}

#[test]
fn tool_run_constraints() {
let context = TestContext::new("3.12");
let tool_dir = context.temp_dir.child("tools");
let bin_dir = context.temp_dir.child("bin");

let constraints_txt = context.temp_dir.child("constraints.txt");
constraints_txt.write_str("pluggy<1.4.0").unwrap();

uv_snapshot!(context.filters(), context.tool_run()
.arg("--constraints")
.arg("constraints.txt")
.arg("pytest")
.arg("--version")
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str()), @r###"
success: true
exit_code: 0
----- stdout -----
pytest 8.0.2
----- stderr -----
Resolved 4 packages in [TIME]
Prepared 4 packages in [TIME]
Installed 4 packages in [TIME]
+ iniconfig==2.0.0
+ packaging==24.0
+ pluggy==1.3.0
+ pytest==8.0.2
"###);
}

#[test]
fn tool_run_overrides() {
let context = TestContext::new("3.12");
let tool_dir = context.temp_dir.child("tools");
let bin_dir = context.temp_dir.child("bin");

let overrides_txt = context.temp_dir.child("overrides.txt");
overrides_txt.write_str("pluggy<1.4.0").unwrap();

uv_snapshot!(context.filters(), context.tool_run()
.arg("--overrides")
.arg("overrides.txt")
.arg("pytest")
.arg("--version")
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str()), @r###"
success: true
exit_code: 0
----- stdout -----
pytest 8.1.1
----- stderr -----
Resolved 4 packages in [TIME]
Prepared 4 packages in [TIME]
Installed 4 packages in [TIME]
+ iniconfig==2.0.0
+ packaging==24.0
+ pluggy==1.3.0
+ pytest==8.1.1
"###);
}

#[test]
fn tool_run_suggest_valid_commands() {
let context = TestContext::new("3.12").with_filtered_exe_suffix();
Expand Down
Loading

0 comments on commit 5443d5b

Please sign in to comment.