mirror of https://github.com/astral-sh/uv
Set lower bounds in `uv add` (#5688)
## Summary Closes https://github.com/astral-sh/uv/issues/5178.
This commit is contained in:
parent
159108b728
commit
ad384ecacf
|
|
@ -2,10 +2,10 @@ use std::path::Path;
|
|||
use std::str::FromStr;
|
||||
use std::{fmt, mem};
|
||||
|
||||
use pep440_rs::{Version, VersionSpecifier, VersionSpecifiers};
|
||||
use pep508_rs::{ExtraName, PackageName, Requirement, VersionOrUrl};
|
||||
use thiserror::Error;
|
||||
use toml_edit::{Array, DocumentMut, Item, RawString, Table, TomlError, Value};
|
||||
|
||||
use pep508_rs::{ExtraName, PackageName, Requirement, VersionOrUrl};
|
||||
use uv_fs::PortablePath;
|
||||
|
||||
use crate::pyproject::{DependencyType, PyProjectToml, Source};
|
||||
|
|
@ -32,10 +32,21 @@ pub enum Error {
|
|||
MalformedSources,
|
||||
#[error("Workspace in `pyproject.toml` is malformed")]
|
||||
MalformedWorkspace,
|
||||
#[error("Expected a dependency at index {0}")]
|
||||
MissingDependency(usize),
|
||||
#[error("Cannot perform ambiguous update; found multiple entries with matching package names")]
|
||||
Ambiguous,
|
||||
}
|
||||
|
||||
/// The result of editing an array in a TOML document.
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
pub enum ArrayEdit {
|
||||
/// An existing entry (at the given index) was updated.
|
||||
Update(usize),
|
||||
/// A new entry was added at the given index (typically, the end of the array).
|
||||
Add(usize),
|
||||
}
|
||||
|
||||
impl PyProjectTomlMut {
|
||||
/// Initialize a [`PyProjectTomlMut`] from a [`PyProjectToml`].
|
||||
pub fn from_toml(pyproject: &PyProjectToml) -> Result<Self, Error> {
|
||||
|
|
@ -78,11 +89,13 @@ impl PyProjectTomlMut {
|
|||
}
|
||||
|
||||
/// Adds a dependency to `project.dependencies`.
|
||||
///
|
||||
/// Returns `true` if the dependency was added, `false` if it was updated.
|
||||
pub fn add_dependency(
|
||||
&mut self,
|
||||
req: Requirement,
|
||||
source: Option<Source>,
|
||||
) -> Result<(), Error> {
|
||||
req: &Requirement,
|
||||
source: Option<&Source>,
|
||||
) -> Result<ArrayEdit, Error> {
|
||||
// Get or create `project.dependencies`.
|
||||
let dependencies = self
|
||||
.doc
|
||||
|
|
@ -96,21 +109,23 @@ impl PyProjectTomlMut {
|
|||
.ok_or(Error::MalformedDependencies)?;
|
||||
|
||||
let name = req.name.clone();
|
||||
add_dependency(req, dependencies, source.is_some())?;
|
||||
let edit = add_dependency(req, dependencies, source.is_some())?;
|
||||
|
||||
if let Some(source) = source {
|
||||
self.add_source(&name, &source)?;
|
||||
self.add_source(&name, source)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
Ok(edit)
|
||||
}
|
||||
|
||||
/// Adds a development dependency to `tool.uv.dev-dependencies`.
|
||||
///
|
||||
/// Returns `true` if the dependency was added, `false` if it was updated.
|
||||
pub fn add_dev_dependency(
|
||||
&mut self,
|
||||
req: Requirement,
|
||||
source: Option<Source>,
|
||||
) -> Result<(), Error> {
|
||||
req: &Requirement,
|
||||
source: Option<&Source>,
|
||||
) -> Result<ArrayEdit, Error> {
|
||||
// Get or create `tool.uv.dev-dependencies`.
|
||||
let dev_dependencies = self
|
||||
.doc
|
||||
|
|
@ -128,22 +143,24 @@ impl PyProjectTomlMut {
|
|||
.ok_or(Error::MalformedDependencies)?;
|
||||
|
||||
let name = req.name.clone();
|
||||
add_dependency(req, dev_dependencies, source.is_some())?;
|
||||
let edit = add_dependency(req, dev_dependencies, source.is_some())?;
|
||||
|
||||
if let Some(source) = source {
|
||||
self.add_source(&name, &source)?;
|
||||
self.add_source(&name, source)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
Ok(edit)
|
||||
}
|
||||
|
||||
/// Adds a dependency to `project.optional-dependencies`.
|
||||
///
|
||||
/// Returns `true` if the dependency was added, `false` if it was updated.
|
||||
pub fn add_optional_dependency(
|
||||
&mut self,
|
||||
req: Requirement,
|
||||
group: &ExtraName,
|
||||
source: Option<Source>,
|
||||
) -> Result<(), Error> {
|
||||
req: &Requirement,
|
||||
source: Option<&Source>,
|
||||
) -> Result<ArrayEdit, Error> {
|
||||
// Get or create `project.optional-dependencies`.
|
||||
let optional_dependencies = self
|
||||
.doc
|
||||
|
|
@ -163,12 +180,125 @@ impl PyProjectTomlMut {
|
|||
.ok_or(Error::MalformedDependencies)?;
|
||||
|
||||
let name = req.name.clone();
|
||||
add_dependency(req, group, source.is_some())?;
|
||||
let added = add_dependency(req, group, source.is_some())?;
|
||||
|
||||
if let Some(source) = source {
|
||||
self.add_source(&name, &source)?;
|
||||
self.add_source(&name, source)?;
|
||||
}
|
||||
|
||||
Ok(added)
|
||||
}
|
||||
|
||||
/// Set the minimum version for an existing dependency in `project.dependencies`.
|
||||
pub fn set_dependency_minimum_version(
|
||||
&mut self,
|
||||
index: usize,
|
||||
version: &Version,
|
||||
) -> Result<(), Error> {
|
||||
// Get or create `project.dependencies`.
|
||||
let dependencies = self
|
||||
.doc
|
||||
.entry("project")
|
||||
.or_insert(Item::Table(Table::new()))
|
||||
.as_table_mut()
|
||||
.ok_or(Error::MalformedDependencies)?
|
||||
.entry("dependencies")
|
||||
.or_insert(Item::Value(Value::Array(Array::new())))
|
||||
.as_array_mut()
|
||||
.ok_or(Error::MalformedDependencies)?;
|
||||
|
||||
let Some(req) = dependencies.get(index) else {
|
||||
return Err(Error::MissingDependency(index));
|
||||
};
|
||||
|
||||
let mut req = req
|
||||
.as_str()
|
||||
.and_then(try_parse_requirement)
|
||||
.ok_or(Error::MalformedDependencies)?;
|
||||
req.version_or_url = Some(VersionOrUrl::VersionSpecifier(VersionSpecifiers::from(
|
||||
VersionSpecifier::greater_than_equal_version(version.clone()),
|
||||
)));
|
||||
dependencies.replace(index, req.to_string());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set the minimum version for an existing dependency in `tool.uv.dev-dependencies`.
|
||||
pub fn set_dev_dependency_minimum_version(
|
||||
&mut self,
|
||||
index: usize,
|
||||
version: &Version,
|
||||
) -> Result<(), Error> {
|
||||
// Get or create `tool.uv.dev-dependencies`.
|
||||
let dev_dependencies = self
|
||||
.doc
|
||||
.entry("tool")
|
||||
.or_insert(implicit())
|
||||
.as_table_mut()
|
||||
.ok_or(Error::MalformedSources)?
|
||||
.entry("uv")
|
||||
.or_insert(Item::Table(Table::new()))
|
||||
.as_table_mut()
|
||||
.ok_or(Error::MalformedSources)?
|
||||
.entry("dev-dependencies")
|
||||
.or_insert(Item::Value(Value::Array(Array::new())))
|
||||
.as_array_mut()
|
||||
.ok_or(Error::MalformedDependencies)?;
|
||||
|
||||
let Some(req) = dev_dependencies.get(index) else {
|
||||
return Err(Error::MissingDependency(index));
|
||||
};
|
||||
|
||||
let mut req = req
|
||||
.as_str()
|
||||
.and_then(try_parse_requirement)
|
||||
.ok_or(Error::MalformedDependencies)?;
|
||||
req.version_or_url = Some(VersionOrUrl::VersionSpecifier(VersionSpecifiers::from(
|
||||
VersionSpecifier::greater_than_equal_version(version.clone()),
|
||||
)));
|
||||
dev_dependencies.replace(index, req.to_string());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set the minimum version for an existing dependency in `project.optional-dependencies`.
|
||||
pub fn set_optional_dependency_minimum_version(
|
||||
&mut self,
|
||||
group: &ExtraName,
|
||||
index: usize,
|
||||
version: &Version,
|
||||
) -> Result<(), Error> {
|
||||
// Get or create `project.optional-dependencies`.
|
||||
let optional_dependencies = self
|
||||
.doc
|
||||
.entry("project")
|
||||
.or_insert(Item::Table(Table::new()))
|
||||
.as_table_mut()
|
||||
.ok_or(Error::MalformedDependencies)?
|
||||
.entry("optional-dependencies")
|
||||
.or_insert(Item::Table(Table::new()))
|
||||
.as_table_mut()
|
||||
.ok_or(Error::MalformedDependencies)?;
|
||||
|
||||
let group = optional_dependencies
|
||||
.entry(group.as_ref())
|
||||
.or_insert(Item::Value(Value::Array(Array::new())))
|
||||
.as_array_mut()
|
||||
.ok_or(Error::MalformedDependencies)?;
|
||||
|
||||
let Some(req) = group.get(index) else {
|
||||
return Err(Error::MissingDependency(index));
|
||||
};
|
||||
|
||||
let mut req = req
|
||||
.as_str()
|
||||
.and_then(try_parse_requirement)
|
||||
.ok_or(Error::MalformedDependencies)?;
|
||||
req.version_or_url = Some(VersionOrUrl::VersionSpecifier(VersionSpecifiers::from(
|
||||
VersionSpecifier::greater_than_equal_version(version.clone()),
|
||||
)));
|
||||
group.replace(index, req.to_string());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -267,7 +397,7 @@ impl PyProjectTomlMut {
|
|||
Ok(requirements)
|
||||
}
|
||||
|
||||
// Remove a matching source from `tool.uv.sources`, if it exists.
|
||||
/// Remove a matching source from `tool.uv.sources`, if it exists.
|
||||
fn remove_source(&mut self, name: &PackageName) -> Result<(), Error> {
|
||||
if let Some(sources) = self
|
||||
.doc
|
||||
|
|
@ -350,27 +480,37 @@ fn implicit() -> Item {
|
|||
}
|
||||
|
||||
/// Adds a dependency to the given `deps` array.
|
||||
pub fn add_dependency(req: Requirement, deps: &mut Array, has_source: bool) -> Result<(), Error> {
|
||||
///
|
||||
/// Returns `true` if the dependency was added, `false` if it was updated.
|
||||
pub fn add_dependency(
|
||||
req: &Requirement,
|
||||
deps: &mut Array,
|
||||
has_source: bool,
|
||||
) -> Result<ArrayEdit, Error> {
|
||||
// Find matching dependencies.
|
||||
let mut to_replace = find_dependencies(&req.name, deps);
|
||||
match to_replace.as_slice() {
|
||||
[] => deps.push(req.to_string()),
|
||||
[] => {
|
||||
deps.push(req.to_string());
|
||||
reformat_array_multiline(deps);
|
||||
Ok(ArrayEdit::Add(deps.len() - 1))
|
||||
}
|
||||
[_] => {
|
||||
let (i, mut old_req) = to_replace.remove(0);
|
||||
update_requirement(&mut old_req, req, has_source);
|
||||
deps.replace(i, old_req.to_string());
|
||||
reformat_array_multiline(deps);
|
||||
Ok(ArrayEdit::Update(i))
|
||||
}
|
||||
// Cannot perform ambiguous updates.
|
||||
_ => return Err(Error::Ambiguous),
|
||||
_ => Err(Error::Ambiguous),
|
||||
}
|
||||
reformat_array_multiline(deps);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Update an existing requirement.
|
||||
fn update_requirement(old: &mut Requirement, new: Requirement, has_source: bool) {
|
||||
fn update_requirement(old: &mut Requirement, new: &Requirement, has_source: bool) {
|
||||
// Add any new extras.
|
||||
old.extras.extend(new.extras);
|
||||
old.extras.extend(new.extras.iter().cloned());
|
||||
old.extras.sort_unstable();
|
||||
old.extras.dedup();
|
||||
|
||||
|
|
@ -380,14 +520,14 @@ fn update_requirement(old: &mut Requirement, new: Requirement, has_source: bool)
|
|||
}
|
||||
|
||||
// Update the source if a new one was specified.
|
||||
match new.version_or_url {
|
||||
match &new.version_or_url {
|
||||
None => {}
|
||||
Some(VersionOrUrl::VersionSpecifier(specifier)) if specifier.is_empty() => {}
|
||||
Some(version_or_url) => old.version_or_url = Some(version_or_url),
|
||||
Some(version_or_url) => old.version_or_url = Some(version_or_url.clone()),
|
||||
}
|
||||
|
||||
// Update the marker expression.
|
||||
if let Some(marker) = new.marker {
|
||||
if let Some(marker) = new.marker.clone() {
|
||||
old.marker = Some(marker);
|
||||
}
|
||||
}
|
||||
|
|
@ -412,8 +552,8 @@ fn remove_dependency(req: &PackageName, deps: &mut Array) -> Vec<Requirement> {
|
|||
removed
|
||||
}
|
||||
|
||||
// Returns a `Vec` containing the all dependencies with the given name, along with their positions
|
||||
// in the array.
|
||||
/// Returns a `Vec` containing the all dependencies with the given name, along with their positions
|
||||
/// in the array.
|
||||
fn find_dependencies(name: &PackageName, deps: &Array) -> Vec<(usize, Requirement)> {
|
||||
let mut to_replace = Vec::new();
|
||||
for (i, dep) in deps.iter().enumerate() {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
use anyhow::{Context, Result};
|
||||
|
||||
use pep508_rs::ExtraName;
|
||||
use pep508_rs::{ExtraName, Requirement, VersionOrUrl};
|
||||
use rustc_hash::{FxBuildHasher, FxHashMap};
|
||||
use std::collections::hash_map::Entry;
|
||||
use uv_auth::store_credentials_from_url;
|
||||
use uv_cache::Cache;
|
||||
use uv_client::{BaseClientBuilder, Connectivity, FlatIndexClient, RegistryClientBuilder};
|
||||
|
|
@ -15,7 +16,7 @@ use uv_resolver::FlatIndex;
|
|||
use uv_types::{BuildIsolation, HashStrategy};
|
||||
use uv_warnings::warn_user_once;
|
||||
use uv_workspace::pyproject::{DependencyType, Source, SourceError};
|
||||
use uv_workspace::pyproject_mut::PyProjectTomlMut;
|
||||
use uv_workspace::pyproject_mut::{ArrayEdit, PyProjectTomlMut};
|
||||
use uv_workspace::{DiscoveryOptions, ProjectWorkspace, VirtualProject, Workspace};
|
||||
|
||||
use crate::commands::pip::operations::Modifications;
|
||||
|
|
@ -157,21 +158,25 @@ pub(crate) async fn add(
|
|||
// Add the requirements to the `pyproject.toml`.
|
||||
let existing = project.current_project().pyproject_toml();
|
||||
let mut pyproject = PyProjectTomlMut::from_toml(existing)?;
|
||||
for mut req in requirements {
|
||||
let mut edits = Vec::with_capacity(requirements.len());
|
||||
for mut requirement in requirements {
|
||||
// Add the specified extras.
|
||||
req.extras.extend(extras.iter().cloned());
|
||||
req.extras.sort_unstable();
|
||||
req.extras.dedup();
|
||||
requirement.extras.extend(extras.iter().cloned());
|
||||
requirement.extras.sort_unstable();
|
||||
requirement.extras.dedup();
|
||||
|
||||
let (req, source) = if raw_sources {
|
||||
let (requirement, source) = if raw_sources {
|
||||
// Use the PEP 508 requirement directly.
|
||||
(pep508_rs::Requirement::from(req), None)
|
||||
(pep508_rs::Requirement::from(requirement), None)
|
||||
} else {
|
||||
// Otherwise, try to construct the source.
|
||||
let workspace = project.workspace().packages().contains_key(&req.name);
|
||||
let workspace = project
|
||||
.workspace()
|
||||
.packages()
|
||||
.contains_key(&requirement.name);
|
||||
let result = Source::from_requirement(
|
||||
&req.name,
|
||||
req.source.clone(),
|
||||
&requirement.name,
|
||||
requirement.source.clone(),
|
||||
workspace,
|
||||
editable,
|
||||
rev.clone(),
|
||||
|
|
@ -182,29 +187,36 @@ pub(crate) async fn add(
|
|||
let source = match result {
|
||||
Ok(source) => source,
|
||||
Err(SourceError::UnresolvedReference(rev)) => {
|
||||
anyhow::bail!("Cannot resolve Git reference `{rev}` for requirement `{}`. Specify the reference with one of `--tag`, `--branch`, or `--rev`, or use the `--raw-sources` flag.", req.name)
|
||||
anyhow::bail!("Cannot resolve Git reference `{rev}` for requirement `{name}`. Specify the reference with one of `--tag`, `--branch`, or `--rev`, or use the `--raw-sources` flag.", name = requirement.name)
|
||||
}
|
||||
Err(err) => return Err(err.into()),
|
||||
};
|
||||
|
||||
// Ignore the PEP 508 source.
|
||||
let mut req = pep508_rs::Requirement::from(req);
|
||||
req.clear_url();
|
||||
let mut requirement = pep508_rs::Requirement::from(requirement);
|
||||
requirement.clear_url();
|
||||
|
||||
(req, source)
|
||||
(requirement, source)
|
||||
};
|
||||
|
||||
match dependency_type {
|
||||
// Update the `pyproject.toml`.
|
||||
let edit = match dependency_type {
|
||||
DependencyType::Production => {
|
||||
pyproject.add_dependency(req, source)?;
|
||||
}
|
||||
DependencyType::Dev => {
|
||||
pyproject.add_dev_dependency(req, source)?;
|
||||
pyproject.add_dependency(&requirement, source.as_ref())?
|
||||
}
|
||||
DependencyType::Dev => pyproject.add_dev_dependency(&requirement, source.as_ref())?,
|
||||
DependencyType::Optional(ref group) => {
|
||||
pyproject.add_optional_dependency(req, group, source)?;
|
||||
pyproject.add_optional_dependency(group, &requirement, source.as_ref())?
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Keep track of the exact location of the edit.
|
||||
edits.push(DependencyEdit {
|
||||
dependency_type: &dependency_type,
|
||||
requirement,
|
||||
source,
|
||||
edit,
|
||||
});
|
||||
}
|
||||
|
||||
// Save the modified `pyproject.toml`.
|
||||
|
|
@ -264,6 +276,78 @@ pub(crate) async fn add(
|
|||
Err(err) => return Err(err.into()),
|
||||
};
|
||||
|
||||
// Avoid modifying the user request further if `--raw-sources` is set.
|
||||
if !raw_sources {
|
||||
// Extract the minimum-supported version for each dependency.
|
||||
let mut minimum_version =
|
||||
FxHashMap::with_capacity_and_hasher(lock.lock.distributions().len(), FxBuildHasher);
|
||||
for dist in lock.lock.distributions() {
|
||||
let name = dist.name();
|
||||
let version = dist.version();
|
||||
match minimum_version.entry(name) {
|
||||
Entry::Vacant(entry) => {
|
||||
entry.insert(version);
|
||||
}
|
||||
Entry::Occupied(mut entry) => {
|
||||
if version < *entry.get() {
|
||||
entry.insert(version);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If any of the requirements were added without version specifiers, add a lower bound.
|
||||
let mut modified = false;
|
||||
for edit in &edits {
|
||||
// Only set a minimum version for newly-added dependencies (as opposed to updates).
|
||||
let ArrayEdit::Add(index) = &edit.edit else {
|
||||
continue;
|
||||
};
|
||||
|
||||
// Only set a minimum version for registry requirements.
|
||||
if edit.source.is_some() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Only set a minimum version for registry requirements.
|
||||
let is_empty = match edit.requirement.version_or_url.as_ref() {
|
||||
Some(VersionOrUrl::VersionSpecifier(version)) => version.is_empty(),
|
||||
Some(VersionOrUrl::Url(_)) => false,
|
||||
None => true,
|
||||
};
|
||||
if !is_empty {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Set the minimum version.
|
||||
let Some(minimum) = minimum_version.get(&edit.requirement.name) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
match edit.dependency_type {
|
||||
DependencyType::Production => {
|
||||
pyproject.set_dependency_minimum_version(*index, minimum)?;
|
||||
}
|
||||
DependencyType::Dev => {
|
||||
pyproject.set_dev_dependency_minimum_version(*index, minimum)?;
|
||||
}
|
||||
DependencyType::Optional(ref group) => {
|
||||
pyproject.set_optional_dependency_minimum_version(group, *index, minimum)?;
|
||||
}
|
||||
}
|
||||
|
||||
modified = true;
|
||||
}
|
||||
|
||||
// Save the modified `pyproject.toml`.
|
||||
if modified {
|
||||
fs_err::write(
|
||||
project.current_project().root().join("pyproject.toml"),
|
||||
pyproject.to_string(),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
// Perform a full sync, because we don't know what exactly is affected by the removal.
|
||||
// TODO(ibraheem): Should we accept CLI overrides for this? Should we even sync here?
|
||||
let extras = ExtrasSpecification::All;
|
||||
|
|
@ -290,6 +374,14 @@ pub(crate) async fn add(
|
|||
Ok(ExitStatus::Success)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct DependencyEdit<'a> {
|
||||
dependency_type: &'a DependencyType,
|
||||
requirement: Requirement,
|
||||
source: Option<Source>,
|
||||
edit: ArrayEdit,
|
||||
}
|
||||
|
||||
/// Render a [`uv_resolver::NoSolutionError`] with a help message.
|
||||
#[derive(Debug, miette::Diagnostic, thiserror::Error)]
|
||||
#[error("{header}")]
|
||||
|
|
|
|||
|
|
@ -2187,3 +2187,325 @@ fn add_error() -> Result<()> {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set a lower bound when adding unconstrained dependencies.
|
||||
#[test]
|
||||
fn add_lower_bound() -> Result<()> {
|
||||
let context = TestContext::new("3.12");
|
||||
|
||||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||||
pyproject_toml.write_str(indoc! {r#"
|
||||
[project]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = []
|
||||
"#})?;
|
||||
|
||||
// Adding `anyio` should include a lower-bound.
|
||||
uv_snapshot!(context.filters(), context.add(&["anyio"]), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
warning: `uv add` is experimental and may change without warning
|
||||
Resolved 4 packages in [TIME]
|
||||
Prepared 4 packages in [TIME]
|
||||
Installed 4 packages in [TIME]
|
||||
+ anyio==4.3.0
|
||||
+ idna==3.6
|
||||
+ project==0.1.0 (from file://[TEMP_DIR]/)
|
||||
+ sniffio==1.3.1
|
||||
"###);
|
||||
|
||||
let pyproject_toml = fs_err::read_to_string(context.temp_dir.join("pyproject.toml"))?;
|
||||
|
||||
insta::with_settings!({
|
||||
filters => context.filters(),
|
||||
}, {
|
||||
assert_snapshot!(
|
||||
pyproject_toml, @r###"
|
||||
[project]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"anyio>=4.3.0",
|
||||
]
|
||||
"###
|
||||
);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Avoid setting a lower bound when updating existing dependencies.
|
||||
#[test]
|
||||
fn add_lower_bound_existing() -> Result<()> {
|
||||
let context = TestContext::new("3.12");
|
||||
|
||||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||||
pyproject_toml.write_str(indoc! {r#"
|
||||
[project]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = ["anyio"]
|
||||
"#})?;
|
||||
|
||||
// Adding `anyio` should _not_ set a lower-bound, since it's already present (even if
|
||||
// unconstrained).
|
||||
uv_snapshot!(context.filters(), context.add(&["anyio"]), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
warning: `uv add` is experimental and may change without warning
|
||||
Resolved 4 packages in [TIME]
|
||||
Prepared 4 packages in [TIME]
|
||||
Installed 4 packages in [TIME]
|
||||
+ anyio==4.3.0
|
||||
+ idna==3.6
|
||||
+ project==0.1.0 (from file://[TEMP_DIR]/)
|
||||
+ sniffio==1.3.1
|
||||
"###);
|
||||
|
||||
let pyproject_toml = fs_err::read_to_string(context.temp_dir.join("pyproject.toml"))?;
|
||||
|
||||
insta::with_settings!({
|
||||
filters => context.filters(),
|
||||
}, {
|
||||
assert_snapshot!(
|
||||
pyproject_toml, @r###"
|
||||
[project]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"anyio",
|
||||
]
|
||||
"###
|
||||
);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Avoid setting a lower bound with `--raw-sources`.
|
||||
#[test]
|
||||
fn add_lower_bound_raw() -> Result<()> {
|
||||
let context = TestContext::new("3.12");
|
||||
|
||||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||||
pyproject_toml.write_str(indoc! {r#"
|
||||
[project]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = ["anyio"]
|
||||
"#})?;
|
||||
|
||||
// Adding `anyio` should _not_ set a lower-bound when using `--raw-sources`.
|
||||
uv_snapshot!(context.filters(), context.add(&["anyio"]).arg("--raw-sources"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
warning: `uv add` is experimental and may change without warning
|
||||
Resolved 4 packages in [TIME]
|
||||
Prepared 4 packages in [TIME]
|
||||
Installed 4 packages in [TIME]
|
||||
+ anyio==4.3.0
|
||||
+ idna==3.6
|
||||
+ project==0.1.0 (from file://[TEMP_DIR]/)
|
||||
+ sniffio==1.3.1
|
||||
"###);
|
||||
|
||||
let pyproject_toml = fs_err::read_to_string(context.temp_dir.join("pyproject.toml"))?;
|
||||
|
||||
insta::with_settings!({
|
||||
filters => context.filters(),
|
||||
}, {
|
||||
assert_snapshot!(
|
||||
pyproject_toml, @r###"
|
||||
[project]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"anyio",
|
||||
]
|
||||
"###
|
||||
);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set a lower bound when adding unconstrained dev dependencies.
|
||||
#[test]
|
||||
fn add_lower_bound_dev() -> Result<()> {
|
||||
let context = TestContext::new("3.12");
|
||||
|
||||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||||
pyproject_toml.write_str(indoc! {r#"
|
||||
[project]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = []
|
||||
"#})?;
|
||||
|
||||
// Adding `anyio` should include a lower-bound.
|
||||
uv_snapshot!(context.filters(), context.add(&["anyio"]).arg("--dev"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
warning: `uv add` is experimental and may change without warning
|
||||
Resolved 4 packages in [TIME]
|
||||
Prepared 4 packages in [TIME]
|
||||
Installed 4 packages in [TIME]
|
||||
+ anyio==4.3.0
|
||||
+ idna==3.6
|
||||
+ project==0.1.0 (from file://[TEMP_DIR]/)
|
||||
+ sniffio==1.3.1
|
||||
"###);
|
||||
|
||||
let pyproject_toml = fs_err::read_to_string(context.temp_dir.join("pyproject.toml"))?;
|
||||
|
||||
insta::with_settings!({
|
||||
filters => context.filters(),
|
||||
}, {
|
||||
assert_snapshot!(
|
||||
pyproject_toml, @r###"
|
||||
[project]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = []
|
||||
|
||||
[tool.uv]
|
||||
dev-dependencies = [
|
||||
"anyio>=4.3.0",
|
||||
]
|
||||
"###
|
||||
);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set a lower bound when adding unconstrained dev dependencies.
|
||||
#[test]
|
||||
fn add_lower_bound_optional() -> Result<()> {
|
||||
let context = TestContext::new("3.12");
|
||||
|
||||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||||
pyproject_toml.write_str(indoc! {r#"
|
||||
[project]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = []
|
||||
"#})?;
|
||||
|
||||
// Adding `anyio` should include a lower-bound.
|
||||
uv_snapshot!(context.filters(), context.add(&["anyio"]).arg("--optional=io"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
warning: `uv add` is experimental and may change without warning
|
||||
Resolved 4 packages in [TIME]
|
||||
Prepared 4 packages in [TIME]
|
||||
Installed 4 packages in [TIME]
|
||||
+ anyio==4.3.0
|
||||
+ idna==3.6
|
||||
+ project==0.1.0 (from file://[TEMP_DIR]/)
|
||||
+ sniffio==1.3.1
|
||||
"###);
|
||||
|
||||
let pyproject_toml = fs_err::read_to_string(context.temp_dir.join("pyproject.toml"))?;
|
||||
|
||||
insta::with_settings!({
|
||||
filters => context.filters(),
|
||||
}, {
|
||||
assert_snapshot!(
|
||||
pyproject_toml, @r###"
|
||||
[project]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = []
|
||||
|
||||
[project.optional-dependencies]
|
||||
io = [
|
||||
"anyio>=4.3.0",
|
||||
]
|
||||
"###
|
||||
);
|
||||
});
|
||||
|
||||
let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock"))?;
|
||||
|
||||
insta::with_settings!({
|
||||
filters => context.filters(),
|
||||
}, {
|
||||
assert_snapshot!(
|
||||
lock, @r###"
|
||||
version = 1
|
||||
requires-python = ">=3.12"
|
||||
exclude-newer = "2024-03-25 00:00:00 UTC"
|
||||
|
||||
[[distribution]]
|
||||
name = "anyio"
|
||||
version = "4.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "idna" },
|
||||
{ name = "sniffio" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/db/4d/3970183622f0330d3c23d9b8a5f52e365e50381fd484d08e3285104333d3/anyio-4.3.0.tar.gz", hash = "sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6", size = 159642 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/14/fd/2f20c40b45e4fb4324834aea24bd4afdf1143390242c0b33774da0e2e34f/anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8", size = 85584 },
|
||||
]
|
||||
|
||||
[[distribution]]
|
||||
name = "idna"
|
||||
version = "3.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/bf/3f/ea4b9117521a1e9c50344b909be7886dd00a519552724809bb1f486986c2/idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", size = 175426 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/e7/a82b05cf63a603df6e68d59ae6a68bf5064484a0718ea5033660af4b54a9/idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f", size = 61567 },
|
||||
]
|
||||
|
||||
[[distribution]]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
source = { editable = "." }
|
||||
|
||||
[distribution.optional-dependencies]
|
||||
io = [
|
||||
{ name = "anyio" },
|
||||
]
|
||||
|
||||
[[distribution]]
|
||||
name = "sniffio"
|
||||
version = "1.3.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 },
|
||||
]
|
||||
"###
|
||||
);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue