Skip to content

Commit

Permalink
Error when user-provided environments are disjoint with Python (#6841)
Browse files Browse the repository at this point in the history
  • Loading branch information
charliermarsh authored Aug 30, 2024
1 parent 97e6861 commit 9f8ebca
Show file tree
Hide file tree
Showing 4 changed files with 140 additions and 0 deletions.
80 changes: 80 additions & 0 deletions crates/uv-resolver/src/requires_python.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use pubgrub::Range;

use distribution_filename::WheelFilename;
use pep440_rs::{Version, VersionSpecifier, VersionSpecifiers};
use pep508_rs::{MarkerExpression, MarkerTree, MarkerValueVersion};

#[derive(thiserror::Error, Debug)]
pub enum RequiresPythonError {
Expand Down Expand Up @@ -124,6 +125,85 @@ impl RequiresPython {
}
}

/// Returns the [`RequiresPython`] as a [`MarkerTree`].
pub fn markers(&self) -> MarkerTree {
match (self.range.0.as_ref(), self.range.0.as_ref()) {
(Bound::Included(lower), Bound::Included(upper)) => {
let mut lower = MarkerTree::expression(MarkerExpression::Version {
key: MarkerValueVersion::PythonFullVersion,
specifier: VersionSpecifier::greater_than_equal_version(lower.clone()),
});
let upper = MarkerTree::expression(MarkerExpression::Version {
key: MarkerValueVersion::PythonFullVersion,
specifier: VersionSpecifier::less_than_equal_version(upper.clone()),
});
lower.and(upper);
lower
}
(Bound::Included(lower), Bound::Excluded(upper)) => {
let mut lower = MarkerTree::expression(MarkerExpression::Version {
key: MarkerValueVersion::PythonFullVersion,
specifier: VersionSpecifier::greater_than_equal_version(lower.clone()),
});
let upper = MarkerTree::expression(MarkerExpression::Version {
key: MarkerValueVersion::PythonFullVersion,
specifier: VersionSpecifier::less_than_version(upper.clone()),
});
lower.and(upper);
lower
}
(Bound::Excluded(lower), Bound::Included(upper)) => {
let mut lower = MarkerTree::expression(MarkerExpression::Version {
key: MarkerValueVersion::PythonFullVersion,
specifier: VersionSpecifier::greater_than_version(lower.clone()),
});
let upper = MarkerTree::expression(MarkerExpression::Version {
key: MarkerValueVersion::PythonFullVersion,
specifier: VersionSpecifier::less_than_equal_version(upper.clone()),
});
lower.and(upper);
lower
}
(Bound::Excluded(lower), Bound::Excluded(upper)) => {
let mut lower = MarkerTree::expression(MarkerExpression::Version {
key: MarkerValueVersion::PythonFullVersion,
specifier: VersionSpecifier::greater_than_version(lower.clone()),
});
let upper = MarkerTree::expression(MarkerExpression::Version {
key: MarkerValueVersion::PythonFullVersion,
specifier: VersionSpecifier::less_than_version(upper.clone()),
});
lower.and(upper);
lower
}
(Bound::Unbounded, Bound::Unbounded) => MarkerTree::TRUE,
(Bound::Unbounded, Bound::Included(upper)) => {
MarkerTree::expression(MarkerExpression::Version {
key: MarkerValueVersion::PythonFullVersion,
specifier: VersionSpecifier::less_than_equal_version(upper.clone()),
})
}
(Bound::Unbounded, Bound::Excluded(upper)) => {
MarkerTree::expression(MarkerExpression::Version {
key: MarkerValueVersion::PythonFullVersion,
specifier: VersionSpecifier::less_than_version(upper.clone()),
})
}
(Bound::Included(lower), Bound::Unbounded) => {
MarkerTree::expression(MarkerExpression::Version {
key: MarkerValueVersion::PythonFullVersion,
specifier: VersionSpecifier::greater_than_equal_version(lower.clone()),
})
}
(Bound::Excluded(lower), Bound::Unbounded) => {
MarkerTree::expression(MarkerExpression::Version {
key: MarkerValueVersion::PythonFullVersion,
specifier: VersionSpecifier::greater_than_version(lower.clone()),
})
}
}
}

/// Returns `true` if the `Requires-Python` is compatible with the given version.
pub fn contains(&self, version: &Version) -> bool {
let version = version.only_release();
Expand Down
19 changes: 19 additions & 0 deletions crates/uv/src/commands/project/lock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,25 @@ async fn do_lock(
default
};

// If any of the forks are incompatible with the Python requirement, error.
for environment in environments
.map(SupportedEnvironments::as_markers)
.into_iter()
.flatten()
{
if requires_python.markers().is_disjoint(environment) {
return if let Some(contents) = environment.contents() {
Err(ProjectError::DisjointEnvironment(
contents,
requires_python.specifiers().clone(),
))
} else {
Err(ProjectError::EmptyEnvironment)
};
}
}

// Determine the Python requirement.
let python_requirement = PythonRequirement::from_requires_python(interpreter, &requires_python);

// Add all authenticated sources to the cache.
Expand Down
7 changes: 7 additions & 0 deletions crates/uv/src/commands/project/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use tracing::debug;

use distribution_types::{Resolution, UnresolvedRequirementSpecification};
use pep440_rs::{Version, VersionSpecifiers};
use pep508_rs::MarkerTreeContents;
use pypi_types::Requirement;
use uv_auth::store_credentials_from_url;
use uv_cache::Cache;
Expand Down Expand Up @@ -77,6 +78,12 @@ pub(crate) enum ProjectError {
#[error("Supported environments must be disjoint, but the following markers overlap: `{0}` and `{1}`.\n\n{hint}{colon} replace `{1}` with `{2}`.", hint = "hint".bold().cyan(), colon = ":".bold())]
OverlappingMarkers(String, String, String),

#[error("Environment markers `{0}` don't overlap with Python requirement `{1}`")]
DisjointEnvironment(MarkerTreeContents, VersionSpecifiers),

#[error("Environment marker is empty")]
EmptyEnvironment,

#[error(transparent)]
Python(#[from] uv_python::Error),

Expand Down
34 changes: 34 additions & 0 deletions crates/uv/tests/lock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11962,3 +11962,37 @@ fn lock_implicit_virtual_path() -> Result<()> {

Ok(())
}

#[test]
fn lock_conflicting_environment() -> 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.12"
dependencies = ["anyio"]
[build-system]
requires = ["setuptools>=42", "wheel"]
build-backend = "setuptools.build_meta"
[tool.uv]
environments = ["python_version < '3.11'"]
"#,
)?;

uv_snapshot!(context.filters(), context.lock(), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Environment markers `python_full_version < '3.11'` don't overlap with Python requirement `>=3.12`
"###);

Ok(())
}

0 comments on commit 9f8ebca

Please sign in to comment.