Skip to content

Commit

Permalink
Error when editables don't match Requires-Python
Browse files Browse the repository at this point in the history
  • Loading branch information
charliermarsh committed Mar 5, 2024
1 parent 2a53e78 commit 0271010
Show file tree
Hide file tree
Showing 7 changed files with 217 additions and 16 deletions.
4 changes: 2 additions & 2 deletions crates/uv-resolver/src/python_requirement.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,12 @@ impl PythonRequirement {
}

/// Return the installed version of Python.
pub(crate) fn installed(&self) -> &Version {
pub fn installed(&self) -> &Version {
&self.installed
}

/// Return the target version of Python.
pub(crate) fn target(&self) -> &Version {
pub fn target(&self) -> &Version {
&self.target
}

Expand Down
30 changes: 21 additions & 9 deletions crates/uv/src/commands/pip_compile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ use uv_interpreter::{Interpreter, PythonVersion};
use uv_normalize::{ExtraName, PackageName};
use uv_resolver::{
AnnotationStyle, DependencyMode, DisplayResolutionGraph, InMemoryIndex, Manifest,
OptionsBuilder, PreReleaseMode, ResolutionMode, Resolver,
OptionsBuilder, PreReleaseMode, PythonRequirement, ResolutionMode, Resolver,
};
use uv_traits::{ConfigSettings, InFlight, NoBuild, SetupPyStrategy};
use uv_warnings::warn_user;
Expand Down Expand Up @@ -244,31 +244,43 @@ pub(crate) async fn pip_compile(
let downloader = Downloader::new(&cache, &tags, &client, &build_dispatch)
.with_reporter(DownloadReporter::from(printer).with_length(editables.len() as u64));

// Build all editables.
let editable_wheel_dir = tempdir_in(cache.root())?;
let editable_metadata: Vec<_> = downloader
let editables: Vec<_> = downloader
.build_editables(editables, editable_wheel_dir.path())
.await
.context("Failed to build editables")?
.into_iter()
.map(|built_editable| (built_editable.editable, built_editable.metadata))
.collect();

let s = if editable_metadata.len() == 1 {
""
} else {
"s"
};
// Validate that the editables are compatible with the target Python version.
let requirement = PythonRequirement::new(&interpreter, &markers);
for (.., metadata) in &editables {
if let Some(python_requires) = metadata.requires_python.as_ref() {
if !python_requires.contains(requirement.target()) {
return Err(anyhow!(
"Editable `{}` requires Python {}, but resolution targets Python {}",
metadata.name,
python_requires,
requirement.target()
));
}
}
}

let s = if editables.len() == 1 { "" } else { "s" };
writeln!(
printer,
"{}",
format!(
"Built {} in {}",
format!("{} editable{}", editable_metadata.len(), s).bold(),
format!("{} editable{}", editables.len(), s).bold(),
elapsed(start.elapsed())
)
.dimmed()
)?;
editable_metadata
editables
};

// Create a manifest of the requirements.
Expand Down
18 changes: 18 additions & 0 deletions crates/uv/src/commands/pip_install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ pub(crate) async fn pip_install(
&editables,
editable_wheel_dir.path(),
&cache,
&interpreter,
tags,
&client,
&resolve_dispatch,
Expand Down Expand Up @@ -356,10 +357,12 @@ fn specification(
}

/// Build a set of editable distributions.
#[allow(clippy::too_many_arguments)]
async fn build_editables(
editables: &[EditableRequirement],
editable_wheel_dir: &Path,
cache: &Cache,
interpreter: &Interpreter,
tags: &Tags,
client: &RegistryClient,
build_dispatch: &BuildDispatch<'_>,
Expand Down Expand Up @@ -389,6 +392,21 @@ async fn build_editables(
.into_iter()
.collect();

// Validate that the editables are compatible with the target Python version.
for editable in &editables {
if let Some(python_requires) = editable.metadata.requires_python.as_ref() {
if !python_requires.contains(interpreter.python_version()) {
return Err(anyhow!(
"Editable `{}` requires Python {}, but {} is installed",
editable.metadata.name,
python_requires,
interpreter.python_version()
)
.into());
}
}
}

let s = if editables.len() == 1 { "" } else { "s" };
writeln!(
printer,
Expand Down
24 changes: 19 additions & 5 deletions crates/uv/src/commands/pip_sync.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::fmt::Write;

use anyhow::{Context, Result};
use anyhow::{anyhow, Context, Result};
use itertools::Itertools;
use owo_colors::OwoColorize;
use tracing::debug;
Expand All @@ -18,7 +18,7 @@ use uv_fs::Simplified;
use uv_installer::{
is_dynamic, Downloader, NoBinary, Plan, Planner, Reinstall, ResolvedEditable, SitePackages,
};
use uv_interpreter::PythonEnvironment;
use uv_interpreter::{Interpreter, PythonEnvironment};
use uv_resolver::InMemoryIndex;
use uv_traits::{ConfigSettings, InFlight, NoBuild, SetupPyStrategy};

Expand Down Expand Up @@ -151,7 +151,7 @@ pub(crate) async fn pip_sync(
editables,
&site_packages,
reinstall,
&venv,
venv.interpreter(),
tags,
&cache,
&client,
Expand Down Expand Up @@ -413,7 +413,7 @@ async fn resolve_editables(
editables: Vec<EditableRequirement>,
site_packages: &SitePackages<'_>,
reinstall: &Reinstall,
venv: &PythonEnvironment,
interpreter: &Interpreter,
tags: &Tags,
cache: &Cache,
client: &RegistryClient,
Expand Down Expand Up @@ -479,7 +479,7 @@ async fn resolve_editables(
} else {
let start = std::time::Instant::now();

let temp_dir = tempfile::tempdir_in(venv.root())?;
let temp_dir = tempfile::tempdir_in(cache.root())?;

let downloader = Downloader::new(cache, tags, client, build_dispatch)
.with_reporter(DownloadReporter::from(printer).with_length(uninstalled.len() as u64));
Expand All @@ -503,6 +503,20 @@ async fn resolve_editables(
.into_iter()
.collect();

// Validate that the editables are compatible with the target Python version.
for editable in &built_editables {
if let Some(python_requires) = editable.metadata.requires_python.as_ref() {
if !python_requires.contains(interpreter.python_version()) {
return Err(anyhow!(
"Editable `{}` requires Python {}, but {} is installed",
editable.metadata.name,
python_requires,
interpreter.python_version()
));
}
}
}

let s = if built_editables.len() == 1 { "" } else { "s" };
writeln!(
printer,
Expand Down
86 changes: 86 additions & 0 deletions crates/uv/tests/pip_compile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4670,3 +4670,89 @@ fn expand_env_var_requirements_txt() -> Result<()> {

Ok(())
}

/// Raise an error when an editable's `Requires-Python` constraint is not met.
#[test]
fn requires_python_editable() -> Result<()> {
let context = TestContext::new("3.12");

// Create an editable package with a `Requires-Python` constraint that is not met.
let editable_dir = TempDir::new()?;
let pyproject_toml = editable_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"[project]
name = "example"
version = "0.0.0"
dependencies = [
"anyio==4.0.0"
]
requires-python = "<=3.8"
"#,
)?;

// Write to a requirements file.
let requirements_in = context.temp_dir.child("requirements.in");
requirements_in.write_str(&format!("-e {}", editable_dir.path().display()))?;

uv_snapshot!(context.compile()
.arg("requirements.in"), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Editable `example` requires Python <=3.8, but resolution targets Python 3.12.1
"###
);

Ok(())
}

/// Raise an error when an editable's `Requires-Python` constraint is not met.
#[test]
fn requires_python_editable_target_version() -> Result<()> {
let context = TestContext::new("3.12");

// Create an editable package with a `Requires-Python` constraint that is not met.
let editable_dir = TempDir::new()?;
let pyproject_toml = editable_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"[project]
name = "example"
version = "0.0.0"
dependencies = [
"anyio==4.0.0"
]
requires-python = "<=3.8"
"#,
)?;

// Write to a requirements file.
let requirements_in = context.temp_dir.child("requirements.in");
requirements_in.write_str(&format!("-e {}", editable_dir.path().display()))?;

let filters: Vec<_> = [
// 3.11 may not be installed
(
"warning: The requested Python version 3.11 is not available; .* will be used to build dependencies instead.\n",
"",
),
]
.into_iter()
.chain(INSTA_FILTERS.to_vec())
.collect();

uv_snapshot!(filters, context.compile()
.arg("requirements.in")
.arg("--python-version=3.11"), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Editable `example` requires Python <=3.8, but resolution targets Python 3.11
"###
);

Ok(())
}
34 changes: 34 additions & 0 deletions crates/uv/tests/pip_install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2247,3 +2247,37 @@ requires-python = ">=3.11,<3.13"

Ok(())
}

/// Raise an error when an editable's `Requires-Python` constraint is not met.
#[test]
fn requires_python_editable() -> Result<()> {
let context = TestContext::new("3.12");

// Create an editable package with a `Requires-Python` constraint that is not met.
let editable_dir = assert_fs::TempDir::new()?;
let pyproject_toml = editable_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"[project]
name = "example"
version = "0.0.0"
dependencies = [
"anyio==4.0.0"
]
requires-python = "<=3.8"
"#,
)?;

uv_snapshot!(command(&context)
.arg("--editable")
.arg(editable_dir.path()), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Editable `example` requires Python <=3.8, but 3.12.1 is installed
"###
);

Ok(())
}
37 changes: 37 additions & 0 deletions crates/uv/tests/pip_sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2956,3 +2956,40 @@ fn compile() -> Result<()> {

Ok(())
}

/// Raise an error when an editable's `Requires-Python` constraint is not met.
#[test]
fn requires_python_editable() -> Result<()> {
let context = TestContext::new("3.12");

// Create an editable package with a `Requires-Python` constraint that is not met.
let editable_dir = assert_fs::TempDir::new()?;
let pyproject_toml = editable_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"[project]
name = "example"
version = "0.0.0"
dependencies = [
"anyio==4.0.0"
]
requires-python = "<=3.5"
"#,
)?;

// Write to a requirements file.
let requirements_in = context.temp_dir.child("requirements.in");
requirements_in.write_str(&format!("-e {}", editable_dir.path().display()))?;

uv_snapshot!(command(&context)
.arg("requirements.in"), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Editable `example` requires Python <=3.5, but 3.12.1 is installed
"###
);

Ok(())
}

0 comments on commit 0271010

Please sign in to comment.