mirror of
https://github.com/astral-sh/uv.git
synced 2025-07-07 21:35:00 +00:00
Allow constraints to be provided in --upgrade-package
(#4952)
## Summary Allows, e.g., `--upgrade-package flask<3.0.0`. Closes https://github.com/astral-sh/uv/issues/1964.
This commit is contained in:
parent
5b6dffe522
commit
23eb42deed
15 changed files with 175 additions and 34 deletions
3
Cargo.lock
generated
3
Cargo.lock
generated
|
@ -2457,6 +2457,7 @@ dependencies = [
|
|||
"pyo3",
|
||||
"pyo3-log",
|
||||
"regex",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"testing_logger",
|
||||
|
@ -4598,6 +4599,8 @@ dependencies = [
|
|||
"fs-err",
|
||||
"insta",
|
||||
"install-wheel-rs",
|
||||
"pep508_rs",
|
||||
"pypi-types",
|
||||
"serde",
|
||||
"url",
|
||||
"uv-cache",
|
||||
|
|
|
@ -26,6 +26,7 @@ pep440_rs = { workspace = true }
|
|||
pyo3 = { workspace = true, optional = true, features = ["abi3", "extension-module"] }
|
||||
pyo3-log = { workspace = true, optional = true }
|
||||
regex = { workspace = true }
|
||||
schemars = { workspace = true, optional = true }
|
||||
serde = { workspace = true, features = ["derive", "rc"] }
|
||||
serde_json = { workspace = true, optional = true }
|
||||
thiserror = { workspace = true }
|
||||
|
@ -44,6 +45,7 @@ testing_logger = { version = "0.1.1" }
|
|||
[features]
|
||||
pyo3 = ["dep:pyo3", "pep440_rs/pyo3", "pyo3-log", "tracing", "tracing/log"]
|
||||
tracing = ["dep:tracing", "pep440_rs/tracing"]
|
||||
schemars = ["dep:schemars"]
|
||||
# PEP 508 allows only URLs such as `foo @ https://example.org/foo` or `foo @ file:///home/ferris/foo`, and
|
||||
# arguably does not allow relative paths in file URLs (`foo @ file://./foo`,
|
||||
# `foo @ file:foo-3.0.0-py3-none-any.whl`, `foo @ file://foo-3.0.0-py3-none-any.whl`), as they are not part of the
|
||||
|
|
|
@ -33,6 +33,8 @@ use pyo3::{
|
|||
create_exception, exceptions::PyNotImplementedError, pyclass, pyclass::CompareOp, pymethods,
|
||||
pymodule, types::PyModule, IntoPy, PyObject, PyResult, Python,
|
||||
};
|
||||
use schemars::gen::SchemaGenerator;
|
||||
use schemars::schema::Schema;
|
||||
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
|
||||
use thiserror::Error;
|
||||
use url::Url;
|
||||
|
@ -489,6 +491,25 @@ impl Reporter for TracingReporter {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "schemars")]
|
||||
impl<T: Pep508Url> schemars::JsonSchema for Requirement<T> {
|
||||
fn schema_name() -> String {
|
||||
"Requirement".to_string()
|
||||
}
|
||||
|
||||
fn json_schema(_gen: &mut SchemaGenerator) -> Schema {
|
||||
schemars::schema::SchemaObject {
|
||||
instance_type: Some(schemars::schema::InstanceType::String.into()),
|
||||
metadata: Some(Box::new(schemars::schema::Metadata {
|
||||
description: Some("A PEP 508 dependency specifier".to_string()),
|
||||
..schemars::schema::Metadata::default()
|
||||
})),
|
||||
..schemars::schema::SchemaObject::default()
|
||||
}
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Pep508Url> FromStr for Requirement<T> {
|
||||
type Err = Pep508Error<T>;
|
||||
|
||||
|
|
|
@ -16,12 +16,14 @@ workspace = true
|
|||
[dependencies]
|
||||
distribution-types = { workspace = true }
|
||||
install-wheel-rs = { workspace = true, features = ["clap"], default-features = false }
|
||||
pep508_rs = { workspace = true }
|
||||
pypi-types = { workspace = true }
|
||||
uv-cache = { workspace = true, features = ["clap"] }
|
||||
uv-configuration = { workspace = true, features = ["clap"] }
|
||||
uv-normalize = { workspace = true }
|
||||
uv-python = { workspace = true, features = ["clap", "schemars"]}
|
||||
uv-resolver = { workspace = true, features = ["clap"] }
|
||||
uv-settings = { workspace = true, features = ["schemars"] }
|
||||
uv-python = { workspace = true, features = ["clap", "schemars"]}
|
||||
uv-version = { workspace = true }
|
||||
uv-warnings = { workspace = true }
|
||||
|
||||
|
|
|
@ -7,6 +7,8 @@ use anyhow::{anyhow, Result};
|
|||
use clap::{Args, Parser, Subcommand};
|
||||
|
||||
use distribution_types::{FlatIndexLocation, IndexUrl};
|
||||
use pep508_rs::Requirement;
|
||||
use pypi_types::VerbatimParsedUrl;
|
||||
use uv_cache::CacheArgs;
|
||||
use uv_configuration::{
|
||||
ConfigSettingEntry, IndexStrategy, KeyringProviderType, PackageNameSpecifier, TargetTriple,
|
||||
|
@ -300,7 +302,7 @@ pub enum PipCommand {
|
|||
after_help = "Use `uv help pip sync` for more details.",
|
||||
after_long_help = ""
|
||||
)]
|
||||
Sync(PipSyncArgs),
|
||||
Sync(Box<PipSyncArgs>),
|
||||
/// Install packages into an environment.
|
||||
#[command(
|
||||
after_help = "Use `uv help pip install` for more details.",
|
||||
|
@ -2408,7 +2410,7 @@ pub struct ResolverArgs {
|
|||
/// Allow upgrades for a specific package, ignoring pinned versions in any existing output
|
||||
/// file.
|
||||
#[arg(long, short = 'P')]
|
||||
pub upgrade_package: Vec<PackageName>,
|
||||
pub upgrade_package: Vec<Requirement<VerbatimParsedUrl>>,
|
||||
|
||||
/// The strategy to use when resolving against multiple index URLs.
|
||||
///
|
||||
|
@ -2484,7 +2486,7 @@ pub struct ResolverInstallerArgs {
|
|||
/// Allow upgrades for a specific package, ignoring pinned versions in any existing output
|
||||
/// file.
|
||||
#[arg(long, short = 'P')]
|
||||
pub upgrade_package: Vec<PackageName>,
|
||||
pub upgrade_package: Vec<Requirement<VerbatimParsedUrl>>,
|
||||
|
||||
/// Reinstall all packages, regardless of whether they're already installed.
|
||||
#[arg(long, alias = "force-reinstall", overrides_with("no_reinstall"))]
|
||||
|
|
|
@ -13,7 +13,7 @@ license = { workspace = true }
|
|||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
pep508_rs = { workspace = true }
|
||||
pep508_rs = { workspace = true, features = ["schemars"] }
|
||||
platform-tags = { workspace = true }
|
||||
pypi-types = { workspace = true }
|
||||
uv-auth = { workspace = true }
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
use either::Either;
|
||||
use pep508_rs::MarkerTree;
|
||||
use pypi_types::Requirement;
|
||||
use rustc_hash::{FxBuildHasher, FxHashMap};
|
||||
use std::borrow::Cow;
|
||||
|
||||
use either::Either;
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
use pep508_rs::MarkerTree;
|
||||
use pypi_types::{Requirement, RequirementSource};
|
||||
use uv_normalize::PackageName;
|
||||
|
||||
/// A set of constraints for a set of requirements.
|
||||
|
@ -11,10 +13,16 @@ pub struct Constraints(FxHashMap<PackageName, Vec<Requirement>>);
|
|||
|
||||
impl Constraints {
|
||||
/// Create a new set of constraints from a set of requirements.
|
||||
pub fn from_requirements(requirements: Vec<Requirement>) -> Self {
|
||||
let mut constraints: FxHashMap<PackageName, Vec<Requirement>> =
|
||||
FxHashMap::with_capacity_and_hasher(requirements.len(), FxBuildHasher);
|
||||
pub fn from_requirements(requirements: impl Iterator<Item = Requirement>) -> Self {
|
||||
let mut constraints: FxHashMap<PackageName, Vec<Requirement>> = FxHashMap::default();
|
||||
for requirement in requirements {
|
||||
// Skip empty constraints.
|
||||
if let RequirementSource::Registry { specifier, .. } = &requirement.source {
|
||||
if specifier.is_empty() {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
constraints
|
||||
.entry(requirement.name.clone())
|
||||
.or_default()
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
use either::Either;
|
||||
use pep508_rs::PackageName;
|
||||
|
||||
use rustc_hash::FxHashSet;
|
||||
use pypi_types::Requirement;
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
/// Whether to reinstall packages.
|
||||
#[derive(Debug, Default, Clone)]
|
||||
|
@ -54,12 +56,12 @@ pub enum Upgrade {
|
|||
All,
|
||||
|
||||
/// Allow package upgrades, but only for the specified packages.
|
||||
Packages(FxHashSet<PackageName>),
|
||||
Packages(FxHashMap<PackageName, Vec<Requirement>>),
|
||||
}
|
||||
|
||||
impl Upgrade {
|
||||
/// Determine the upgrade strategy from the command-line arguments.
|
||||
pub fn from_args(upgrade: Option<bool>, upgrade_package: Vec<PackageName>) -> Self {
|
||||
pub fn from_args(upgrade: Option<bool>, upgrade_package: Vec<Requirement>) -> Self {
|
||||
match upgrade {
|
||||
Some(true) => Self::All,
|
||||
Some(false) => Self::None,
|
||||
|
@ -67,7 +69,15 @@ impl Upgrade {
|
|||
if upgrade_package.is_empty() {
|
||||
Self::None
|
||||
} else {
|
||||
Self::Packages(upgrade_package.into_iter().collect())
|
||||
Self::Packages(upgrade_package.into_iter().fold(
|
||||
FxHashMap::default(),
|
||||
|mut map, requirement| {
|
||||
map.entry(requirement.name.clone())
|
||||
.or_default()
|
||||
.push(requirement);
|
||||
map
|
||||
},
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -82,4 +92,28 @@ impl Upgrade {
|
|||
pub fn is_all(&self) -> bool {
|
||||
matches!(self, Self::All)
|
||||
}
|
||||
|
||||
/// Returns `true` if the specified package should be upgraded.
|
||||
pub fn contains(&self, package_name: &PackageName) -> bool {
|
||||
match &self {
|
||||
Self::None => false,
|
||||
Self::All => true,
|
||||
Self::Packages(packages) => packages.contains_key(package_name),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns an iterator over the constraints.
|
||||
///
|
||||
/// When upgrading, users can provide bounds on the upgrade (e.g., `--upgrade-package flask<3`).
|
||||
pub fn constraints(&self) -> impl Iterator<Item = &Requirement> {
|
||||
if let Self::Packages(packages) = self {
|
||||
Either::Right(
|
||||
packages
|
||||
.values()
|
||||
.flat_map(|requirements| requirements.iter()),
|
||||
)
|
||||
} else {
|
||||
Either::Left(std::iter::empty())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -56,7 +56,7 @@ pub async fn read_requirements_txt(
|
|||
// Ignore pinned versions for the specified packages.
|
||||
Upgrade::Packages(packages) => preferences
|
||||
.into_iter()
|
||||
.filter(|preference| !packages.contains(preference.name()))
|
||||
.filter(|preference| !packages.contains_key(preference.name()))
|
||||
.collect(),
|
||||
})
|
||||
}
|
||||
|
@ -68,11 +68,7 @@ pub fn read_lock_requirements(lock: &Lock, upgrade: &Upgrade) -> LockedRequireme
|
|||
|
||||
for dist in lock.distributions() {
|
||||
// Skip the distribution if it's not included in the upgrade strategy.
|
||||
if match upgrade {
|
||||
Upgrade::None => false,
|
||||
Upgrade::All => true,
|
||||
Upgrade::Packages(packages) => packages.contains(dist.name()),
|
||||
} {
|
||||
if upgrade.contains(dist.name()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
|
@ -26,7 +26,7 @@ impl Exclusions {
|
|||
};
|
||||
|
||||
if let Upgrade::Packages(packages) = upgrade {
|
||||
exclusions.extend(packages);
|
||||
exclusions.extend(packages.into_keys());
|
||||
};
|
||||
|
||||
if exclusions.is_empty() {
|
||||
|
|
|
@ -4,6 +4,7 @@ use serde::Deserialize;
|
|||
|
||||
use distribution_types::{FlatIndexLocation, IndexUrl};
|
||||
use install_wheel_rs::linker::LinkMode;
|
||||
use pep508_rs::Requirement;
|
||||
use pypi_types::VerbatimParsedUrl;
|
||||
use uv_configuration::{
|
||||
ConfigSettings, IndexStrategy, KeyringProviderType, PackageNameSpecifier, TargetTriple,
|
||||
|
@ -105,7 +106,7 @@ pub struct ResolverOptions {
|
|||
pub exclude_newer: Option<ExcludeNewer>,
|
||||
pub link_mode: Option<LinkMode>,
|
||||
pub upgrade: Option<bool>,
|
||||
pub upgrade_package: Option<Vec<PackageName>>,
|
||||
pub upgrade_package: Option<Vec<Requirement<VerbatimParsedUrl>>>,
|
||||
pub no_build: Option<bool>,
|
||||
pub no_build_package: Option<Vec<PackageName>>,
|
||||
pub no_binary: Option<bool>,
|
||||
|
@ -132,7 +133,7 @@ pub struct ResolverInstallerOptions {
|
|||
pub link_mode: Option<LinkMode>,
|
||||
pub compile_bytecode: Option<bool>,
|
||||
pub upgrade: Option<bool>,
|
||||
pub upgrade_package: Option<Vec<PackageName>>,
|
||||
pub upgrade_package: Option<Vec<Requirement<VerbatimParsedUrl>>>,
|
||||
pub reinstall: Option<bool>,
|
||||
pub reinstall_package: Option<Vec<PackageName>>,
|
||||
pub no_build: Option<bool>,
|
||||
|
@ -193,7 +194,7 @@ pub struct PipOptions {
|
|||
pub compile_bytecode: Option<bool>,
|
||||
pub require_hashes: Option<bool>,
|
||||
pub upgrade: Option<bool>,
|
||||
pub upgrade_package: Option<Vec<PackageName>>,
|
||||
pub upgrade_package: Option<Vec<Requirement<VerbatimParsedUrl>>>,
|
||||
pub reinstall: Option<bool>,
|
||||
pub reinstall_package: Option<Vec<PackageName>>,
|
||||
pub concurrent_downloads: Option<NonZeroUsize>,
|
||||
|
|
|
@ -180,7 +180,11 @@ pub(crate) async fn resolve<InstalledPackages: InstalledPackagesProvider>(
|
|||
.await?;
|
||||
|
||||
// Collect constraints and overrides.
|
||||
let constraints = Constraints::from_requirements(constraints);
|
||||
let constraints = Constraints::from_requirements(
|
||||
constraints
|
||||
.into_iter()
|
||||
.chain(upgrade.constraints().cloned()),
|
||||
);
|
||||
let overrides = Overrides::from_requirements(overrides);
|
||||
let preferences = Preferences::from_iter(preferences, markers);
|
||||
|
||||
|
|
|
@ -798,7 +798,7 @@ pub(crate) struct PipSyncSettings {
|
|||
|
||||
impl PipSyncSettings {
|
||||
/// Resolve the [`PipSyncSettings`] from the CLI and filesystem configuration.
|
||||
pub(crate) fn resolve(args: PipSyncArgs, filesystem: Option<FilesystemOptions>) -> Self {
|
||||
pub(crate) fn resolve(args: Box<PipSyncArgs>, filesystem: Option<FilesystemOptions>) -> Self {
|
||||
let PipSyncArgs {
|
||||
src_file,
|
||||
constraint,
|
||||
|
@ -829,7 +829,7 @@ impl PipSyncSettings {
|
|||
no_strict,
|
||||
dry_run,
|
||||
compat_args: _,
|
||||
} = args;
|
||||
} = *args;
|
||||
|
||||
Self {
|
||||
src_file,
|
||||
|
@ -1384,7 +1384,10 @@ impl ResolverSettings {
|
|||
args.upgrade.combine(upgrade),
|
||||
args.upgrade_package
|
||||
.combine(upgrade_package)
|
||||
.unwrap_or_default(),
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(Requirement::from)
|
||||
.collect(),
|
||||
),
|
||||
build_options: BuildOptions::new(
|
||||
NoBinary::from_args(
|
||||
|
@ -1522,7 +1525,10 @@ impl ResolverInstallerSettings {
|
|||
args.upgrade.combine(upgrade),
|
||||
args.upgrade_package
|
||||
.combine(upgrade_package)
|
||||
.unwrap_or_default(),
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(Requirement::from)
|
||||
.collect(),
|
||||
),
|
||||
reinstall: Reinstall::from_args(
|
||||
args.reinstall.combine(reinstall),
|
||||
|
@ -1841,7 +1847,10 @@ impl PipSettings {
|
|||
args.upgrade.combine(upgrade),
|
||||
args.upgrade_package
|
||||
.combine(upgrade_package)
|
||||
.unwrap_or_default(),
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(Requirement::from)
|
||||
.collect(),
|
||||
),
|
||||
reinstall: Reinstall::from_args(
|
||||
args.reinstall.combine(reinstall),
|
||||
|
|
|
@ -4770,6 +4770,61 @@ fn upgrade_package() -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Upgrade a package with a constraint on the allowed upgrade.
|
||||
#[test]
|
||||
fn upgrade_constraint() -> Result<()> {
|
||||
let context = TestContext::new("3.12");
|
||||
let requirements_in = context.temp_dir.child("requirements.in");
|
||||
requirements_in.write_str("iniconfig")?;
|
||||
|
||||
let requirements_txt = context.temp_dir.child("requirements.txt");
|
||||
requirements_txt.write_str(indoc! {r"
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# uv pip compile requirements.in --python-version 3.12 --cache-dir [CACHE_DIR]
|
||||
iniconfig==1.0.0
|
||||
"})?;
|
||||
|
||||
uv_snapshot!(context.pip_compile()
|
||||
.arg("requirements.in")
|
||||
.arg("--output-file")
|
||||
.arg("requirements.txt")
|
||||
.arg("--upgrade-package")
|
||||
.arg("iniconfig<2"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# uv pip compile --cache-dir [CACHE_DIR] requirements.in --output-file requirements.txt --upgrade-package iniconfig<2
|
||||
iniconfig==1.1.1
|
||||
# via -r requirements.in
|
||||
|
||||
----- stderr -----
|
||||
Resolved 1 package in [TIME]
|
||||
"###
|
||||
);
|
||||
|
||||
uv_snapshot!(context.pip_compile()
|
||||
.arg("requirements.in")
|
||||
.arg("--output-file")
|
||||
.arg("requirements.txt")
|
||||
.arg("--upgrade-package")
|
||||
.arg("iniconfig"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# uv pip compile --cache-dir [CACHE_DIR] requirements.in --output-file requirements.txt --upgrade-package iniconfig
|
||||
iniconfig==2.0.0
|
||||
# via -r requirements.in
|
||||
|
||||
----- stderr -----
|
||||
Resolved 1 package in [TIME]
|
||||
"###
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Attempt to resolve a requirement at a path that doesn't exist.
|
||||
#[test]
|
||||
fn missing_path_requirement() -> Result<()> {
|
||||
|
|
8
uv.schema.json
generated
8
uv.schema.json
generated
|
@ -267,7 +267,7 @@
|
|||
"null"
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/definitions/PackageName"
|
||||
"$ref": "#/definitions/Requirement"
|
||||
}
|
||||
},
|
||||
"workspace": {
|
||||
|
@ -826,7 +826,7 @@
|
|||
"null"
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/definitions/PackageName"
|
||||
"$ref": "#/definitions/Requirement"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -933,6 +933,10 @@
|
|||
"type": "string",
|
||||
"pattern": "^3\\.\\d+(\\.\\d+)?$"
|
||||
},
|
||||
"Requirement": {
|
||||
"description": "A PEP 508 dependency specifier",
|
||||
"type": "string"
|
||||
},
|
||||
"ResolutionMode": {
|
||||
"oneOf": [
|
||||
{
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue