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

Error when editables don't match Requires-Python #2194

Merged
merged 1 commit into from
Mar 5, 2024
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
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.
Copy link
Contributor

Choose a reason for hiding this comment

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

Not that I know anything about uv codebase but FTR this fix should also be applied on non-editable local installs. (appologies if that's already done).

Thanks for working on this!

Copy link
Member Author

Choose a reason for hiding this comment

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

I thought we already enforced those, but I guess not! Will do it in a separate PR, thanks.

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(())
}