mirror of
https://github.com/astral-sh/uv.git
synced 2025-11-01 12:24:15 +00:00
Error when editables don't match Requires-Python (#2194)
## Summary Closes https://github.com/astral-sh/uv/issues/2192. ## Test Plan (Needs tests.)
This commit is contained in:
parent
2a53e789b0
commit
0bc047866d
7 changed files with 217 additions and 16 deletions
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -244,8 +244,9 @@ 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")?
|
||||
|
|
@ -253,22 +254,33 @@ pub(crate) async fn pip_compile(
|
|||
.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.
|
||||
|
|
|
|||
|
|
@ -215,6 +215,7 @@ pub(crate) async fn pip_install(
|
|||
&editables,
|
||||
editable_wheel_dir.path(),
|
||||
&cache,
|
||||
&interpreter,
|
||||
tags,
|
||||
&client,
|
||||
&resolve_dispatch,
|
||||
|
|
@ -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<'_>,
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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};
|
||||
|
||||
|
|
@ -151,7 +151,7 @@ pub(crate) async fn pip_sync(
|
|||
editables,
|
||||
&site_packages,
|
||||
reinstall,
|
||||
&venv,
|
||||
venv.interpreter(),
|
||||
tags,
|
||||
&cache,
|
||||
&client,
|
||||
|
|
@ -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,
|
||||
|
|
@ -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));
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue