mirror of
https://github.com/astral-sh/uv.git
synced 2025-07-07 21:35:00 +00:00
Add support for constraints during pip-compile resolution (#144)
Closes https://github.com/astral-sh/puffin/issues/130.
This commit is contained in:
parent
d5105a76c5
commit
0b60804db6
5 changed files with 149 additions and 15 deletions
|
@ -21,7 +21,8 @@ use crate::requirements::RequirementsSource;
|
||||||
|
|
||||||
/// Resolve a set of requirements into a set of pinned versions.
|
/// Resolve a set of requirements into a set of pinned versions.
|
||||||
pub(crate) async fn pip_compile(
|
pub(crate) async fn pip_compile(
|
||||||
sources: &[RequirementsSource],
|
requirements: &[RequirementsSource],
|
||||||
|
constraints: &[RequirementsSource],
|
||||||
output_file: Option<&Path>,
|
output_file: Option<&Path>,
|
||||||
cache: Option<&Path>,
|
cache: Option<&Path>,
|
||||||
mut printer: Printer,
|
mut printer: Printer,
|
||||||
|
@ -29,7 +30,12 @@ pub(crate) async fn pip_compile(
|
||||||
let start = std::time::Instant::now();
|
let start = std::time::Instant::now();
|
||||||
|
|
||||||
// Read all requirements from the provided sources.
|
// Read all requirements from the provided sources.
|
||||||
let requirements = sources
|
let requirements = requirements
|
||||||
|
.iter()
|
||||||
|
.map(RequirementsSource::requirements)
|
||||||
|
.flatten_ok()
|
||||||
|
.collect::<Result<Vec<Requirement>>>()?;
|
||||||
|
let constraints = constraints
|
||||||
.iter()
|
.iter()
|
||||||
.map(RequirementsSource::requirements)
|
.map(RequirementsSource::requirements)
|
||||||
.flatten_ok()
|
.flatten_ok()
|
||||||
|
@ -53,7 +59,8 @@ pub(crate) async fn pip_compile(
|
||||||
let client = PypiClientBuilder::default().cache(cache).build();
|
let client = PypiClientBuilder::default().cache(cache).build();
|
||||||
|
|
||||||
// Resolve the dependencies.
|
// Resolve the dependencies.
|
||||||
let resolver = puffin_resolver::Resolver::new(requirements, markers, &tags, &client);
|
let resolver =
|
||||||
|
puffin_resolver::Resolver::new(requirements, constraints, markers, &tags, &client);
|
||||||
let resolution = match resolver.resolve().await {
|
let resolution = match resolver.resolve().await {
|
||||||
Err(puffin_resolver::ResolveError::PubGrub(pubgrub::error::PubGrubError::NoSolution(
|
Err(puffin_resolver::ResolveError::PubGrub(pubgrub::error::PubGrubError::NoSolution(
|
||||||
mut derivation_tree,
|
mut derivation_tree,
|
||||||
|
|
|
@ -39,12 +39,12 @@ enum Commands {
|
||||||
PipCompile(PipCompileArgs),
|
PipCompile(PipCompileArgs),
|
||||||
/// Sync dependencies from a `requirements.txt` file.
|
/// Sync dependencies from a `requirements.txt` file.
|
||||||
PipSync(PipSyncArgs),
|
PipSync(PipSyncArgs),
|
||||||
|
/// Uninstall packages from the current environment.
|
||||||
|
PipUninstall(PipUninstallArgs),
|
||||||
/// Clear the cache.
|
/// Clear the cache.
|
||||||
Clean,
|
Clean,
|
||||||
/// Enumerate the installed packages in the current environment.
|
/// Enumerate the installed packages in the current environment.
|
||||||
Freeze,
|
Freeze,
|
||||||
/// Uninstall packages from the current environment.
|
|
||||||
PipUninstall(PipUninstallArgs),
|
|
||||||
/// Create a virtual environment.
|
/// Create a virtual environment.
|
||||||
Venv(VenvArgs),
|
Venv(VenvArgs),
|
||||||
/// Add a dependency to the workspace.
|
/// Add a dependency to the workspace.
|
||||||
|
@ -59,6 +59,10 @@ struct PipCompileArgs {
|
||||||
#[clap(required(true))]
|
#[clap(required(true))]
|
||||||
src_file: Vec<PathBuf>,
|
src_file: Vec<PathBuf>,
|
||||||
|
|
||||||
|
/// Constrain versions using the given constraints files.
|
||||||
|
#[clap(short, long)]
|
||||||
|
constraint: Vec<PathBuf>,
|
||||||
|
|
||||||
/// Write the compiled requirements to the given `requirements.txt` file.
|
/// Write the compiled requirements to the given `requirements.txt` file.
|
||||||
#[clap(short, long)]
|
#[clap(short, long)]
|
||||||
output_file: Option<PathBuf>,
|
output_file: Option<PathBuf>,
|
||||||
|
@ -85,11 +89,12 @@ struct PipUninstallArgs {
|
||||||
|
|
||||||
#[derive(Args)]
|
#[derive(Args)]
|
||||||
struct VenvArgs {
|
struct VenvArgs {
|
||||||
/// The python interpreter to use for the virtual environment
|
/// The Python interpreter to use for the virtual environment.
|
||||||
// Short `-p` to match `virtualenv`
|
// Short `-p` to match `virtualenv`
|
||||||
// TODO(konstin): Support e.g. `-p 3.10`
|
// TODO(konstin): Support e.g. `-p 3.10`
|
||||||
#[clap(short, long)]
|
#[clap(short, long)]
|
||||||
python: Option<PathBuf>,
|
python: Option<PathBuf>,
|
||||||
|
|
||||||
/// The path to the virtual environment to create.
|
/// The path to the virtual environment to create.
|
||||||
name: PathBuf,
|
name: PathBuf,
|
||||||
}
|
}
|
||||||
|
@ -127,13 +132,19 @@ async fn main() -> ExitCode {
|
||||||
let result = match cli.command {
|
let result = match cli.command {
|
||||||
Commands::PipCompile(args) => {
|
Commands::PipCompile(args) => {
|
||||||
let dirs = ProjectDirs::from("", "", "puffin");
|
let dirs = ProjectDirs::from("", "", "puffin");
|
||||||
let sources = args
|
let requirements = args
|
||||||
.src_file
|
.src_file
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(RequirementsSource::from)
|
.map(RequirementsSource::from)
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
let constraints = args
|
||||||
|
.constraint
|
||||||
|
.into_iter()
|
||||||
|
.map(RequirementsSource::from)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
commands::pip_compile(
|
commands::pip_compile(
|
||||||
&sources,
|
&requirements,
|
||||||
|
&constraints,
|
||||||
args.output_file.as_deref(),
|
args.output_file.as_deref(),
|
||||||
dirs.as_ref()
|
dirs.as_ref()
|
||||||
.map(ProjectDirs::cache_dir)
|
.map(ProjectDirs::cache_dir)
|
||||||
|
|
|
@ -54,7 +54,9 @@ pub(crate) fn iter_requirements<'a>(
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert a PEP 508 specifier to a `PubGrub` range.
|
/// Convert a PEP 508 specifier to a `PubGrub` range.
|
||||||
fn version_range(specifiers: Option<&pep508_rs::VersionOrUrl>) -> Result<Range<PubGrubVersion>> {
|
pub(crate) fn version_range(
|
||||||
|
specifiers: Option<&pep508_rs::VersionOrUrl>,
|
||||||
|
) -> Result<Range<PubGrubVersion>> {
|
||||||
let Some(specifiers) = specifiers else {
|
let Some(specifiers) = specifiers else {
|
||||||
return Ok(Range::any());
|
return Ok(Range::any());
|
||||||
};
|
};
|
||||||
|
|
|
@ -27,13 +27,14 @@ use puffin_package::package_name::PackageName;
|
||||||
use wheel_filename::WheelFilename;
|
use wheel_filename::WheelFilename;
|
||||||
|
|
||||||
use crate::error::ResolveError;
|
use crate::error::ResolveError;
|
||||||
use crate::pubgrub::iter_requirements;
|
|
||||||
use crate::pubgrub::package::PubGrubPackage;
|
use crate::pubgrub::package::PubGrubPackage;
|
||||||
use crate::pubgrub::version::{PubGrubVersion, MIN_VERSION};
|
use crate::pubgrub::version::{PubGrubVersion, MIN_VERSION};
|
||||||
|
use crate::pubgrub::{iter_requirements, version_range};
|
||||||
use crate::resolution::{PinnedPackage, Resolution};
|
use crate::resolution::{PinnedPackage, Resolution};
|
||||||
|
|
||||||
pub struct Resolver<'a> {
|
pub struct Resolver<'a> {
|
||||||
requirements: Vec<Requirement>,
|
requirements: Vec<Requirement>,
|
||||||
|
constraints: Vec<Requirement>,
|
||||||
markers: &'a MarkerEnvironment,
|
markers: &'a MarkerEnvironment,
|
||||||
tags: &'a Tags,
|
tags: &'a Tags,
|
||||||
client: &'a PypiClient,
|
client: &'a PypiClient,
|
||||||
|
@ -44,12 +45,14 @@ impl<'a> Resolver<'a> {
|
||||||
/// Initialize a new resolver.
|
/// Initialize a new resolver.
|
||||||
pub fn new(
|
pub fn new(
|
||||||
requirements: Vec<Requirement>,
|
requirements: Vec<Requirement>,
|
||||||
|
constraints: Vec<Requirement>,
|
||||||
markers: &'a MarkerEnvironment,
|
markers: &'a MarkerEnvironment,
|
||||||
tags: &'a Tags,
|
tags: &'a Tags,
|
||||||
client: &'a PypiClient,
|
client: &'a PypiClient,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
requirements,
|
requirements,
|
||||||
|
constraints,
|
||||||
markers,
|
markers,
|
||||||
tags,
|
tags,
|
||||||
client,
|
client,
|
||||||
|
@ -376,6 +379,8 @@ impl<'a> Resolver<'a> {
|
||||||
match package {
|
match package {
|
||||||
PubGrubPackage::Root => {
|
PubGrubPackage::Root => {
|
||||||
let mut constraints = DependencyConstraints::default();
|
let mut constraints = DependencyConstraints::default();
|
||||||
|
|
||||||
|
// Add the root requirements.
|
||||||
for (package, version) in
|
for (package, version) in
|
||||||
iter_requirements(self.requirements.iter(), None, self.markers)
|
iter_requirements(self.requirements.iter(), None, self.markers)
|
||||||
{
|
{
|
||||||
|
@ -388,6 +393,18 @@ impl<'a> Resolver<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If any requirements were further constrained by the user, add those constraints.
|
||||||
|
for constraint in &self.constraints {
|
||||||
|
let package =
|
||||||
|
PubGrubPackage::Package(PackageName::normalize(&constraint.name), None);
|
||||||
|
if let Some(range) = constraints.get_mut(&package) {
|
||||||
|
*range = range.intersection(
|
||||||
|
&version_range(constraint.version_or_url.as_ref()).unwrap(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(Dependencies::Known(constraints))
|
Ok(Dependencies::Known(constraints))
|
||||||
}
|
}
|
||||||
PubGrubPackage::Package(package_name, extra) => {
|
PubGrubPackage::Package(package_name, extra) => {
|
||||||
|
@ -427,6 +444,17 @@ impl<'a> Resolver<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If any packages were further constrained by the user, add those constraints.
|
||||||
|
for constraint in &self.constraints {
|
||||||
|
let package =
|
||||||
|
PubGrubPackage::Package(PackageName::normalize(&constraint.name), None);
|
||||||
|
if let Some(range) = constraints.get_mut(&package) {
|
||||||
|
*range = range.intersection(
|
||||||
|
&version_range(constraint.version_or_url.as_ref()).unwrap(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(extra) = extra {
|
if let Some(extra) = extra {
|
||||||
if !metadata
|
if !metadata
|
||||||
.provides_extras
|
.provides_extras
|
||||||
|
|
|
@ -17,7 +17,8 @@ async fn pylint() -> Result<()> {
|
||||||
let client = PypiClientBuilder::default().build();
|
let client = PypiClientBuilder::default().build();
|
||||||
|
|
||||||
let requirements = vec![Requirement::from_str("pylint==2.3.0").unwrap()];
|
let requirements = vec![Requirement::from_str("pylint==2.3.0").unwrap()];
|
||||||
let resolver = Resolver::new(requirements, &MARKERS_311, &TAGS_311, &client);
|
let constraints = vec![];
|
||||||
|
let resolver = Resolver::new(requirements, constraints, &MARKERS_311, &TAGS_311, &client);
|
||||||
let resolution = resolver.resolve().await?;
|
let resolution = resolver.resolve().await?;
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
@ -39,7 +40,8 @@ async fn black() -> Result<()> {
|
||||||
let client = PypiClientBuilder::default().build();
|
let client = PypiClientBuilder::default().build();
|
||||||
|
|
||||||
let requirements = vec![Requirement::from_str("black<=23.9.1").unwrap()];
|
let requirements = vec![Requirement::from_str("black<=23.9.1").unwrap()];
|
||||||
let resolver = Resolver::new(requirements, &MARKERS_311, &TAGS_311, &client);
|
let constraints = vec![];
|
||||||
|
let resolver = Resolver::new(requirements, constraints, &MARKERS_311, &TAGS_311, &client);
|
||||||
let resolution = resolver.resolve().await?;
|
let resolution = resolver.resolve().await?;
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
@ -63,7 +65,8 @@ async fn black_colorama() -> Result<()> {
|
||||||
let client = PypiClientBuilder::default().build();
|
let client = PypiClientBuilder::default().build();
|
||||||
|
|
||||||
let requirements = vec![Requirement::from_str("black[colorama]<=23.9.1").unwrap()];
|
let requirements = vec![Requirement::from_str("black[colorama]<=23.9.1").unwrap()];
|
||||||
let resolver = Resolver::new(requirements, &MARKERS_311, &TAGS_311, &client);
|
let constraints = vec![];
|
||||||
|
let resolver = Resolver::new(requirements, constraints, &MARKERS_311, &TAGS_311, &client);
|
||||||
let resolution = resolver.resolve().await?;
|
let resolution = resolver.resolve().await?;
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
@ -88,7 +91,8 @@ async fn black_python_310() -> Result<()> {
|
||||||
let client = PypiClientBuilder::default().build();
|
let client = PypiClientBuilder::default().build();
|
||||||
|
|
||||||
let requirements = vec![Requirement::from_str("black<=23.9.1").unwrap()];
|
let requirements = vec![Requirement::from_str("black<=23.9.1").unwrap()];
|
||||||
let resolver = Resolver::new(requirements, &MARKERS_310, &TAGS_310, &client);
|
let constraints = vec![];
|
||||||
|
let resolver = Resolver::new(requirements, constraints, &MARKERS_310, &TAGS_310, &client);
|
||||||
let resolution = resolver.resolve().await?;
|
let resolution = resolver.resolve().await?;
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
@ -109,12 +113,94 @@ async fn black_python_310() -> Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Resolve `black` with a constraint on `mypy-extensions`, to ensure that constraints are
|
||||||
|
/// respected.
|
||||||
|
#[tokio::test]
|
||||||
|
async fn black_mypy_extensions() -> Result<()> {
|
||||||
|
let client = PypiClientBuilder::default().build();
|
||||||
|
|
||||||
|
let requirements = vec![Requirement::from_str("black<=23.9.1").unwrap()];
|
||||||
|
let constraints = vec![Requirement::from_str("mypy-extensions<1").unwrap()];
|
||||||
|
let resolver = Resolver::new(requirements, constraints, &MARKERS_311, &TAGS_311, &client);
|
||||||
|
let resolution = resolver.resolve().await?;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
format!("{resolution}"),
|
||||||
|
[
|
||||||
|
"black==23.9.1",
|
||||||
|
"click==8.1.7",
|
||||||
|
"mypy-extensions==0.4.3",
|
||||||
|
"packaging==23.2",
|
||||||
|
"pathspec==0.11.2",
|
||||||
|
"platformdirs==3.11.0"
|
||||||
|
]
|
||||||
|
.join("\n")
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolve `black` with a constraint on `mypy-extensions[extra]`, to ensure that extras are
|
||||||
|
/// ignored when resolving constraints.
|
||||||
|
#[tokio::test]
|
||||||
|
async fn black_mypy_extensions_extra() -> Result<()> {
|
||||||
|
let client = PypiClientBuilder::default().build();
|
||||||
|
|
||||||
|
let requirements = vec![Requirement::from_str("black<=23.9.1").unwrap()];
|
||||||
|
let constraints = vec![Requirement::from_str("mypy-extensions[extra]<1").unwrap()];
|
||||||
|
let resolver = Resolver::new(requirements, constraints, &MARKERS_311, &TAGS_311, &client);
|
||||||
|
let resolution = resolver.resolve().await?;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
format!("{resolution}"),
|
||||||
|
[
|
||||||
|
"black==23.9.1",
|
||||||
|
"click==8.1.7",
|
||||||
|
"mypy-extensions==0.4.3",
|
||||||
|
"packaging==23.2",
|
||||||
|
"pathspec==0.11.2",
|
||||||
|
"platformdirs==3.11.0"
|
||||||
|
]
|
||||||
|
.join("\n")
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolve `black` with a redundant constraint on `flake8`, to ensure that constraints don't
|
||||||
|
/// introduce new dependencies.
|
||||||
|
#[tokio::test]
|
||||||
|
async fn black_flake8() -> Result<()> {
|
||||||
|
let client = PypiClientBuilder::default().build();
|
||||||
|
|
||||||
|
let requirements = vec![Requirement::from_str("black<=23.9.1").unwrap()];
|
||||||
|
let constraints = vec![Requirement::from_str("flake8<1").unwrap()];
|
||||||
|
let resolver = Resolver::new(requirements, constraints, &MARKERS_311, &TAGS_311, &client);
|
||||||
|
let resolution = resolver.resolve().await?;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
format!("{resolution}"),
|
||||||
|
[
|
||||||
|
"black==23.9.1",
|
||||||
|
"click==8.1.7",
|
||||||
|
"mypy-extensions==1.0.0",
|
||||||
|
"packaging==23.2",
|
||||||
|
"pathspec==0.11.2",
|
||||||
|
"platformdirs==3.11.0"
|
||||||
|
]
|
||||||
|
.join("\n")
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn htmldate() -> Result<()> {
|
async fn htmldate() -> Result<()> {
|
||||||
let client = PypiClientBuilder::default().build();
|
let client = PypiClientBuilder::default().build();
|
||||||
|
|
||||||
let requirements = vec![Requirement::from_str("htmldate<=1.5.0").unwrap()];
|
let requirements = vec![Requirement::from_str("htmldate<=1.5.0").unwrap()];
|
||||||
let resolver = Resolver::new(requirements, &MARKERS_311, &TAGS_311, &client);
|
let constraints = vec![];
|
||||||
|
let resolver = Resolver::new(requirements, constraints, &MARKERS_311, &TAGS_311, &client);
|
||||||
let resolution = resolver.resolve().await?;
|
let resolution = resolver.resolve().await?;
|
||||||
|
|
||||||
// Resolves to `htmldate==1.4.3` (rather than `htmldate==1.5.2`) because `htmldate==1.5.2` has
|
// Resolves to `htmldate==1.4.3` (rather than `htmldate==1.5.2`) because `htmldate==1.5.2` has
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue