mirror of
https://github.com/astral-sh/uv.git
synced 2025-07-24 13:43:45 +00:00
Surface dedicated errors for .python-version
conflict with requires-python
(#7218)
## Summary I got confused because I had a `.python-version` file that conflicted with my `requires-python`.
This commit is contained in:
parent
5905f40f50
commit
fe8880bf3c
6 changed files with 191 additions and 50 deletions
|
@ -129,6 +129,12 @@ impl PythonVersionFile {
|
||||||
&self.path
|
&self.path
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return the file name of the version file (guaranteed to be one of `.python-version` or
|
||||||
|
/// `.python-versions`).
|
||||||
|
pub fn file_name(&self) -> &str {
|
||||||
|
self.path.file_name().unwrap().to_str().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
/// Set the versions for the file.
|
/// Set the versions for the file.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn with_versions(self, versions: Vec<PythonRequest>) -> Self {
|
pub fn with_versions(self, versions: Vec<PythonRequest>) -> Self {
|
||||||
|
|
|
@ -63,11 +63,36 @@ pub(crate) enum ProjectError {
|
||||||
#[error("The current Python platform is not compatible with the lockfile's supported environments: {0}")]
|
#[error("The current Python platform is not compatible with the lockfile's supported environments: {0}")]
|
||||||
LockedPlatformIncompatibility(String),
|
LockedPlatformIncompatibility(String),
|
||||||
|
|
||||||
#[error("The requested Python interpreter ({0}) is incompatible with the project Python requirement: `{1}`")]
|
#[error("The requested interpreter resolved to Python {0}, which is incompatible with the project's Python requirement: `{1}`")]
|
||||||
RequestedPythonIncompatibility(Version, RequiresPython),
|
RequestedPythonIncompatibility(Version, RequiresPython),
|
||||||
|
|
||||||
#[error("The requested Python interpreter ({0}) is incompatible with the project Python requirement: `{1}`. However, a workspace member (`{member}`) supports Python {3}. To install the workspace member on its own, navigate to `{path}`, then run `{venv}` followed by `{install}`.", member = _2.cyan(), venv = format!("uv venv --python {_0}").green(), install = "uv pip install -e .".green(), path = _4.user_display().cyan() )]
|
#[error("The Python request from `{0}` resolved to Python {1}, which incompatible with the project's Python requirement: `{2}`")]
|
||||||
RequestedMemberPythonIncompatibility(
|
DotPythonVersionPythonIncompatibility(String, Version, RequiresPython),
|
||||||
|
|
||||||
|
#[error("The resolved Python interpreter (Python {0}) is incompatible with the project's Python requirement: `{1}`")]
|
||||||
|
RequiresPythonIncompatibility(Version, RequiresPython),
|
||||||
|
|
||||||
|
#[error("The requested interpreter resolved to Python {0}, which is incompatible with the project's Python requirement: `{1}`. However, a workspace member (`{member}`) supports Python {3}. To install the workspace member on its own, navigate to `{path}`, then run `{venv}` followed by `{install}`.", member = _2.cyan(), venv = format!("uv venv --python {_0}").green(), install = "uv pip install -e .".green(), path = _4.user_display().cyan() )]
|
||||||
|
RequestedMemberIncompatibility(
|
||||||
|
Version,
|
||||||
|
RequiresPython,
|
||||||
|
PackageName,
|
||||||
|
VersionSpecifiers,
|
||||||
|
PathBuf,
|
||||||
|
),
|
||||||
|
|
||||||
|
#[error("The Python request from `{0}` resolved to Python {1}, which incompatible with the project's Python requirement: `{2}`. However, a workspace member (`{member}`) supports Python {4}. To install the workspace member on its own, navigate to `{path}`, then run `{venv}` followed by `{install}`.", member = _3.cyan(), venv = format!("uv venv --python {_1}").green(), install = "uv pip install -e .".green(), path = _5.user_display().cyan() )]
|
||||||
|
DotPythonVersionMemberIncompatibility(
|
||||||
|
String,
|
||||||
|
Version,
|
||||||
|
RequiresPython,
|
||||||
|
PackageName,
|
||||||
|
VersionSpecifiers,
|
||||||
|
PathBuf,
|
||||||
|
),
|
||||||
|
|
||||||
|
#[error("The resolved Python interpreter (Python {0}) is incompatible with the project's Python requirement: `{1}`. However, a workspace member (`{member}`) supports Python {3}. To install the workspace member on its own, navigate to `{path}`, then run `{venv}` followed by `{install}`.", member = _2.cyan(), venv = format!("uv venv --python {_0}").green(), install = "uv pip install -e .".green(), path = _4.user_display().cyan() )]
|
||||||
|
RequiresPythonMemberIncompatibility(
|
||||||
Version,
|
Version,
|
||||||
RequiresPython,
|
RequiresPython,
|
||||||
PackageName,
|
PackageName,
|
||||||
|
@ -161,8 +186,12 @@ pub(crate) fn validate_requires_python(
|
||||||
interpreter: &Interpreter,
|
interpreter: &Interpreter,
|
||||||
workspace: &Workspace,
|
workspace: &Workspace,
|
||||||
requires_python: &RequiresPython,
|
requires_python: &RequiresPython,
|
||||||
|
source: &WorkspacePythonSource,
|
||||||
) -> Result<(), ProjectError> {
|
) -> Result<(), ProjectError> {
|
||||||
if !requires_python.contains(interpreter.python_version()) {
|
if requires_python.contains(interpreter.python_version()) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
// If the Python version is compatible with one of the workspace _members_, raise
|
// If the Python version is compatible with one of the workspace _members_, raise
|
||||||
// a dedicated error. For example, if the workspace root requires Python >=3.12, but
|
// a dedicated error. For example, if the workspace root requires Python >=3.12, but
|
||||||
// a library in the workspace is compatible with Python >=3.8, the user may attempt
|
// a library in the workspace is compatible with Python >=3.8, the user may attempt
|
||||||
|
@ -176,23 +205,56 @@ pub(crate) fn validate_requires_python(
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
if specifiers.contains(interpreter.python_version()) {
|
if specifiers.contains(interpreter.python_version()) {
|
||||||
return Err(ProjectError::RequestedMemberPythonIncompatibility(
|
return match source {
|
||||||
|
WorkspacePythonSource::UserRequest => {
|
||||||
|
Err(ProjectError::RequestedMemberIncompatibility(
|
||||||
interpreter.python_version().clone(),
|
interpreter.python_version().clone(),
|
||||||
requires_python.clone(),
|
requires_python.clone(),
|
||||||
name.clone(),
|
name.clone(),
|
||||||
specifiers.clone(),
|
specifiers.clone(),
|
||||||
member.root().clone(),
|
member.root().clone(),
|
||||||
));
|
))
|
||||||
}
|
}
|
||||||
}
|
WorkspacePythonSource::DotPythonVersion(file) => {
|
||||||
|
Err(ProjectError::DotPythonVersionMemberIncompatibility(
|
||||||
return Err(ProjectError::RequestedPythonIncompatibility(
|
file.to_string(),
|
||||||
interpreter.python_version().clone(),
|
interpreter.python_version().clone(),
|
||||||
requires_python.clone(),
|
requires_python.clone(),
|
||||||
));
|
name.clone(),
|
||||||
|
specifiers.clone(),
|
||||||
|
member.root().clone(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
WorkspacePythonSource::RequiresPython => {
|
||||||
|
Err(ProjectError::RequiresPythonMemberIncompatibility(
|
||||||
|
interpreter.python_version().clone(),
|
||||||
|
requires_python.clone(),
|
||||||
|
name.clone(),
|
||||||
|
specifiers.clone(),
|
||||||
|
member.root().clone(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
match source {
|
||||||
|
WorkspacePythonSource::UserRequest => Err(ProjectError::RequestedPythonIncompatibility(
|
||||||
|
interpreter.python_version().clone(),
|
||||||
|
requires_python.clone(),
|
||||||
|
)),
|
||||||
|
WorkspacePythonSource::DotPythonVersion(file) => {
|
||||||
|
Err(ProjectError::DotPythonVersionPythonIncompatibility(
|
||||||
|
file.to_string(),
|
||||||
|
interpreter.python_version().clone(),
|
||||||
|
requires_python.clone(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
WorkspacePythonSource::RequiresPython => Err(ProjectError::RequiresPythonIncompatibility(
|
||||||
|
interpreter.python_version().clone(),
|
||||||
|
requires_python.clone(),
|
||||||
|
)),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Find the virtual environment for the current project.
|
/// Find the virtual environment for the current project.
|
||||||
|
@ -210,9 +272,21 @@ pub(crate) enum FoundInterpreter {
|
||||||
Environment(PythonEnvironment),
|
Environment(PythonEnvironment),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub(crate) enum WorkspacePythonSource {
|
||||||
|
/// The request was provided by the user.
|
||||||
|
UserRequest,
|
||||||
|
/// The request was inferred from a `.python-version` or `.python-versions` file.
|
||||||
|
DotPythonVersion(String),
|
||||||
|
/// The request was inferred from a `pyproject.toml` file.
|
||||||
|
RequiresPython,
|
||||||
|
}
|
||||||
|
|
||||||
/// The resolved Python request and requirement for a [`Workspace`].
|
/// The resolved Python request and requirement for a [`Workspace`].
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub(crate) struct WorkspacePython {
|
pub(crate) struct WorkspacePython {
|
||||||
|
/// The source of the Python request.
|
||||||
|
source: WorkspacePythonSource,
|
||||||
/// The resolved Python request, computed by considering (1) any explicit request from the user
|
/// The resolved Python request, computed by considering (1) any explicit request from the user
|
||||||
/// via `--python`, (2) any implicit request from the user via `.python-version`, and (3) any
|
/// via `--python`, (2) any implicit request from the user via `.python-version`, and (3) any
|
||||||
/// `Requires-Python` specifier in the `pyproject.toml`.
|
/// `Requires-Python` specifier in the `pyproject.toml`.
|
||||||
|
@ -230,25 +304,32 @@ impl WorkspacePython {
|
||||||
) -> Result<Self, ProjectError> {
|
) -> Result<Self, ProjectError> {
|
||||||
let requires_python = find_requires_python(workspace)?;
|
let requires_python = find_requires_python(workspace)?;
|
||||||
|
|
||||||
|
let (source, python_request) = if let Some(request) = python_request {
|
||||||
// (1) Explicit request from user
|
// (1) Explicit request from user
|
||||||
let python_request = if let Some(request) = python_request {
|
let source = WorkspacePythonSource::UserRequest;
|
||||||
Some(request)
|
let request = Some(request);
|
||||||
// (2) Request from `.python-version`
|
(source, request)
|
||||||
} else if let Some(request) =
|
} else if let Some(file) =
|
||||||
PythonVersionFile::discover(workspace.install_path(), false, false)
|
PythonVersionFile::discover(workspace.install_path(), false, false).await?
|
||||||
.await?
|
|
||||||
.and_then(PythonVersionFile::into_version)
|
|
||||||
{
|
{
|
||||||
Some(request)
|
// (2) Request from `.python-version`
|
||||||
// (3) `Requires-Python` in `pyproject.toml`
|
let source = WorkspacePythonSource::DotPythonVersion(file.file_name().to_string());
|
||||||
|
let request = file.into_version();
|
||||||
|
(source, request)
|
||||||
} else {
|
} else {
|
||||||
requires_python
|
// (3) `Requires-Python` in `pyproject.toml`
|
||||||
|
let request = requires_python
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(RequiresPython::specifiers)
|
.map(RequiresPython::specifiers)
|
||||||
.map(|specifiers| PythonRequest::Version(VersionRequest::Range(specifiers.clone())))
|
.map(|specifiers| {
|
||||||
|
PythonRequest::Version(VersionRequest::Range(specifiers.clone()))
|
||||||
|
});
|
||||||
|
let source = WorkspacePythonSource::RequiresPython;
|
||||||
|
(source, request)
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
|
source,
|
||||||
python_request,
|
python_request,
|
||||||
requires_python,
|
requires_python,
|
||||||
})
|
})
|
||||||
|
@ -269,6 +350,7 @@ impl FoundInterpreter {
|
||||||
) -> Result<Self, ProjectError> {
|
) -> Result<Self, ProjectError> {
|
||||||
// Resolve the Python request and requirement for the workspace.
|
// Resolve the Python request and requirement for the workspace.
|
||||||
let WorkspacePython {
|
let WorkspacePython {
|
||||||
|
source,
|
||||||
python_request,
|
python_request,
|
||||||
requires_python,
|
requires_python,
|
||||||
} = WorkspacePython::from_request(python_request, workspace).await?;
|
} = WorkspacePython::from_request(python_request, workspace).await?;
|
||||||
|
@ -346,7 +428,7 @@ impl FoundInterpreter {
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(requires_python) = requires_python.as_ref() {
|
if let Some(requires_python) = requires_python.as_ref() {
|
||||||
validate_requires_python(&interpreter, workspace, requires_python)?;
|
validate_requires_python(&interpreter, workspace, requires_python, &source)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Self::Interpreter(interpreter))
|
Ok(Self::Interpreter(interpreter))
|
||||||
|
|
|
@ -358,6 +358,7 @@ pub(crate) async fn run(
|
||||||
|
|
||||||
// Resolve the Python request and requirement for the workspace.
|
// Resolve the Python request and requirement for the workspace.
|
||||||
let WorkspacePython {
|
let WorkspacePython {
|
||||||
|
source,
|
||||||
python_request,
|
python_request,
|
||||||
requires_python,
|
requires_python,
|
||||||
} = WorkspacePython::from_request(
|
} = WorkspacePython::from_request(
|
||||||
|
@ -379,7 +380,12 @@ pub(crate) async fn run(
|
||||||
.into_interpreter();
|
.into_interpreter();
|
||||||
|
|
||||||
if let Some(requires_python) = requires_python.as_ref() {
|
if let Some(requires_python) = requires_python.as_ref() {
|
||||||
validate_requires_python(&interpreter, project.workspace(), requires_python)?;
|
validate_requires_python(
|
||||||
|
&interpreter,
|
||||||
|
project.workspace(),
|
||||||
|
requires_python,
|
||||||
|
&source,
|
||||||
|
)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a virtual environment
|
// Create a virtual environment
|
||||||
|
|
|
@ -12581,3 +12581,50 @@ fn lock_strip_fragment() -> Result<()> {
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn lock_request_requires_python() -> Result<()> {
|
||||||
|
let context = TestContext::new("3.12");
|
||||||
|
|
||||||
|
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||||||
|
pyproject_toml.write_str(
|
||||||
|
r#"
|
||||||
|
[project]
|
||||||
|
name = "project"
|
||||||
|
version = "0.1.0"
|
||||||
|
requires-python = ">=3.8, <=3.10"
|
||||||
|
dependencies = ["iniconfig"]
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["setuptools>=42"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
"#,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// Request a version that conflicts with `--requires-python`.
|
||||||
|
uv_snapshot!(context.filters(), context.lock().arg("--python").arg("3.12"), @r###"
|
||||||
|
success: false
|
||||||
|
exit_code: 2
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
Using Python 3.12.[X] interpreter at: [PYTHON-3.12]
|
||||||
|
error: The requested interpreter resolved to Python 3.12.[X], which is incompatible with the project's Python requirement: `>=3.8, <=3.10`
|
||||||
|
"###);
|
||||||
|
|
||||||
|
// Add a `.python-version` file that conflicts.
|
||||||
|
let python_version = context.temp_dir.child(".python-version");
|
||||||
|
python_version.write_str("3.12")?;
|
||||||
|
|
||||||
|
uv_snapshot!(context.filters(), context.lock(), @r###"
|
||||||
|
success: false
|
||||||
|
exit_code: 2
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
Using Python 3.12.[X] interpreter at: [PYTHON-3.12]
|
||||||
|
error: The Python request from `.python-version` resolved to Python 3.12.[X], which incompatible with the project's Python requirement: `>=3.8, <=3.10`
|
||||||
|
"###);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
|
@ -133,7 +133,7 @@ fn run_with_python_version() -> Result<()> {
|
||||||
|
|
||||||
----- stderr -----
|
----- stderr -----
|
||||||
Using Python 3.8.[X] interpreter at: [PYTHON-3.8]
|
Using Python 3.8.[X] interpreter at: [PYTHON-3.8]
|
||||||
error: The requested Python interpreter (3.8.[X]) is incompatible with the project Python requirement: `>=3.11, <4`
|
error: The requested interpreter resolved to Python 3.8.[X], which is incompatible with the project's Python requirement: `>=3.11, <4`
|
||||||
"###);
|
"###);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -1657,7 +1657,7 @@ fn run_isolated_incompatible_python() -> Result<()> {
|
||||||
|
|
||||||
----- stderr -----
|
----- stderr -----
|
||||||
Using Python 3.8.[X] interpreter at: [PYTHON-3.8]
|
Using Python 3.8.[X] interpreter at: [PYTHON-3.8]
|
||||||
error: The requested Python interpreter (3.8.[X]) is incompatible with the project Python requirement: `>=3.12`
|
error: The Python request from `.python-version` resolved to Python 3.8.[X], which incompatible with the project's Python requirement: `>=3.12`
|
||||||
"###);
|
"###);
|
||||||
|
|
||||||
// ...even if `--isolated` is provided.
|
// ...even if `--isolated` is provided.
|
||||||
|
@ -1667,7 +1667,7 @@ fn run_isolated_incompatible_python() -> Result<()> {
|
||||||
----- stdout -----
|
----- stdout -----
|
||||||
|
|
||||||
----- stderr -----
|
----- stderr -----
|
||||||
error: The requested Python interpreter (3.8.[X]) is incompatible with the project Python requirement: `>=3.12`
|
error: The Python request from `.python-version` resolved to Python 3.8.[X], which incompatible with the project's Python requirement: `>=3.12`
|
||||||
"###);
|
"###);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -378,7 +378,7 @@ fn mixed_requires_python() -> Result<()> {
|
||||||
|
|
||||||
----- stderr -----
|
----- stderr -----
|
||||||
Using Python 3.8.[X] interpreter at: [PYTHON-3.8]
|
Using Python 3.8.[X] interpreter at: [PYTHON-3.8]
|
||||||
error: The requested Python interpreter (3.8.[X]) is incompatible with the project Python requirement: `>=3.12`. However, a workspace member (`bird-feeder`) supports Python >=3.8. To install the workspace member on its own, navigate to `packages/bird-feeder`, then run `uv venv --python 3.8.[X]` followed by `uv pip install -e .`.
|
error: The requested interpreter resolved to Python 3.8.[X], which is incompatible with the project's Python requirement: `>=3.12`. However, a workspace member (`bird-feeder`) supports Python >=3.8. To install the workspace member on its own, navigate to `packages/bird-feeder`, then run `uv venv --python 3.8.[X]` followed by `uv pip install -e .`.
|
||||||
"###);
|
"###);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue