mirror of
https://github.com/astral-sh/uv.git
synced 2025-07-07 21:35:00 +00:00
Initial implementation of uv add
and uv remove
(#4193)
## Summary Basic implementation of `uv add` and `uv remove` that supports writing PEP508 requirements to `project.dependencies`. First step for https://github.com/astral-sh/uv/issues/3959 and https://github.com/astral-sh/uv/issues/3960.
This commit is contained in:
parent
60431ce78c
commit
eefa9e62fc
16 changed files with 1278 additions and 17 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -4681,6 +4681,7 @@ dependencies = [
|
|||
"tokio",
|
||||
"tokio-util",
|
||||
"toml",
|
||||
"toml_edit",
|
||||
"tracing",
|
||||
"url",
|
||||
"uv-cache",
|
||||
|
|
|
@ -48,6 +48,7 @@ thiserror = { workspace = true }
|
|||
tokio = { workspace = true }
|
||||
tokio-util = { workspace = true, features = ["compat"] }
|
||||
toml = { workspace = true }
|
||||
toml_edit = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
url = { workspace = true }
|
||||
zip = { workspace = true }
|
||||
|
|
|
@ -14,6 +14,7 @@ mod index;
|
|||
mod locks;
|
||||
mod metadata;
|
||||
pub mod pyproject;
|
||||
pub mod pyproject_mut;
|
||||
mod reporter;
|
||||
mod source;
|
||||
mod workspace;
|
||||
|
|
|
@ -156,7 +156,7 @@ mod test {
|
|||
use crate::{ProjectWorkspace, RequiresDist};
|
||||
|
||||
async fn requires_dist_from_pyproject_toml(contents: &str) -> anyhow::Result<RequiresDist> {
|
||||
let pyproject_toml: PyProjectToml = toml::from_str(contents)?;
|
||||
let pyproject_toml = PyProjectToml::from_string(contents.to_string())?;
|
||||
let path = Path::new("pyproject.toml");
|
||||
let project_workspace = ProjectWorkspace::from_project(
|
||||
path,
|
||||
|
|
|
@ -18,15 +18,35 @@ use pypi_types::VerbatimParsedUrl;
|
|||
use uv_normalize::{ExtraName, PackageName};
|
||||
|
||||
/// A `pyproject.toml` as specified in PEP 517.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct PyProjectToml {
|
||||
/// PEP 621-compliant project metadata.
|
||||
pub project: Option<Project>,
|
||||
/// Tool-specific metadata.
|
||||
pub tool: Option<Tool>,
|
||||
/// The raw unserialized document.
|
||||
#[serde(skip)]
|
||||
pub(crate) raw: String,
|
||||
}
|
||||
|
||||
impl PyProjectToml {
|
||||
/// Parse a `PyProjectToml` from a raw TOML string.
|
||||
pub fn from_string(raw: String) -> Result<Self, toml::de::Error> {
|
||||
let pyproject = toml::from_str(&raw)?;
|
||||
Ok(PyProjectToml { raw, ..pyproject })
|
||||
}
|
||||
}
|
||||
|
||||
// Ignore raw document in comparison.
|
||||
impl PartialEq for PyProjectToml {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.project.eq(&other.project) && self.tool.eq(&other.tool)
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for PyProjectToml {}
|
||||
|
||||
/// PEP 621 project metadata (`project`).
|
||||
///
|
||||
/// See <https://packaging.python.org/en/latest/specifications/pyproject-toml>.
|
||||
|
|
161
crates/uv-distribution/src/pyproject_mut.rs
Normal file
161
crates/uv-distribution/src/pyproject_mut.rs
Normal file
|
@ -0,0 +1,161 @@
|
|||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
|
||||
use thiserror::Error;
|
||||
use toml_edit::{Array, DocumentMut, Item, RawString, TomlError, Value};
|
||||
|
||||
use pep508_rs::{PackageName, Requirement};
|
||||
use pypi_types::VerbatimParsedUrl;
|
||||
|
||||
use crate::pyproject::PyProjectToml;
|
||||
|
||||
/// Raw and mutable representation of a `pyproject.toml`.
|
||||
///
|
||||
/// This is useful for operations that require editing an existing `pyproject.toml` while
|
||||
/// preserving comments and other structure, such as `uv add` and `uv remove`.
|
||||
pub struct PyProjectTomlMut {
|
||||
doc: DocumentMut,
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error("Failed to parse `pyproject.toml`")]
|
||||
Parse(#[from] Box<TomlError>),
|
||||
#[error("Dependencies in `pyproject.toml` are malformed")]
|
||||
MalformedDependencies,
|
||||
}
|
||||
|
||||
impl PyProjectTomlMut {
|
||||
/// Initialize a `PyProjectTomlMut` from a `PyProjectToml`.
|
||||
pub fn from_toml(pyproject: &PyProjectToml) -> Result<Self, Error> {
|
||||
Ok(Self {
|
||||
doc: pyproject.raw.parse().map_err(Box::new)?,
|
||||
})
|
||||
}
|
||||
|
||||
/// Adds a dependency.
|
||||
pub fn add_dependency(&mut self, req: &Requirement) -> Result<(), Error> {
|
||||
let deps = &mut self.doc["project"]["dependencies"];
|
||||
if deps.is_none() {
|
||||
*deps = Item::Value(Value::Array(Array::new()));
|
||||
}
|
||||
let deps = deps.as_array_mut().ok_or(Error::MalformedDependencies)?;
|
||||
|
||||
// Try to find matching dependencies.
|
||||
let mut to_replace = Vec::new();
|
||||
for (i, dep) in deps.iter().enumerate() {
|
||||
if dep
|
||||
.as_str()
|
||||
.and_then(try_parse_requirement)
|
||||
.filter(|dep| dep.name == req.name)
|
||||
.is_some()
|
||||
{
|
||||
to_replace.push(i);
|
||||
}
|
||||
}
|
||||
|
||||
if to_replace.is_empty() {
|
||||
deps.push(req.to_string());
|
||||
} else {
|
||||
// Replace the first occurrence of the dependency and remove the rest.
|
||||
deps.replace(to_replace[0], req.to_string());
|
||||
for &i in to_replace[1..].iter().rev() {
|
||||
deps.remove(i);
|
||||
}
|
||||
}
|
||||
|
||||
reformat_array_multiline(deps);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Removes all occurrences of dependencies with the given name.
|
||||
pub fn remove_dependency(&mut self, req: &PackageName) -> Result<Vec<Requirement>, Error> {
|
||||
let deps = &mut self.doc["project"]["dependencies"];
|
||||
if deps.is_none() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let deps = deps.as_array_mut().ok_or(Error::MalformedDependencies)?;
|
||||
|
||||
// Try to find matching dependencies.
|
||||
let mut to_remove = Vec::new();
|
||||
for (i, dep) in deps.iter().enumerate() {
|
||||
if dep
|
||||
.as_str()
|
||||
.and_then(try_parse_requirement)
|
||||
.filter(|dep| dep.name == *req)
|
||||
.is_some()
|
||||
{
|
||||
to_remove.push(i);
|
||||
}
|
||||
}
|
||||
|
||||
let removed = to_remove
|
||||
.into_iter()
|
||||
.rev() // Reverse to preserve indices as we remove them.
|
||||
.filter_map(|i| {
|
||||
deps.remove(i)
|
||||
.as_str()
|
||||
.and_then(|req| Requirement::from_str(req).ok())
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if !removed.is_empty() {
|
||||
reformat_array_multiline(deps);
|
||||
}
|
||||
|
||||
Ok(removed)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for PyProjectTomlMut {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
self.doc.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
fn try_parse_requirement(req: &str) -> Option<Requirement<VerbatimParsedUrl>> {
|
||||
Requirement::from_str(req).ok()
|
||||
}
|
||||
|
||||
/// Reformats a TOML array to multi line while trying to preserve all comments
|
||||
/// and move them around. This also formats the array to have a trailing comma.
|
||||
fn reformat_array_multiline(deps: &mut Array) {
|
||||
fn find_comments(s: Option<&RawString>) -> impl Iterator<Item = &str> {
|
||||
s.and_then(|x| x.as_str())
|
||||
.unwrap_or("")
|
||||
.lines()
|
||||
.filter_map(|line| {
|
||||
let line = line.trim();
|
||||
line.starts_with('#').then_some(line)
|
||||
})
|
||||
}
|
||||
|
||||
for item in deps.iter_mut() {
|
||||
let decor = item.decor_mut();
|
||||
let mut prefix = String::new();
|
||||
for comment in find_comments(decor.prefix()).chain(find_comments(decor.suffix())) {
|
||||
prefix.push_str("\n ");
|
||||
prefix.push_str(comment);
|
||||
}
|
||||
prefix.push_str("\n ");
|
||||
decor.set_prefix(prefix);
|
||||
decor.set_suffix("");
|
||||
}
|
||||
|
||||
deps.set_trailing(&{
|
||||
let mut comments = find_comments(Some(deps.trailing())).peekable();
|
||||
let mut rv = String::new();
|
||||
if comments.peek().is_some() {
|
||||
for comment in comments {
|
||||
rv.push_str("\n ");
|
||||
rv.push_str(comment);
|
||||
}
|
||||
}
|
||||
if !rv.is_empty() || !deps.is_empty() {
|
||||
rv.push('\n');
|
||||
}
|
||||
rv
|
||||
});
|
||||
deps.set_trailing_comma(true);
|
||||
}
|
|
@ -75,7 +75,7 @@ impl Workspace {
|
|||
|
||||
let pyproject_path = project_root.join("pyproject.toml");
|
||||
let contents = fs_err::tokio::read_to_string(&pyproject_path).await?;
|
||||
let pyproject_toml: PyProjectToml = toml::from_str(&contents)
|
||||
let pyproject_toml = PyProjectToml::from_string(contents)
|
||||
.map_err(|err| WorkspaceError::Toml(pyproject_path.clone(), Box::new(err)))?;
|
||||
|
||||
let project_path = absolutize_path(project_root)
|
||||
|
@ -242,7 +242,7 @@ impl Workspace {
|
|||
if let Some(project) = &workspace_pyproject_toml.project {
|
||||
let pyproject_path = workspace_root.join("pyproject.toml");
|
||||
let contents = fs_err::read_to_string(&pyproject_path)?;
|
||||
let pyproject_toml = toml::from_str(&contents)
|
||||
let pyproject_toml = PyProjectToml::from_string(contents)
|
||||
.map_err(|err| WorkspaceError::Toml(pyproject_path, Box::new(err)))?;
|
||||
|
||||
debug!(
|
||||
|
@ -297,7 +297,7 @@ impl Workspace {
|
|||
// Read the member `pyproject.toml`.
|
||||
let pyproject_path = member_root.join("pyproject.toml");
|
||||
let contents = fs_err::tokio::read_to_string(&pyproject_path).await?;
|
||||
let pyproject_toml: PyProjectToml = toml::from_str(&contents)
|
||||
let pyproject_toml = PyProjectToml::from_string(contents)
|
||||
.map_err(|err| WorkspaceError::Toml(pyproject_path, Box::new(err)))?;
|
||||
|
||||
// Extract the package name.
|
||||
|
@ -490,7 +490,7 @@ impl ProjectWorkspace {
|
|||
// Read the current `pyproject.toml`.
|
||||
let pyproject_path = project_root.join("pyproject.toml");
|
||||
let contents = fs_err::tokio::read_to_string(&pyproject_path).await?;
|
||||
let pyproject_toml: PyProjectToml = toml::from_str(&contents)
|
||||
let pyproject_toml = PyProjectToml::from_string(contents)
|
||||
.map_err(|err| WorkspaceError::Toml(pyproject_path.clone(), Box::new(err)))?;
|
||||
|
||||
// It must have a `[project]` table.
|
||||
|
@ -514,7 +514,7 @@ impl ProjectWorkspace {
|
|||
// No `pyproject.toml`, but there may still be a `setup.py` or `setup.cfg`.
|
||||
return Ok(None);
|
||||
};
|
||||
let pyproject_toml: PyProjectToml = toml::from_str(&contents)
|
||||
let pyproject_toml = PyProjectToml::from_string(contents)
|
||||
.map_err(|err| WorkspaceError::Toml(pyproject_path.clone(), Box::new(err)))?;
|
||||
|
||||
// Extract the `[project]` metadata.
|
||||
|
@ -656,7 +656,7 @@ async fn find_workspace(
|
|||
|
||||
// Read the `pyproject.toml`.
|
||||
let contents = fs_err::tokio::read_to_string(&pyproject_path).await?;
|
||||
let pyproject_toml: PyProjectToml = toml::from_str(&contents)
|
||||
let pyproject_toml = PyProjectToml::from_string(contents)
|
||||
.map_err(|err| WorkspaceError::Toml(pyproject_path.clone(), Box::new(err)))?;
|
||||
|
||||
return if let Some(workspace) = pyproject_toml
|
||||
|
|
|
@ -153,6 +153,12 @@ pub(crate) enum Commands {
|
|||
/// Resolve the project requirements into a lockfile.
|
||||
#[clap(hide = true)]
|
||||
Lock(LockArgs),
|
||||
/// Add one or more packages to the project requirements.
|
||||
#[clap(hide = true)]
|
||||
Add(AddArgs),
|
||||
/// Remove one or more packages from the project requirements.
|
||||
#[clap(hide = true)]
|
||||
Remove(RemoveArgs),
|
||||
/// Display uv's version
|
||||
Version {
|
||||
#[arg(long, value_enum, default_value = "text")]
|
||||
|
@ -1922,16 +1928,48 @@ pub(crate) struct LockArgs {
|
|||
|
||||
#[derive(Args)]
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
struct AddArgs {
|
||||
/// The name of the package to add (e.g., `Django==4.2.6`).
|
||||
name: String,
|
||||
pub(crate) struct AddArgs {
|
||||
/// The packages to remove, as PEP 508 requirements (e.g., `flask==2.2.3`).
|
||||
#[arg(required = true)]
|
||||
pub(crate) requirements: Vec<String>,
|
||||
|
||||
/// The Python interpreter into which packages should be installed.
|
||||
///
|
||||
/// By default, `uv` installs into the virtual environment in the current working directory or
|
||||
/// any parent directory. The `--python` option allows you to specify a different interpreter,
|
||||
/// which is intended for use in continuous integration (CI) environments or other automated
|
||||
/// workflows.
|
||||
///
|
||||
/// Supported formats:
|
||||
/// - `3.10` looks for an installed Python 3.10 using `py --list-paths` on Windows, or
|
||||
/// `python3.10` on Linux and macOS.
|
||||
/// - `python3.10` or `python.exe` looks for a binary with the given name in `PATH`.
|
||||
/// - `/home/ferris/.local/bin/python3.10` uses the exact Python at the given path.
|
||||
#[arg(long, short, env = "UV_PYTHON", verbatim_doc_comment)]
|
||||
pub(crate) python: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
struct RemoveArgs {
|
||||
/// The name of the package to remove (e.g., `Django`).
|
||||
name: PackageName,
|
||||
pub(crate) struct RemoveArgs {
|
||||
/// The names of the packages to remove (e.g., `flask`).
|
||||
#[arg(required = true)]
|
||||
pub(crate) requirements: Vec<PackageName>,
|
||||
|
||||
/// The Python interpreter into which packages should be installed.
|
||||
///
|
||||
/// By default, `uv` installs into the virtual environment in the current working directory or
|
||||
/// any parent directory. The `--python` option allows you to specify a different interpreter,
|
||||
/// which is intended for use in continuous integration (CI) environments or other automated
|
||||
/// workflows.
|
||||
///
|
||||
/// Supported formats:
|
||||
/// - `3.10` looks for an installed Python 3.10 using `py --list-paths` on Windows, or
|
||||
/// `python3.10` on Linux and macOS.
|
||||
/// - `python3.10` or `python.exe` looks for a binary with the given name in `PATH`.
|
||||
/// - `/home/ferris/.local/bin/python3.10` uses the exact Python at the given path.
|
||||
#[arg(long, short, env = "UV_PYTHON", verbatim_doc_comment)]
|
||||
pub(crate) python: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
|
|
|
@ -16,7 +16,9 @@ pub(crate) use pip::list::pip_list;
|
|||
pub(crate) use pip::show::pip_show;
|
||||
pub(crate) use pip::sync::pip_sync;
|
||||
pub(crate) use pip::uninstall::pip_uninstall;
|
||||
pub(crate) use project::add::add;
|
||||
pub(crate) use project::lock::lock;
|
||||
pub(crate) use project::remove::remove;
|
||||
pub(crate) use project::run::run;
|
||||
pub(crate) use project::sync::sync;
|
||||
#[cfg(feature = "self-update")]
|
||||
|
|
97
crates/uv/src/commands/project/add.rs
Normal file
97
crates/uv/src/commands/project/add.rs
Normal file
|
@ -0,0 +1,97 @@
|
|||
use anyhow::Result;
|
||||
use std::str::FromStr;
|
||||
use uv_distribution::pyproject_mut::PyProjectTomlMut;
|
||||
|
||||
use distribution_types::IndexLocations;
|
||||
use pep508_rs::Requirement;
|
||||
use uv_cache::Cache;
|
||||
use uv_configuration::{ExtrasSpecification, PreviewMode, Upgrade};
|
||||
use uv_distribution::ProjectWorkspace;
|
||||
use uv_warnings::warn_user;
|
||||
|
||||
use crate::commands::{project, ExitStatus};
|
||||
use crate::printer::Printer;
|
||||
|
||||
/// Add one or more packages to the project requirements.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) async fn add(
|
||||
requirements: Vec<String>,
|
||||
python: Option<String>,
|
||||
preview: PreviewMode,
|
||||
cache: &Cache,
|
||||
printer: Printer,
|
||||
) -> Result<ExitStatus> {
|
||||
if preview.is_disabled() {
|
||||
warn_user!("`uv add` is experimental and may change without warning.");
|
||||
}
|
||||
|
||||
// Find the project requirements.
|
||||
let project = ProjectWorkspace::discover(&std::env::current_dir()?, None).await?;
|
||||
|
||||
let mut pyproject = PyProjectTomlMut::from_toml(project.current_project().pyproject_toml())?;
|
||||
for req in requirements {
|
||||
let req = Requirement::from_str(&req)?;
|
||||
pyproject.add_dependency(&req)?;
|
||||
}
|
||||
|
||||
// Save the modified `pyproject.toml`.
|
||||
fs_err::write(
|
||||
project.current_project().root().join("pyproject.toml"),
|
||||
pyproject.to_string(),
|
||||
)?;
|
||||
|
||||
// Discover or create the virtual environment.
|
||||
let venv = project::init_environment(
|
||||
project.workspace(),
|
||||
python.as_deref(),
|
||||
preview,
|
||||
cache,
|
||||
printer,
|
||||
)?;
|
||||
|
||||
let index_locations = IndexLocations::default();
|
||||
let upgrade = Upgrade::default();
|
||||
let exclude_newer = None;
|
||||
|
||||
// Lock and sync the environment.
|
||||
let root_project_name = project
|
||||
.current_project()
|
||||
.pyproject_toml()
|
||||
.project
|
||||
.as_ref()
|
||||
.map(|project| project.name.clone());
|
||||
|
||||
let lock = project::lock::do_lock(
|
||||
root_project_name,
|
||||
project.workspace(),
|
||||
venv.interpreter(),
|
||||
&index_locations,
|
||||
upgrade,
|
||||
exclude_newer,
|
||||
preview,
|
||||
cache,
|
||||
printer,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// 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;
|
||||
let dev = true;
|
||||
|
||||
project::sync::do_sync(
|
||||
project.project_name(),
|
||||
project.workspace().root(),
|
||||
&venv,
|
||||
&lock,
|
||||
&index_locations,
|
||||
extras,
|
||||
dev,
|
||||
preview,
|
||||
cache,
|
||||
printer,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(ExitStatus::Success)
|
||||
}
|
|
@ -28,7 +28,9 @@ use uv_warnings::warn_user;
|
|||
use crate::commands::pip;
|
||||
use crate::printer::Printer;
|
||||
|
||||
pub(crate) mod add;
|
||||
pub(crate) mod lock;
|
||||
pub(crate) mod remove;
|
||||
pub(crate) mod run;
|
||||
pub(crate) mod sync;
|
||||
|
||||
|
|
100
crates/uv/src/commands/project/remove.rs
Normal file
100
crates/uv/src/commands/project/remove.rs
Normal file
|
@ -0,0 +1,100 @@
|
|||
use anyhow::Result;
|
||||
use pep508_rs::PackageName;
|
||||
use uv_distribution::pyproject_mut::PyProjectTomlMut;
|
||||
|
||||
use distribution_types::IndexLocations;
|
||||
use uv_cache::Cache;
|
||||
use uv_configuration::{ExtrasSpecification, PreviewMode, Upgrade};
|
||||
use uv_distribution::ProjectWorkspace;
|
||||
use uv_warnings::warn_user;
|
||||
|
||||
use crate::commands::{project, ExitStatus};
|
||||
use crate::printer::Printer;
|
||||
|
||||
/// Remove one or more packages from the project requirements.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) async fn remove(
|
||||
requirements: Vec<PackageName>,
|
||||
python: Option<String>,
|
||||
preview: PreviewMode,
|
||||
cache: &Cache,
|
||||
printer: Printer,
|
||||
) -> Result<ExitStatus> {
|
||||
if preview.is_disabled() {
|
||||
warn_user!("`uv remove` is experimental and may change without warning.");
|
||||
}
|
||||
|
||||
// Find the project requirements.
|
||||
let project = ProjectWorkspace::discover(&std::env::current_dir()?, None).await?;
|
||||
|
||||
let mut pyproject = PyProjectTomlMut::from_toml(project.current_project().pyproject_toml())?;
|
||||
for req in requirements {
|
||||
if pyproject.remove_dependency(&req)?.is_empty() {
|
||||
anyhow::bail!(
|
||||
"The dependency `{}` could not be found in `dependencies`",
|
||||
req
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Save the modified `pyproject.toml`.
|
||||
fs_err::write(
|
||||
project.current_project().root().join("pyproject.toml"),
|
||||
pyproject.to_string(),
|
||||
)?;
|
||||
|
||||
// Discover or create the virtual environment.
|
||||
let venv = project::init_environment(
|
||||
project.workspace(),
|
||||
python.as_deref(),
|
||||
preview,
|
||||
cache,
|
||||
printer,
|
||||
)?;
|
||||
|
||||
let index_locations = IndexLocations::default();
|
||||
let upgrade = Upgrade::None;
|
||||
let exclude_newer = None;
|
||||
|
||||
// Lock and sync the environment.
|
||||
let root_project_name = project
|
||||
.current_project()
|
||||
.pyproject_toml()
|
||||
.project
|
||||
.as_ref()
|
||||
.map(|project| project.name.clone());
|
||||
|
||||
let lock = project::lock::do_lock(
|
||||
root_project_name,
|
||||
project.workspace(),
|
||||
venv.interpreter(),
|
||||
&index_locations,
|
||||
upgrade,
|
||||
exclude_newer,
|
||||
preview,
|
||||
cache,
|
||||
printer,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// 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;
|
||||
let dev = true;
|
||||
|
||||
project::sync::do_sync(
|
||||
project.project_name(),
|
||||
project.workspace().root(),
|
||||
&venv,
|
||||
&lock,
|
||||
&index_locations,
|
||||
extras,
|
||||
dev,
|
||||
preview,
|
||||
cache,
|
||||
printer,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(ExitStatus::Success)
|
||||
}
|
|
@ -637,6 +637,38 @@ async fn run() -> Result<ExitStatus> {
|
|||
)
|
||||
.await
|
||||
}
|
||||
Commands::Add(args) => {
|
||||
// Resolve the settings from the command-line arguments and workspace configuration.
|
||||
let args = settings::AddSettings::resolve(args, workspace);
|
||||
|
||||
// Initialize the cache.
|
||||
let cache = cache.init()?;
|
||||
|
||||
commands::add(
|
||||
args.requirements,
|
||||
args.python,
|
||||
globals.preview,
|
||||
&cache,
|
||||
printer,
|
||||
)
|
||||
.await
|
||||
}
|
||||
Commands::Remove(args) => {
|
||||
// Resolve the settings from the command-line arguments and workspace configuration.
|
||||
let args = settings::RemoveSettings::resolve(args, workspace);
|
||||
|
||||
// Initialize the cache.
|
||||
let cache = cache.init()?;
|
||||
|
||||
commands::remove(
|
||||
args.requirements,
|
||||
args.python,
|
||||
globals.preview,
|
||||
&cache,
|
||||
printer,
|
||||
)
|
||||
.await
|
||||
}
|
||||
#[cfg(feature = "self-update")]
|
||||
Commands::Self_(SelfNamespace {
|
||||
command: SelfCommand::Update,
|
||||
|
|
|
@ -21,9 +21,9 @@ use uv_toolchain::{Prefix, PythonVersion, Target};
|
|||
use uv_workspace::{Combine, PipOptions, Workspace};
|
||||
|
||||
use crate::cli::{
|
||||
ColorChoice, GlobalArgs, LockArgs, Maybe, PipCheckArgs, PipCompileArgs, PipFreezeArgs,
|
||||
PipInstallArgs, PipListArgs, PipShowArgs, PipSyncArgs, PipUninstallArgs, RunArgs, SyncArgs,
|
||||
ToolRunArgs, ToolchainInstallArgs, ToolchainListArgs, VenvArgs,
|
||||
AddArgs, ColorChoice, GlobalArgs, LockArgs, Maybe, PipCheckArgs, PipCompileArgs, PipFreezeArgs,
|
||||
PipInstallArgs, PipListArgs, PipShowArgs, PipSyncArgs, PipUninstallArgs, RemoveArgs, RunArgs,
|
||||
SyncArgs, ToolRunArgs, ToolchainInstallArgs, ToolchainListArgs, VenvArgs,
|
||||
};
|
||||
use crate::commands::ListFormat;
|
||||
|
||||
|
@ -387,6 +387,54 @@ impl LockSettings {
|
|||
}
|
||||
}
|
||||
|
||||
/// The resolved settings to use for a `add` invocation.
|
||||
#[allow(clippy::struct_excessive_bools, dead_code)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct AddSettings {
|
||||
pub(crate) requirements: Vec<String>,
|
||||
pub(crate) python: Option<String>,
|
||||
}
|
||||
|
||||
impl AddSettings {
|
||||
/// Resolve the [`AddSettings`] from the CLI and workspace configuration.
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
pub(crate) fn resolve(args: AddArgs, _workspace: Option<Workspace>) -> Self {
|
||||
let AddArgs {
|
||||
requirements,
|
||||
python,
|
||||
} = args;
|
||||
|
||||
Self {
|
||||
requirements,
|
||||
python,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The resolved settings to use for a `remove` invocation.
|
||||
#[allow(clippy::struct_excessive_bools, dead_code)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct RemoveSettings {
|
||||
pub(crate) requirements: Vec<PackageName>,
|
||||
pub(crate) python: Option<String>,
|
||||
}
|
||||
|
||||
impl RemoveSettings {
|
||||
/// Resolve the [`RemoveSettings`] from the CLI and workspace configuration.
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
pub(crate) fn resolve(args: RemoveArgs, _workspace: Option<Workspace>) -> Self {
|
||||
let RemoveArgs {
|
||||
requirements,
|
||||
python,
|
||||
} = args;
|
||||
|
||||
Self {
|
||||
requirements,
|
||||
python,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The resolved settings to use for a `pip compile` invocation.
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
#[derive(Debug, Clone)]
|
||||
|
|
|
@ -317,6 +317,48 @@ impl TestContext {
|
|||
command
|
||||
}
|
||||
|
||||
/// Create a `uv add` command for the given requirements.
|
||||
pub fn add(&self, reqs: &[&str]) -> std::process::Command {
|
||||
let mut command = std::process::Command::new(get_bin());
|
||||
command
|
||||
.arg("add")
|
||||
.args(reqs)
|
||||
.arg("--cache-dir")
|
||||
.arg(self.cache_dir.path())
|
||||
.env("VIRTUAL_ENV", self.venv.as_os_str())
|
||||
.env("UV_NO_WRAP", "1")
|
||||
.current_dir(&self.temp_dir);
|
||||
|
||||
if cfg!(all(windows, debug_assertions)) {
|
||||
// TODO(konstin): Reduce stack usage in debug mode enough that the tests pass with the
|
||||
// default windows stack of 1MB
|
||||
command.env("UV_STACK_SIZE", (4 * 1024 * 1024).to_string());
|
||||
}
|
||||
|
||||
command
|
||||
}
|
||||
|
||||
/// Create a `uv remove` command for the given requirements.
|
||||
pub fn remove(&self, reqs: &[&str]) -> std::process::Command {
|
||||
let mut command = std::process::Command::new(get_bin());
|
||||
command
|
||||
.arg("remove")
|
||||
.args(reqs)
|
||||
.arg("--cache-dir")
|
||||
.arg(self.cache_dir.path())
|
||||
.env("VIRTUAL_ENV", self.venv.as_os_str())
|
||||
.env("UV_NO_WRAP", "1")
|
||||
.current_dir(&self.temp_dir);
|
||||
|
||||
if cfg!(all(windows, debug_assertions)) {
|
||||
// TODO(konstin): Reduce stack usage in debug mode enough that the tests pass with the
|
||||
// default windows stack of 1MB
|
||||
command.env("UV_STACK_SIZE", (4 * 1024 * 1024).to_string());
|
||||
}
|
||||
|
||||
command
|
||||
}
|
||||
|
||||
pub fn interpreter(&self) -> PathBuf {
|
||||
venv_to_interpreter(&self.venv)
|
||||
}
|
||||
|
|
716
crates/uv/tests/edit.rs
Normal file
716
crates/uv/tests/edit.rs
Normal file
|
@ -0,0 +1,716 @@
|
|||
#![cfg(all(feature = "python", feature = "pypi"))]
|
||||
|
||||
use anyhow::Result;
|
||||
use assert_fs::prelude::*;
|
||||
use indoc::indoc;
|
||||
use insta::assert_snapshot;
|
||||
|
||||
use common::{uv_snapshot, TestContext};
|
||||
|
||||
mod common;
|
||||
|
||||
/// Add a PyPI requirement.
|
||||
#[test]
|
||||
fn add_registry() -> 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 = []
|
||||
"#})?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.add(&["anyio==3.7.0"]), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
warning: `uv add` is experimental and may change without warning.
|
||||
Resolved 6 packages in [TIME]
|
||||
Downloaded 4 packages in [TIME]
|
||||
Installed 4 packages in [TIME]
|
||||
+ anyio==3.7.0
|
||||
+ idna==3.7
|
||||
+ 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==3.7.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"
|
||||
|
||||
[[distribution]]
|
||||
name = "anyio"
|
||||
version = "3.7.0"
|
||||
source = "registry+https://pypi.org/simple"
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c6/b3/fefbf7e78ab3b805dec67d698dc18dd505af7a18a8dd08868c9b4fa736b5/anyio-3.7.0.tar.gz", hash = "sha256:275d9973793619a5374e1c89a4f4ad3f4b0a5510a2b5b939444bee8f4c4d37ce", size = 142737 }
|
||||
wheels = [{ url = "https://files.pythonhosted.org/packages/68/fe/7ce1926952c8a403b35029e194555558514b365ad77d75125f521a2bec62/anyio-3.7.0-py3-none-any.whl", hash = "sha256:eddca883c4175f14df8aedce21054bfca3adb70ffe76a9f607aef9d7fa2ea7f0", size = 80873 }]
|
||||
|
||||
[[distribution.dependencies]]
|
||||
name = "exceptiongroup"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://pypi.org/simple"
|
||||
marker = "python_version < '3.11'"
|
||||
|
||||
[[distribution.dependencies]]
|
||||
name = "idna"
|
||||
version = "3.7"
|
||||
source = "registry+https://pypi.org/simple"
|
||||
|
||||
[[distribution.dependencies]]
|
||||
name = "sniffio"
|
||||
version = "1.3.1"
|
||||
source = "registry+https://pypi.org/simple"
|
||||
|
||||
[[distribution.dependencies]]
|
||||
name = "typing-extensions"
|
||||
version = "4.12.2"
|
||||
source = "registry+https://pypi.org/simple"
|
||||
marker = "python_version < '3.8'"
|
||||
|
||||
[[distribution]]
|
||||
name = "exceptiongroup"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://pypi.org/simple"
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a0/65/d66b7fbaef021b3c954b3bbb196d21d8a4b97918ea524f82cfae474215af/exceptiongroup-1.2.1.tar.gz", hash = "sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16", size = 28717 }
|
||||
wheels = [{ url = "https://files.pythonhosted.org/packages/01/90/79fe92dd413a9cab314ef5c591b5aa9b9ba787ae4cadab75055b0ae00b33/exceptiongroup-1.2.1-py3-none-any.whl", hash = "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad", size = 16458 }]
|
||||
|
||||
[[distribution]]
|
||||
name = "idna"
|
||||
version = "3.7"
|
||||
source = "registry+https://pypi.org/simple"
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/21/ed/f86a79a07470cb07819390452f178b3bef1d375f2ec021ecfc709fc7cf07/idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc", size = 189575 }
|
||||
wheels = [{ url = "https://files.pythonhosted.org/packages/e5/3e/741d8c82801c347547f8a2a06aa57dbb1992be9e948df2ea0eda2c8b79e8/idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0", size = 66836 }]
|
||||
|
||||
[[distribution]]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
source = "editable+."
|
||||
sdist = { path = "." }
|
||||
|
||||
[[distribution.dependencies]]
|
||||
name = "anyio"
|
||||
version = "3.7.0"
|
||||
source = "registry+https://pypi.org/simple"
|
||||
|
||||
[[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 }]
|
||||
|
||||
[[distribution]]
|
||||
name = "typing-extensions"
|
||||
version = "4.12.2"
|
||||
source = "registry+https://pypi.org/simple"
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 }
|
||||
wheels = [{ url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }]
|
||||
"###
|
||||
);
|
||||
});
|
||||
|
||||
// Install from the lockfile.
|
||||
uv_snapshot!(context.filters(), context.sync(), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
warning: `uv sync` is experimental and may change without warning.
|
||||
Audited 4 packages in [TIME]
|
||||
"###);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Add a Git requirement.
|
||||
#[test]
|
||||
fn add_git() -> 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==3.7.0"]
|
||||
"#})?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.lock(), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
warning: `uv lock` is experimental and may change without warning.
|
||||
Resolved 6 packages in [TIME]
|
||||
"###);
|
||||
|
||||
uv_snapshot!(context.filters(), context.sync(), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
warning: `uv sync` is experimental and may change without warning.
|
||||
Downloaded 4 packages in [TIME]
|
||||
Installed 4 packages in [TIME]
|
||||
+ anyio==3.7.0
|
||||
+ idna==3.6
|
||||
+ project==0.1.0 (from file://[TEMP_DIR]/)
|
||||
+ sniffio==1.3.1
|
||||
"###);
|
||||
|
||||
uv_snapshot!(context.filters(), context.add(&["uv-public-pypackage @ git+https://github.com/astral-test/uv-public-pypackage@0.0.1"]), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
warning: `uv add` is experimental and may change without warning.
|
||||
Resolved 7 packages in [TIME]
|
||||
Downloaded 2 packages in [TIME]
|
||||
Uninstalled 1 package in [TIME]
|
||||
Installed 2 packages in [TIME]
|
||||
- project==0.1.0 (from file://[TEMP_DIR]/)
|
||||
+ project==0.1.0 (from file://[TEMP_DIR]/)
|
||||
+ uv-public-pypackage==0.1.0 (from git+https://github.com/astral-test/uv-public-pypackage@0dacfd662c64cb4ceb16e6cf65a157a8b715b979?rev=0.0.1#0dacfd662c64cb4ceb16e6cf65a157a8b715b979)
|
||||
"###);
|
||||
|
||||
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==3.7.0",
|
||||
"uv-public-pypackage @ git+https://github.com/astral-test/uv-public-pypackage@0.0.1",
|
||||
]
|
||||
"###
|
||||
);
|
||||
});
|
||||
|
||||
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"
|
||||
|
||||
[[distribution]]
|
||||
name = "anyio"
|
||||
version = "3.7.0"
|
||||
source = "registry+https://pypi.org/simple"
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c6/b3/fefbf7e78ab3b805dec67d698dc18dd505af7a18a8dd08868c9b4fa736b5/anyio-3.7.0.tar.gz", hash = "sha256:275d9973793619a5374e1c89a4f4ad3f4b0a5510a2b5b939444bee8f4c4d37ce", size = 142737 }
|
||||
wheels = [{ url = "https://files.pythonhosted.org/packages/68/fe/7ce1926952c8a403b35029e194555558514b365ad77d75125f521a2bec62/anyio-3.7.0-py3-none-any.whl", hash = "sha256:eddca883c4175f14df8aedce21054bfca3adb70ffe76a9f607aef9d7fa2ea7f0", size = 80873 }]
|
||||
|
||||
[[distribution.dependencies]]
|
||||
name = "exceptiongroup"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://pypi.org/simple"
|
||||
marker = "python_version < '3.11'"
|
||||
|
||||
[[distribution.dependencies]]
|
||||
name = "idna"
|
||||
version = "3.6"
|
||||
source = "registry+https://pypi.org/simple"
|
||||
|
||||
[[distribution.dependencies]]
|
||||
name = "sniffio"
|
||||
version = "1.3.1"
|
||||
source = "registry+https://pypi.org/simple"
|
||||
|
||||
[[distribution.dependencies]]
|
||||
name = "typing-extensions"
|
||||
version = "4.10.0"
|
||||
source = "registry+https://pypi.org/simple"
|
||||
marker = "python_version < '3.8'"
|
||||
|
||||
[[distribution]]
|
||||
name = "exceptiongroup"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://pypi.org/simple"
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8e/1c/beef724eaf5b01bb44b6338c8c3494eff7cab376fab4904cfbbc3585dc79/exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68", size = 26264 }
|
||||
wheels = [{ url = "https://files.pythonhosted.org/packages/b8/9a/5028fd52db10e600f1c4674441b968cf2ea4959085bfb5b99fb1250e5f68/exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14", size = 16210 }]
|
||||
|
||||
[[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+."
|
||||
sdist = { path = "." }
|
||||
|
||||
[[distribution.dependencies]]
|
||||
name = "anyio"
|
||||
version = "3.7.0"
|
||||
source = "registry+https://pypi.org/simple"
|
||||
|
||||
[[distribution.dependencies]]
|
||||
name = "uv-public-pypackage"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/astral-test/uv-public-pypackage?rev=0.0.1#0dacfd662c64cb4ceb16e6cf65a157a8b715b979"
|
||||
|
||||
[[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 }]
|
||||
|
||||
[[distribution]]
|
||||
name = "typing-extensions"
|
||||
version = "4.10.0"
|
||||
source = "registry+https://pypi.org/simple"
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/16/3a/0d26ce356c7465a19c9ea8814b960f8a36c3b0d07c323176620b7b483e44/typing_extensions-4.10.0.tar.gz", hash = "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb", size = 77558 }
|
||||
wheels = [{ url = "https://files.pythonhosted.org/packages/f9/de/dc04a3ea60b22624b51c703a84bbe0184abcd1d0b9bc8074b5d6b7ab90bb/typing_extensions-4.10.0-py3-none-any.whl", hash = "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475", size = 33926 }]
|
||||
|
||||
[[distribution]]
|
||||
name = "uv-public-pypackage"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/astral-test/uv-public-pypackage?rev=0.0.1#0dacfd662c64cb4ceb16e6cf65a157a8b715b979"
|
||||
sdist = { url = "https://github.com/astral-test/uv-public-pypackage?rev=0.0.1#0dacfd662c64cb4ceb16e6cf65a157a8b715b979" }
|
||||
"###
|
||||
);
|
||||
});
|
||||
|
||||
// Install from the lockfile.
|
||||
uv_snapshot!(context.filters(), context.sync(), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
warning: `uv sync` is experimental and may change without warning.
|
||||
Audited 5 packages in [TIME]
|
||||
"###);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Update a PyPI requirement.
|
||||
#[test]
|
||||
fn update_registry() -> 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 == 3.7.0 ; python_version >= '3.12'",
|
||||
"anyio < 3.7.0 ; python_version < '3.12'",
|
||||
]
|
||||
"#})?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.lock(), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
warning: `uv lock` is experimental and may change without warning.
|
||||
Resolved 10 packages in [TIME]
|
||||
"###);
|
||||
|
||||
uv_snapshot!(context.filters(), context.sync(), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
warning: `uv sync` is experimental and may change without warning.
|
||||
Downloaded 4 packages in [TIME]
|
||||
Installed 4 packages in [TIME]
|
||||
+ anyio==3.7.0
|
||||
+ idna==3.6
|
||||
+ project==0.1.0 (from file://[TEMP_DIR]/)
|
||||
+ sniffio==1.3.1
|
||||
"###);
|
||||
|
||||
uv_snapshot!(context.filters(), context.add(&["anyio==4.3.0"]), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
warning: `uv add` is experimental and may change without warning.
|
||||
Resolved 6 packages in [TIME]
|
||||
Downloaded 2 packages in [TIME]
|
||||
Uninstalled 2 packages in [TIME]
|
||||
Installed 2 packages in [TIME]
|
||||
- anyio==3.7.0
|
||||
+ anyio==4.3.0
|
||||
- project==0.1.0 (from file://[TEMP_DIR]/)
|
||||
+ project==0.1.0 (from file://[TEMP_DIR]/)
|
||||
"###);
|
||||
|
||||
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",
|
||||
]
|
||||
"###
|
||||
);
|
||||
});
|
||||
|
||||
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"
|
||||
|
||||
[[distribution]]
|
||||
name = "anyio"
|
||||
version = "4.3.0"
|
||||
source = "registry+https://pypi.org/simple"
|
||||
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.dependencies]]
|
||||
name = "exceptiongroup"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://pypi.org/simple"
|
||||
marker = "python_version < '3.11'"
|
||||
|
||||
[[distribution.dependencies]]
|
||||
name = "idna"
|
||||
version = "3.6"
|
||||
source = "registry+https://pypi.org/simple"
|
||||
|
||||
[[distribution.dependencies]]
|
||||
name = "sniffio"
|
||||
version = "1.3.1"
|
||||
source = "registry+https://pypi.org/simple"
|
||||
|
||||
[[distribution.dependencies]]
|
||||
name = "typing-extensions"
|
||||
version = "4.10.0"
|
||||
source = "registry+https://pypi.org/simple"
|
||||
marker = "python_version < '3.11'"
|
||||
|
||||
[[distribution]]
|
||||
name = "exceptiongroup"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://pypi.org/simple"
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8e/1c/beef724eaf5b01bb44b6338c8c3494eff7cab376fab4904cfbbc3585dc79/exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68", size = 26264 }
|
||||
wheels = [{ url = "https://files.pythonhosted.org/packages/b8/9a/5028fd52db10e600f1c4674441b968cf2ea4959085bfb5b99fb1250e5f68/exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14", size = 16210 }]
|
||||
|
||||
[[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+."
|
||||
sdist = { path = "." }
|
||||
|
||||
[[distribution.dependencies]]
|
||||
name = "anyio"
|
||||
version = "4.3.0"
|
||||
source = "registry+https://pypi.org/simple"
|
||||
|
||||
[[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 }]
|
||||
|
||||
[[distribution]]
|
||||
name = "typing-extensions"
|
||||
version = "4.10.0"
|
||||
source = "registry+https://pypi.org/simple"
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/16/3a/0d26ce356c7465a19c9ea8814b960f8a36c3b0d07c323176620b7b483e44/typing_extensions-4.10.0.tar.gz", hash = "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb", size = 77558 }
|
||||
wheels = [{ url = "https://files.pythonhosted.org/packages/f9/de/dc04a3ea60b22624b51c703a84bbe0184abcd1d0b9bc8074b5d6b7ab90bb/typing_extensions-4.10.0-py3-none-any.whl", hash = "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475", size = 33926 }]
|
||||
"###
|
||||
);
|
||||
});
|
||||
|
||||
// Install from the lockfile.
|
||||
uv_snapshot!(context.filters(), context.sync(), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
warning: `uv sync` is experimental and may change without warning.
|
||||
Audited 4 packages in [TIME]
|
||||
"###);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Remove a PyPI requirement.
|
||||
#[test]
|
||||
fn remove_registry() -> 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==3.7.0"]
|
||||
"#})?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.lock(), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
warning: `uv lock` is experimental and may change without warning.
|
||||
Resolved 6 packages in [TIME]
|
||||
"###);
|
||||
|
||||
uv_snapshot!(context.filters(), context.sync(), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
warning: `uv sync` is experimental and may change without warning.
|
||||
Downloaded 4 packages in [TIME]
|
||||
Installed 4 packages in [TIME]
|
||||
+ anyio==3.7.0
|
||||
+ idna==3.6
|
||||
+ project==0.1.0 (from file://[TEMP_DIR]/)
|
||||
+ sniffio==1.3.1
|
||||
"###);
|
||||
|
||||
uv_snapshot!(context.filters(), context.remove(&["anyio"]), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
warning: `uv remove` is experimental and may change without warning.
|
||||
Resolved 1 package in [TIME]
|
||||
Downloaded 1 package in [TIME]
|
||||
Uninstalled 1 package in [TIME]
|
||||
Installed 1 package in [TIME]
|
||||
- project==0.1.0 (from file://[TEMP_DIR]/)
|
||||
+ project==0.1.0 (from file://[TEMP_DIR]/)
|
||||
"###);
|
||||
|
||||
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 = []
|
||||
"###
|
||||
);
|
||||
});
|
||||
|
||||
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"
|
||||
|
||||
[[distribution]]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
source = "editable+."
|
||||
sdist = { path = "." }
|
||||
"###
|
||||
);
|
||||
});
|
||||
|
||||
// Install from the lockfile.
|
||||
uv_snapshot!(context.filters(), context.sync(), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
warning: `uv sync` is experimental and may change without warning.
|
||||
Audited 1 package in [TIME]
|
||||
"###);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Remove a PyPI requirement that occurs multiple times.
|
||||
#[test]
|
||||
fn remove_all_registry() -> 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 == 3.7.0 ; python_version >= '3.12'",
|
||||
"anyio < 3.7.0 ; python_version < '3.12'",
|
||||
]
|
||||
"#})?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.lock(), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
warning: `uv lock` is experimental and may change without warning.
|
||||
Resolved 10 packages in [TIME]
|
||||
"###);
|
||||
|
||||
uv_snapshot!(context.filters(), context.sync(), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
warning: `uv sync` is experimental and may change without warning.
|
||||
Downloaded 4 packages in [TIME]
|
||||
Installed 4 packages in [TIME]
|
||||
+ anyio==3.7.0
|
||||
+ idna==3.6
|
||||
+ project==0.1.0 (from file://[TEMP_DIR]/)
|
||||
+ sniffio==1.3.1
|
||||
"###);
|
||||
|
||||
uv_snapshot!(context.filters(), context.remove(&["anyio"]), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
warning: `uv remove` is experimental and may change without warning.
|
||||
Resolved 1 package in [TIME]
|
||||
Downloaded 1 package in [TIME]
|
||||
Uninstalled 1 package in [TIME]
|
||||
Installed 1 package in [TIME]
|
||||
- project==0.1.0 (from file://[TEMP_DIR]/)
|
||||
+ project==0.1.0 (from file://[TEMP_DIR]/)
|
||||
"###);
|
||||
|
||||
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 = []
|
||||
"###
|
||||
);
|
||||
});
|
||||
|
||||
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"
|
||||
|
||||
[[distribution]]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
source = "editable+."
|
||||
sdist = { path = "." }
|
||||
"###
|
||||
);
|
||||
});
|
||||
|
||||
// Install from the lockfile.
|
||||
uv_snapshot!(context.filters(), context.sync(), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
warning: `uv sync` is experimental and may change without warning.
|
||||
Audited 1 package in [TIME]
|
||||
"###);
|
||||
|
||||
Ok(())
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue