use std::fmt::Write;
use std::io;
use std::path::Path;
use std::str::FromStr;
use anyhow::Result;
use owo_colors::OwoColorize;
use tracing::{debug, warn};
use uv_cache::Cache;
use uv_client::BaseClientBuilder;
use uv_configuration::{
Concurrency, DependencyGroups, DryRun, ExtrasSpecification, InstallOptions,
};
use uv_fs::Simplified;
use uv_normalize::PackageName;
use uv_normalize::{DEV_DEPENDENCIES, DefaultExtras, DefaultGroups};
use uv_preview::Preview;
use uv_python::{PythonDownloads, PythonPreference, PythonRequest};
use uv_scripts::{Pep723Metadata, Pep723Script};
use uv_settings::PythonInstallMirrors;
use uv_warnings::warn_user_once;
use uv_workspace::pyproject::DependencyType;
use uv_workspace::pyproject_mut::{DependencyTarget, PyProjectTomlMut};
use uv_workspace::{DiscoveryOptions, VirtualProject, WorkspaceCache};
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::lock_target::LockTarget;
use crate::commands::project::{
ProjectEnvironment, ProjectError, ProjectInterpreter, ScriptInterpreter, UniversalState,
WorkspacePython, default_dependency_groups,
};
use crate::commands::{ExitStatus, diagnostics, project};
use crate::printer::Printer;
use crate::settings::{FrozenSource, LockCheck, ResolverInstallerSettings};
pub(crate) async fn remove(
project_dir: &Path,
lock_check: LockCheck,
frozen: Option<FrozenSource>,
active: Option<bool>,
no_sync: bool,
packages: Vec<PackageName>,
dependency_type: DependencyType,
package: Option<PackageName>,
python: Option<String>,
install_mirrors: PythonInstallMirrors,
settings: ResolverInstallerSettings,
client_builder: BaseClientBuilder<'_>,
script: Option<Pep723Script>,
python_preference: PythonPreference,
python_downloads: PythonDownloads,
installer_metadata: bool,
concurrency: Concurrency,
no_config: bool,
cache: &Cache,
printer: Printer,
preview: Preview,
) -> Result<ExitStatus> {
let target = if let Some(script) = script {
if package.is_some() {
warn_user_once!(
"`--package` is a no-op for Python scripts with inline metadata, which always run in isolation"
);
}
if let LockCheck::Enabled(lock_check) = lock_check {
warn_user_once!(
"`{lock_check}` is a no-op for Python scripts with inline metadata, which always run in isolation",
);
}
if frozen.is_some() {
warn_user_once!(
"`--frozen` is a no-op for Python scripts with inline metadata, which always run in isolation"
);
}
if no_sync {
warn_user_once!(
"`--no-sync` is a no-op for Python scripts with inline metadata, which always run in isolation"
);
}
RemoveTarget::Script(script)
} else {
let project = if let Some(package) = package {
VirtualProject::discover_with_package(
project_dir,
&DiscoveryOptions::default(),
&WorkspaceCache::default(),
package.clone(),
)
.await?
} else {
VirtualProject::discover(
project_dir,
&DiscoveryOptions::default(),
&WorkspaceCache::default(),
)
.await?
};
RemoveTarget::Project(project)
};
let mut toml = match &target {
RemoveTarget::Script(script) => {
PyProjectTomlMut::from_toml(&script.metadata.raw, DependencyTarget::Script)
}
RemoveTarget::Project(project) => PyProjectTomlMut::from_toml(
project.pyproject_toml().raw.as_ref(),
DependencyTarget::PyProjectToml,
),
}?;
for package in packages {
match dependency_type {
DependencyType::Production => {
let deps = toml.remove_dependency(&package)?;
if deps.is_empty() {
show_other_dependency_type_hint(printer, &package, &toml)?;
anyhow::bail!(
"The dependency `{package}` could not be found in `project.dependencies`"
)
}
}
DependencyType::Dev => {
let dev_deps = toml.remove_dev_dependency(&package)?;
let group_deps =
toml.remove_dependency_group_requirement(&package, &DEV_DEPENDENCIES)?;
if dev_deps.is_empty() && group_deps.is_empty() {
show_other_dependency_type_hint(printer, &package, &toml)?;
anyhow::bail!(
"The dependency `{package}` could not be found in `tool.uv.dev-dependencies` or `tool.uv.dependency-groups.dev`"
);
}
}
DependencyType::Optional(ref extra) => {
let deps = toml.remove_optional_dependency(&package, extra)?;
if deps.is_empty() {
show_other_dependency_type_hint(printer, &package, &toml)?;
anyhow::bail!(
"The dependency `{package}` could not be found in `project.optional-dependencies.{extra}`"
);
}
}
DependencyType::Group(ref group) => {
if group == &*DEV_DEPENDENCIES {
let dev_deps = toml.remove_dev_dependency(&package)?;
let group_deps =
toml.remove_dependency_group_requirement(&package, &DEV_DEPENDENCIES)?;
if dev_deps.is_empty() && group_deps.is_empty() {
show_other_dependency_type_hint(printer, &package, &toml)?;
anyhow::bail!(
"The dependency `{package}` could not be found in `tool.uv.dev-dependencies` or `tool.uv.dependency-groups.dev`"
);
}
} else {
let deps = toml.remove_dependency_group_requirement(&package, group)?;
if deps.is_empty() {
show_other_dependency_type_hint(printer, &package, &toml)?;
anyhow::bail!(
"The dependency `{package}` could not be found in `dependency-groups.{group}`"
);
}
}
}
}
}
let content = toml.to_string();
target.write(&content)?;
if frozen.is_some() {
return Ok(ExitStatus::Success);
}
if let RemoveTarget::Script(ref script) = target {
if !LockTarget::from(script).lock_path().is_file() {
writeln!(
printer.stderr(),
"Updated `{}`",
script.path.user_display().cyan()
)?;
return Ok(ExitStatus::Success);
}
}
let target = target.update(&content)?;
let default_groups = match &target {
RemoveTarget::Project(project) => default_dependency_groups(project.pyproject_toml())?,
RemoveTarget::Script(_) => DefaultGroups::default(),
};
let groups = DependencyGroups::default().with_defaults(default_groups);
let extras = ExtrasSpecification::default().with_defaults(DefaultExtras::default());
let target = match target {
RemoveTarget::Project(project) => {
if no_sync {
let workspace_python = WorkspacePython::from_request(
python.as_deref().map(PythonRequest::parse),
Some(project.workspace()),
&groups,
project_dir,
no_config,
)
.await?;
let interpreter = ProjectInterpreter::discover(
project.workspace(),
&groups,
workspace_python,
&client_builder,
python_preference,
python_downloads,
&install_mirrors,
false,
active,
cache,
printer,
preview,
)
.await?
.into_interpreter();
AddTarget::Project(project, Box::new(PythonTarget::Interpreter(interpreter)))
} else {
let environment = ProjectEnvironment::get_or_init(
project.workspace(),
&groups,
python.as_deref().map(PythonRequest::parse),
&install_mirrors,
&client_builder,
python_preference,
python_downloads,
no_sync,
no_config,
active,
cache,
DryRun::Disabled,
printer,
preview,
)
.await?
.into_environment()?;
AddTarget::Project(project, Box::new(PythonTarget::Environment(environment)))
}
}
RemoveTarget::Script(script) => {
let interpreter = ScriptInterpreter::discover(
(&script).into(),
python.as_deref().map(PythonRequest::parse),
&client_builder,
python_preference,
python_downloads,
&install_mirrors,
no_sync,
no_config,
active,
cache,
printer,
preview,
)
.await?
.into_interpreter();
AddTarget::Script(script, Box::new(interpreter))
}
};
let _lock = target
.acquire_lock()
.await
.inspect_err(|err| {
warn!("Failed to acquire environment lock: {err}");
})
.ok();
let mode = if let LockCheck::Enabled(lock_check) = lock_check {
LockMode::Locked(target.interpreter(), lock_check)
} else {
LockMode::Write(target.interpreter())
};
let state = UniversalState::default();
let lock = match Box::pin(
project::lock::LockOperation::new(
mode,
&settings.resolver,
&client_builder,
&state,
Box::new(DefaultResolveLogger),
&concurrency,
cache,
&WorkspaceCache::default(),
printer,
preview,
)
.execute((&target).into()),
)
.await
{
Ok(result) => result.into_lock(),
Err(ProjectError::Operation(err)) => {
return diagnostics::OperationDiagnostic::with_system_certs(
client_builder.system_certs(),
)
.report(err)
.map_or(Ok(ExitStatus::Failure), |err| Err(err.into()));
}
Err(err) => return Err(err.into()),
};
let AddTarget::Project(project, environment) = target else {
return Ok(ExitStatus::Success);
};
let PythonTarget::Environment(venv) = &*environment else {
return Ok(ExitStatus::Success);
};
let target = match &project {
VirtualProject::Project(project) => InstallTarget::Project {
workspace: project.workspace(),
name: project.project_name(),
lock: &lock,
},
VirtualProject::NonProject(workspace) => InstallTarget::NonProjectWorkspace {
workspace,
lock: &lock,
},
};
let state = state.fork();
match project::sync::do_sync(
target,
venv,
&extras,
&groups,
None,
InstallOptions::default(),
Modifications::Exact,
None,
(&settings).into(),
&client_builder,
&state,
Box::new(DefaultInstallLogger),
installer_metadata,
&concurrency,
cache,
&WorkspaceCache::default(),
DryRun::Disabled,
printer,
preview,
)
.await
{
Ok(_) => {}
Err(ProjectError::Operation(err)) => {
return diagnostics::OperationDiagnostic::with_system_certs(
client_builder.system_certs(),
)
.report(err)
.map_or(Ok(ExitStatus::Failure), |err| Err(err.into()));
}
Err(err) => return Err(err.into()),
}
Ok(ExitStatus::Success)
}
#[derive(Debug)]
#[expect(clippy::large_enum_variant)]
enum RemoveTarget {
Project(VirtualProject),
Script(Pep723Script),
}
impl RemoveTarget {
fn write(&self, content: &str) -> Result<bool, io::Error> {
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)
}
}
}
}
fn update(self, content: &str) -> Result<Self, ProjectError> {
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
.update_member(
toml::from_str(content).map_err(ProjectError::PyprojectTomlParse)?,
)?
.ok_or(ProjectError::PyprojectTomlUpdate)?;
Ok(Self::Project(project))
}
}
}
}
fn show_other_dependency_type_hint(
printer: Printer,
name: &PackageName,
pyproject: &PyProjectTomlMut,
) -> Result<()> {
for dep_ty in pyproject.find_dependency(name, None) {
match dep_ty {
DependencyType::Production => writeln!(
printer.stderr(),
"{}{} `{name}` is a production dependency",
"hint".bold().cyan(),
":".bold(),
)?,
DependencyType::Dev => writeln!(
printer.stderr(),
"{}{} `{name}` is a development dependency (try: `{}`)",
"hint".bold().cyan(),
":".bold(),
format!("uv remove {name} --dev`").bold()
)?,
DependencyType::Optional(group) => writeln!(
printer.stderr(),
"{}{} `{name}` is an optional dependency (try: `{}`)",
"hint".bold().cyan(),
":".bold(),
format!("uv remove {name} --optional {group}").bold()
)?,
DependencyType::Group(group) => writeln!(
printer.stderr(),
"{}{} `{name}` is in the `{group}` group (try: `{}`)",
"hint".bold().cyan(),
":".bold(),
format!("uv remove {name} --group {group}").bold()
)?,
}
}
Ok(())
}