diff --git a/crates/uv/src/commands/project/add.rs b/crates/uv/src/commands/project/add.rs index 394592b17ba1..21abff893558 100644 --- a/crates/uv/src/commands/project/add.rs +++ b/crates/uv/src/commands/project/add.rs @@ -992,9 +992,27 @@ fn resolve_requirement( Ok((processed_requirement, source)) } +/// A Python [`Interpreter`] or [`PythonEnvironment`] for a project. +#[derive(Debug, Clone)] +#[allow(clippy::large_enum_variant)] +pub(super) enum PythonTarget { + Interpreter(Interpreter), + Environment(PythonEnvironment), +} + +impl PythonTarget { + /// Return the [`Interpreter`] for the project. + fn interpreter(&self) -> &Interpreter { + match self { + Self::Interpreter(interpreter) => interpreter, + Self::Environment(venv) => venv.interpreter(), + } + } +} + /// Represents the destination where dependencies are added, either to a project or a script. #[derive(Debug, Clone)] -enum AddTarget { +pub(super) enum AddTarget { /// A PEP 723 script, with inline metadata. Script(Pep723Script, Box), @@ -1013,7 +1031,7 @@ impl<'lock> From<&'lock AddTarget> for LockTarget<'lock> { impl AddTarget { /// Returns the [`Interpreter`] for the target. - fn interpreter(&self) -> &Interpreter { + pub(super) fn interpreter(&self) -> &Interpreter { match self { Self::Script(_, interpreter) => interpreter, Self::Project(_, venv) => venv.interpreter(), @@ -1133,24 +1151,6 @@ impl AddTargetSnapshot { } } -/// A Python [`Interpreter`] or [`PythonEnvironment`] for a project. -#[derive(Debug, Clone)] -#[allow(clippy::large_enum_variant)] -enum PythonTarget { - Interpreter(Interpreter), - Environment(PythonEnvironment), -} - -impl PythonTarget { - /// Return the [`Interpreter`] for the project. - fn interpreter(&self) -> &Interpreter { - match self { - Self::Interpreter(interpreter) => interpreter, - Self::Environment(venv) => venv.interpreter(), - } - } -} - #[derive(Debug, Clone)] struct DependencyEdit { dependency_type: DependencyType, diff --git a/crates/uv/src/commands/project/remove.rs b/crates/uv/src/commands/project/remove.rs index 415115aa358a..d68a0f789004 100644 --- a/crates/uv/src/commands/project/remove.rs +++ b/crates/uv/src/commands/project/remove.rs @@ -1,9 +1,10 @@ -use std::fmt::Write; -use std::path::Path; - use anyhow::{Context, Result}; use owo_colors::OwoColorize; - +use std::fmt::Write; +use std::io; +use std::path::Path; +use std::str::FromStr; +use tracing::debug; use uv_cache::Cache; use uv_client::Connectivity; use uv_configuration::{ @@ -15,7 +16,7 @@ use uv_fs::Simplified; use uv_normalize::DEV_DEPENDENCIES; use uv_pep508::PackageName; use uv_python::{PythonDownloads, PythonPreference, PythonRequest}; -use uv_scripts::Pep723Script; +use uv_scripts::{Pep723Item, Pep723Metadata, Pep723Script}; use uv_settings::PythonInstallMirrors; use uv_warnings::warn_user_once; use uv_workspace::pyproject::DependencyType; @@ -24,9 +25,13 @@ use uv_workspace::{DiscoveryOptions, VirtualProject, Workspace}; use crate::commands::pip::loggers::{DefaultInstallLogger, DefaultResolveLogger}; use crate::commands::pip::operations::Modifications; +use crate::commands::project::add::{AddTarget, PythonTarget}; use crate::commands::project::install_target::InstallTarget; use crate::commands::project::lock::LockMode; -use crate::commands::project::{default_dependency_groups, ProjectError}; +use crate::commands::project::lock_target::LockTarget; +use crate::commands::project::{ + default_dependency_groups, ProjectError, ProjectInterpreter, ScriptInterpreter, +}; use crate::commands::{diagnostics, project, ExitStatus}; use crate::printer::Printer; use crate::settings::ResolverInstallerSettings; @@ -79,7 +84,7 @@ pub(crate) async fn remove( "`--no-sync` is a no-op for Python scripts with inline metadata, which always run in isolation" ); } - Target::Script(script) + RemoveTarget::Script(script) } else { // Find the project in the workspace. let project = if let Some(package) = package { @@ -93,14 +98,14 @@ pub(crate) async fn remove( VirtualProject::discover(project_dir, &DiscoveryOptions::default()).await? }; - Target::Project(project) + RemoveTarget::Project(project) }; let mut toml = match &target { - Target::Script(script) => { + RemoveTarget::Script(script) => { PyProjectTomlMut::from_toml(&script.metadata.raw, DependencyTarget::Script) } - Target::Project(project) => PyProjectTomlMut::from_toml( + RemoveTarget::Project(project) => PyProjectTomlMut::from_toml( project.pyproject_toml().raw.as_ref(), DependencyTarget::PyProjectToml, ), @@ -161,27 +166,20 @@ pub(crate) async fn remove( } } - // Save the modified dependencies. - match &target { - Target::Script(script) => { - script.write(&toml.to_string())?; - } - Target::Project(project) => { - let pyproject_path = project.root().join("pyproject.toml"); - fs_err::write(pyproject_path, toml.to_string())?; - } - }; + let content = toml.to_string(); - // If `--frozen`, exit early. There's no reason to lock and sync, and we don't need a `uv.lock` + // Save the modified `pyproject.toml` or script. + target.write(&content)?; + + // If `--frozen`, exit early. There's no reason to lock and sync, since we don't need a `uv.lock` // to exist at all. if frozen { return Ok(ExitStatus::Success); } - let project = match target { - Target::Project(project) => project, - // If `--script`, exit early. There's no reason to lock and sync. - Target::Script(script) => { + // If we're modifying a script, and lockfile doesn't exist, don't create it. + if let RemoveTarget::Script(ref script) = target { + if !LockTarget::from(script).lock_path().is_file() { writeln!( printer.stderr(), "Updated `{}`", @@ -189,31 +187,80 @@ pub(crate) async fn remove( )?; return Ok(ExitStatus::Success); } - }; + } - // Discover or create the virtual environment. - let venv = project::get_or_init_environment( - project.workspace(), - python.as_deref().map(PythonRequest::parse), - &install_mirrors, - python_preference, - python_downloads, - connectivity, - native_tls, - allow_insecure_host, - no_config, - cache, - printer, - ) - .await?; + // Update the `pypackage.toml` in-memory. + let target = target.update(&content)?; + + // Convert to an `AddTarget` by attaching the appropriate interpreter or environment. + let target = match target { + RemoveTarget::Project(project) => { + if no_sync { + // Discover the interpreter. + let interpreter = ProjectInterpreter::discover( + project.workspace(), + project_dir, + python.as_deref().map(PythonRequest::parse), + python_preference, + python_downloads, + connectivity, + native_tls, + allow_insecure_host, + &install_mirrors, + no_config, + cache, + printer, + ) + .await? + .into_interpreter(); + + AddTarget::Project(project, Box::new(PythonTarget::Interpreter(interpreter))) + } else { + // Discover or create the virtual environment. + let venv = project::get_or_init_environment( + project.workspace(), + python.as_deref().map(PythonRequest::parse), + &install_mirrors, + python_preference, + python_downloads, + connectivity, + native_tls, + allow_insecure_host, + no_config, + cache, + printer, + ) + .await?; + + AddTarget::Project(project, Box::new(PythonTarget::Environment(venv))) + } + } + RemoveTarget::Script(script) => { + let interpreter = ScriptInterpreter::discover( + &Pep723Item::Script(script.clone()), + python.as_deref().map(PythonRequest::parse), + python_preference, + python_downloads, + connectivity, + native_tls, + allow_insecure_host, + &install_mirrors, + no_config, + cache, + printer, + ) + .await? + .into_interpreter(); + + AddTarget::Script(script, Box::new(interpreter)) + } + }; // Determine the lock mode. - let mode = if frozen { - LockMode::Frozen - } else if locked { - LockMode::Locked(venv.interpreter()) + let mode = if locked { + LockMode::Locked(target.interpreter()) } else { - LockMode::Write(venv.interpreter()) + LockMode::Write(target.interpreter()) }; // Initialize any shared state. @@ -222,7 +269,7 @@ pub(crate) async fn remove( // Lock and sync the environment, if necessary. let lock = match project::lock::do_safe_lock( mode, - project.workspace().into(), + (&target).into(), settings.as_ref().into(), LowerBound::Allow, &state, @@ -246,9 +293,15 @@ pub(crate) async fn remove( Err(err) => return Err(err.into()), }; - if no_sync { + let AddTarget::Project(project, environment) = target else { + // If we're not adding to a project, exit early. return Ok(ExitStatus::Success); - } + }; + + let PythonTarget::Environment(venv) = &*environment else { + // If we're not syncing, exit early. + return Ok(ExitStatus::Success); + }; // 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? @@ -273,7 +326,7 @@ pub(crate) async fn remove( match project::sync::do_sync( target, - &venv, + venv, &extras, &DevGroupsManifest::from_defaults(defaults), EditableMode::Editable, @@ -306,13 +359,62 @@ pub(crate) async fn remove( /// Represents the destination where dependencies are added, either to a project or a script. #[derive(Debug)] -enum Target { +enum RemoveTarget { /// A PEP 723 script, with inline metadata. Project(VirtualProject), /// A project with a `pyproject.toml`. Script(Pep723Script), } +impl RemoveTarget { + /// Write the updated content to the target. + /// + /// Returns `true` if the content was modified. + fn write(&self, content: &str) -> Result { + match self { + Self::Script(script) => { + if content == script.metadata.raw { + debug!("No changes to dependencies; skipping update"); + Ok(false) + } else { + script.write(content)?; + Ok(true) + } + } + Self::Project(project) => { + if content == project.pyproject_toml().raw { + debug!("No changes to dependencies; skipping update"); + Ok(false) + } else { + let pyproject_path = project.root().join("pyproject.toml"); + fs_err::write(pyproject_path, content)?; + Ok(true) + } + } + } + } + + /// Update the target in-memory to incorporate the new content. + #[allow(clippy::result_large_err)] + fn update(self, content: &str) -> Result { + match self { + Self::Script(mut script) => { + script.metadata = Pep723Metadata::from_str(content) + .map_err(ProjectError::Pep723ScriptTomlParse)?; + Ok(Self::Script(script)) + } + Self::Project(project) => { + let project = project + .with_pyproject_toml( + toml::from_str(content).map_err(ProjectError::PyprojectTomlParse)?, + ) + .ok_or(ProjectError::PyprojectTomlUpdate)?; + Ok(Self::Project(project)) + } + } + } +} + /// Show a hint if a dependency with the given name is present as any dependency type. /// /// This is useful when a dependency of the user-specified type was not found, but it may be present diff --git a/crates/uv/tests/it/edit.rs b/crates/uv/tests/it/edit.rs index 82d8f39827e2..c2fe4a33c4a9 100644 --- a/crates/uv/tests/it/edit.rs +++ b/crates/uv/tests/it/edit.rs @@ -5334,9 +5334,9 @@ fn remove_repeated() -> Result<()> { Ok(()) } -/// Add to a PEP 732 script with a lockfile. +/// Add to (and remove from) a PEP 732 script with a lockfile. #[test] -fn add_script_lock() -> Result<()> { +fn add_remove_script_lock() -> Result<()> { let context = TestContext::new("3.12"); let script = context.temp_dir.child("script.py"); @@ -5718,10 +5718,191 @@ fn add_script_lock() -> Result<()> { ); }); + // Removing from a locked script should update the lockfile. + uv_snapshot!(context.filters(), context.remove().arg("anyio").arg("--script").arg("script.py"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 9 packages in [TIME] + "###); + + let script_content = context.read("script.py"); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + script_content, @r###" + # /// script + # requires-python = ">=3.11" + # dependencies = [ + # "requests<3", + # "rich", + # ] + # /// + + import requests + from rich.pretty import pprint + + resp = requests.get("https://peps.python.org/api/peps.json") + data = resp.json() + pprint([(k, v["title"]) for k, v in data.items()][:10]) + "### + ); + }); + + let lock = context.read("script.py.lock"); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + lock, @r###" + version = 1 + requires-python = ">=3.11" + + [options] + exclude-newer = "2024-03-25T00:00:00Z" + + [manifest] + requirements = [ + { name = "requests", specifier = "<3" }, + { name = "rich" }, + ] + + [[package]] + name = "certifi" + version = "2024.2.2" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/71/da/e94e26401b62acd6d91df2b52954aceb7f561743aa5ccc32152886c76c96/certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f", size = 164886 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/06/a07f096c664aeb9f01624f858c3add0a4e913d6c96257acb4fce61e7de14/certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1", size = 163774 }, + ] + + [[package]] + name = "charset-normalizer" + version = "3.3.2" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/63/09/c1bc53dab74b1816a00d8d030de5bf98f724c52c1635e07681d312f20be8/charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5", size = 104809 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/68/77/02839016f6fbbf808e8b38601df6e0e66c17bbab76dff4613f7511413597/charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db", size = 191647 }, + { url = "https://files.pythonhosted.org/packages/3e/33/21a875a61057165e92227466e54ee076b73af1e21fe1b31f1e292251aa1e/charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96", size = 121434 }, + { url = "https://files.pythonhosted.org/packages/dd/51/68b61b90b24ca35495956b718f35a9756ef7d3dd4b3c1508056fa98d1a1b/charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e", size = 118979 }, + { url = "https://files.pythonhosted.org/packages/e4/a6/7ee57823d46331ddc37dd00749c95b0edec2c79b15fc0d6e6efb532e89ac/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f", size = 136582 }, + { url = "https://files.pythonhosted.org/packages/74/f1/0d9fe69ac441467b737ba7f48c68241487df2f4522dd7246d9426e7c690e/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574", size = 146645 }, + { url = "https://files.pythonhosted.org/packages/05/31/e1f51c76db7be1d4aef220d29fbfa5dbb4a99165d9833dcbf166753b6dc0/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4", size = 139398 }, + { url = "https://files.pythonhosted.org/packages/40/26/f35951c45070edc957ba40a5b1db3cf60a9dbb1b350c2d5bef03e01e61de/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8", size = 140273 }, + { url = "https://files.pythonhosted.org/packages/07/07/7e554f2bbce3295e191f7e653ff15d55309a9ca40d0362fcdab36f01063c/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc", size = 142577 }, + { url = "https://files.pythonhosted.org/packages/d8/b5/eb705c313100defa57da79277d9207dc8d8e45931035862fa64b625bfead/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae", size = 137747 }, + { url = "https://files.pythonhosted.org/packages/19/28/573147271fd041d351b438a5665be8223f1dd92f273713cb882ddafe214c/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887", size = 143375 }, + { url = "https://files.pythonhosted.org/packages/cf/7c/f3b682fa053cc21373c9a839e6beba7705857075686a05c72e0f8c4980ca/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae", size = 148474 }, + { url = "https://files.pythonhosted.org/packages/1e/49/7ab74d4ac537ece3bc3334ee08645e231f39f7d6df6347b29a74b0537103/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce", size = 140232 }, + { url = "https://files.pythonhosted.org/packages/2d/dc/9dacba68c9ac0ae781d40e1a0c0058e26302ea0660e574ddf6797a0347f7/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f", size = 140859 }, + { url = "https://files.pythonhosted.org/packages/6c/c2/4a583f800c0708dd22096298e49f887b49d9746d0e78bfc1d7e29816614c/charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab", size = 92509 }, + { url = "https://files.pythonhosted.org/packages/57/ec/80c8d48ac8b1741d5b963797b7c0c869335619e13d4744ca2f67fc11c6fc/charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77", size = 99870 }, + { url = "https://files.pythonhosted.org/packages/d1/b2/fcedc8255ec42afee97f9e6f0145c734bbe104aac28300214593eb326f1d/charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8", size = 192892 }, + { url = "https://files.pythonhosted.org/packages/2e/7d/2259318c202f3d17f3fe6438149b3b9e706d1070fe3fcbb28049730bb25c/charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b", size = 122213 }, + { url = "https://files.pythonhosted.org/packages/3a/52/9f9d17c3b54dc238de384c4cb5a2ef0e27985b42a0e5cc8e8a31d918d48d/charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6", size = 119404 }, + { url = "https://files.pythonhosted.org/packages/99/b0/9c365f6d79a9f0f3c379ddb40a256a67aa69c59609608fe7feb6235896e1/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a", size = 137275 }, + { url = "https://files.pythonhosted.org/packages/91/33/749df346e93d7a30cdcb90cbfdd41a06026317bfbfb62cd68307c1a3c543/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389", size = 147518 }, + { url = "https://files.pythonhosted.org/packages/72/1a/641d5c9f59e6af4c7b53da463d07600a695b9824e20849cb6eea8a627761/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa", size = 140182 }, + { url = "https://files.pythonhosted.org/packages/ee/fb/14d30eb4956408ee3ae09ad34299131fb383c47df355ddb428a7331cfa1e/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b", size = 141869 }, + { url = "https://files.pythonhosted.org/packages/df/3e/a06b18788ca2eb6695c9b22325b6fde7dde0f1d1838b1792a0076f58fe9d/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed", size = 144042 }, + { url = "https://files.pythonhosted.org/packages/45/59/3d27019d3b447a88fe7e7d004a1e04be220227760264cc41b405e863891b/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26", size = 138275 }, + { url = "https://files.pythonhosted.org/packages/7b/ef/5eb105530b4da8ae37d506ccfa25057961b7b63d581def6f99165ea89c7e/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d", size = 144819 }, + { url = "https://files.pythonhosted.org/packages/a2/51/e5023f937d7f307c948ed3e5c29c4b7a3e42ed2ee0b8cdf8f3a706089bf0/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068", size = 149415 }, + { url = "https://files.pythonhosted.org/packages/24/9d/2e3ef673dfd5be0154b20363c5cdcc5606f35666544381bee15af3778239/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143", size = 141212 }, + { url = "https://files.pythonhosted.org/packages/5b/ae/ce2c12fcac59cb3860b2e2d76dc405253a4475436b1861d95fe75bdea520/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4", size = 142167 }, + { url = "https://files.pythonhosted.org/packages/ed/3a/a448bf035dce5da359daf9ae8a16b8a39623cc395a2ffb1620aa1bce62b0/charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7", size = 93041 }, + { url = "https://files.pythonhosted.org/packages/b6/7c/8debebb4f90174074b827c63242c23851bdf00a532489fba57fef3416e40/charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001", size = 100397 }, + { url = "https://files.pythonhosted.org/packages/28/76/e6222113b83e3622caa4bb41032d0b1bf785250607392e1b778aca0b8a7d/charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc", size = 48543 }, + ] + + [[package]] + 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 }, + ] + + [[package]] + name = "markdown-it-py" + version = "3.0.0" + source = { registry = "https://pypi.org/simple" } + dependencies = [ + { name = "mdurl" }, + ] + sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 }, + ] + + [[package]] + name = "mdurl" + version = "0.1.2" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, + ] + + [[package]] + name = "pygments" + version = "2.17.2" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/55/59/8bccf4157baf25e4aa5a0bb7fa3ba8600907de105ebc22b0c78cfbf6f565/pygments-2.17.2.tar.gz", hash = "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367", size = 4827772 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/97/9c/372fef8377a6e340b1704768d20daaded98bf13282b5327beb2e2fe2c7ef/pygments-2.17.2-py3-none-any.whl", hash = "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c", size = 1179756 }, + ] + + [[package]] + name = "requests" + version = "2.31.0" + source = { registry = "https://pypi.org/simple" } + dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, + ] + sdist = { url = "https://files.pythonhosted.org/packages/9d/be/10918a2eac4ae9f02f6cfe6414b7a155ccd8f7f9d4380d62fd5b955065c3/requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1", size = 110794 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/70/8e/0e2d847013cb52cd35b38c009bb167a1a26b2ce6cd6965bf26b47bc0bf44/requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f", size = 62574 }, + ] + + [[package]] + name = "rich" + version = "13.7.1" + source = { registry = "https://pypi.org/simple" } + dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, + ] + sdist = { url = "https://files.pythonhosted.org/packages/b3/01/c954e134dc440ab5f96952fe52b4fdc64225530320a910473c1fe270d9aa/rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432", size = 221248 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/87/67/a37f6214d0e9fe57f6ae54b2956d550ca8365857f42a1ce0392bb21d9410/rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222", size = 240681 }, + ] + + [[package]] + name = "urllib3" + version = "2.2.1" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/7a/50/7fd50a27caa0652cd4caf224aa87741ea41d3265ad13f010886167cfcc79/urllib3-2.2.1.tar.gz", hash = "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19", size = 291020 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/73/a68704750a7679d0b6d3ad7aa8d4da8e14e151ae82e6fee774e6e0d05ec8/urllib3-2.2.1-py3-none-any.whl", hash = "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d", size = 121067 }, + ] + "### + ); + }); + Ok(()) } -/// Remove from a PEP732 script, +/// Remove from a PEP 723 script. #[test] fn remove_script() -> Result<()> { let context = TestContext::new("3.12"); @@ -5781,7 +5962,78 @@ fn remove_script() -> Result<()> { Ok(()) } -/// Remove last dependency PEP732 script +/// Remove from a PEP 723 script. +#[test] +fn remove_script_lock() -> Result<()> { + let context = TestContext::new("3.12"); + + let script = context.temp_dir.child("script.py"); + script.write_str(indoc! {r#" + # /// script + # requires-python = ">=3.11" + # dependencies = [ + # "requests<3", + # "rich", + # "anyio", + # ] + # /// + + import requests + from rich.pretty import pprint + + resp = requests.get("https://peps.python.org/api/peps.json") + data = resp.json() + pprint([(k, v["title"]) for k, v in data.items()][:10]) + "#})?; + + // Explicitly lock the script. + uv_snapshot!(context.filters(), context.lock().arg("--script").arg("script.py"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 9 packages in [TIME] + "###); + + uv_snapshot!(context.filters(), context.remove().arg("anyio").arg("--script").arg("script.py"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Updated `script.py` + "###); + + let script_content = context.read("script.py"); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + script_content, @r###" + # /// script + # requires-python = ">=3.11" + # dependencies = [ + # "requests<3", + # "rich", + # ] + # /// + + import requests + from rich.pretty import pprint + + resp = requests.get("https://peps.python.org/api/peps.json") + data = resp.json() + pprint([(k, v["title"]) for k, v in data.items()][:10]) + "### + ); + }); + + Ok(()) +} + +/// Remove last dependency PEP 723 script #[test] fn remove_last_dep_script() -> Result<()> { let context = TestContext::new("3.12"); @@ -5836,7 +6088,7 @@ fn remove_last_dep_script() -> Result<()> { Ok(()) } -/// Add a Git requirement to PEP732 script. +/// Add a Git requirement to PEP 723 script. #[test] #[cfg(feature = "git")] fn add_git_to_script() -> Result<()> {