use std::collections::BTreeMap;
use std::collections::hash_map::Entry;
use std::fmt::Write;
use std::io;
use std::path::Path;
use std::str::FromStr;
use std::sync::Arc;
use anyhow::{Result, bail};
use itertools::Itertools;
use owo_colors::OwoColorize;
use rustc_hash::{FxBuildHasher, FxHashMap};
use tracing::{debug, warn};
use uv_cache::Cache;
use uv_cache_key::RepositoryUrl;
use uv_client::{BaseClientBuilder, FlatIndexClient, RegistryClientBuilder};
use uv_configuration::{
Concurrency, Constraints, DependencyGroups, DependencyGroupsWithDefaults, DevMode, DryRun,
ExtrasSpecification, ExtrasSpecificationWithDefaults, GitLfsSetting, InstallOptions, NoSources,
};
use uv_dispatch::BuildDispatch;
use uv_distribution::{DistributionDatabase, LoweredExtraBuildDependencies};
use uv_distribution_types::{
Identifier, Index, IndexName, IndexUrl, IndexUrls, NameRequirementSpecification, Requirement,
RequirementSource, UnresolvedRequirement,
};
use uv_fs::{LockedFile, LockedFileError, Simplified};
use uv_git::GIT_STORE;
use uv_normalize::{DEV_DEPENDENCIES, DefaultExtras, DefaultGroups, ExtraName, PackageName};
use uv_pep508::{MarkerTree, VersionOrUrl};
use uv_preview::Preview;
use uv_python::{Interpreter, PythonDownloads, PythonEnvironment, PythonPreference, PythonRequest};
use uv_redacted::DisplaySafeUrl;
use uv_requirements::{NamedRequirementsResolver, RequirementsSource, RequirementsSpecification};
use uv_resolver::FlatIndex;
use uv_scripts::{Pep723Metadata, Pep723Script};
use uv_settings::PythonInstallMirrors;
use uv_types::{BuildIsolation, HashStrategy, SourceTreeEditablePolicy};
use uv_warnings::warn_user_once;
use uv_workspace::pyproject::{DependencyType, Source, SourceError, Sources, ToolUvSources};
use uv_workspace::pyproject_mut::{AddBoundsKind, ArrayEdit, DependencyTarget, PyProjectTomlMut};
use uv_workspace::{DiscoveryOptions, VirtualProject, WorkspaceCache};
use crate::commands::pip::loggers::{
DefaultInstallLogger, DefaultResolveLogger, SummaryResolveLogger,
};
use crate::commands::pip::operations::Modifications;
use crate::commands::project::install_target::InstallTarget;
use crate::commands::project::lock::LockMode;
use crate::commands::project::lock_target::LockTarget;
use crate::commands::project::{
PlatformState, ProjectEnvironment, ProjectError, ProjectInterpreter, ScriptInterpreter,
UniversalState, WorkspacePython, default_dependency_groups, init_script_python_requirement,
};
use crate::commands::reporters::{PythonDownloadReporter, ResolverReporter};
use crate::commands::{ExitStatus, ScriptPath, diagnostics, project};
use crate::printer::Printer;
use crate::settings::{FrozenSource, LockCheck, ResolverInstallerSettings};
#[expect(clippy::fn_params_excessive_bools)]
pub(crate) async fn add(
project_dir: &Path,
lock_check: LockCheck,
frozen: Option<FrozenSource>,
active: Option<bool>,
no_sync: bool,
no_install_project: bool,
only_install_project: bool,
no_install_workspace: bool,
only_install_workspace: bool,
no_install_local: bool,
only_install_local: bool,
no_install_package: Vec<PackageName>,
only_install_package: Vec<PackageName>,
requirements: Vec<RequirementsSource>,
constraints: Vec<RequirementsSource>,
marker: Option<MarkerTree>,
editable: Option<bool>,
dependency_type: DependencyType,
raw: bool,
bounds: Option<AddBoundsKind>,
indexes: Vec<Index>,
rev: Option<String>,
tag: Option<String>,
branch: Option<String>,
lfs: GitLfsSetting,
extras_of_dependency: Vec<ExtraName>,
package: Option<PackageName>,
python: Option<String>,
workspace: Option<bool>,
install_mirrors: PythonInstallMirrors,
settings: ResolverInstallerSettings,
client_builder: BaseClientBuilder<'_>,
script: Option<ScriptPath>,
python_preference: PythonPreference,
python_downloads: PythonDownloads,
installer_metadata: bool,
concurrency: Concurrency,
no_config: bool,
cache: &Cache,
printer: Printer,
preview: Preview,
) -> Result<ExitStatus> {
for source in &requirements {
match source {
RequirementsSource::PyprojectToml(_) => {
bail!("Adding requirements from a `pyproject.toml` is not supported in `uv add`");
}
RequirementsSource::SetupPy(_) => {
bail!("Adding requirements from a `setup.py` is not supported in `uv add`");
}
RequirementsSource::Pep723Script(_) => {
bail!("Adding requirements from a PEP 723 script is not supported in `uv add`");
}
RequirementsSource::SetupCfg(_) => {
bail!("Adding requirements from a `setup.cfg` is not supported in `uv add`");
}
RequirementsSource::PylockToml(_) => {
bail!("Adding requirements from a `pylock.toml` is not supported in `uv add`");
}
RequirementsSource::Package(_)
| RequirementsSource::Editable(_)
| RequirementsSource::RequirementsTxt(_)
| RequirementsSource::Extensionless(_)
| RequirementsSource::EnvironmentYml(_) => {}
}
}
let reporter = PythonDownloadReporter::single(printer);
let (extras, groups) = match &dependency_type {
DependencyType::Production => {
let extras = ExtrasSpecification::from_extra(vec![]);
let groups = DependencyGroups::from_dev_mode(DevMode::Exclude);
(extras, groups)
}
DependencyType::Dev => {
let extras = ExtrasSpecification::from_extra(vec![]);
let groups = DependencyGroups::from_dev_mode(DevMode::Include);
(extras, groups)
}
DependencyType::Optional(extra_name) => {
let extras = ExtrasSpecification::from_extra(vec![extra_name.clone()]);
let groups = DependencyGroups::from_dev_mode(DevMode::Exclude);
(extras, groups)
}
DependencyType::Group(group_name) => {
let extras = ExtrasSpecification::from_extra(vec![]);
let groups = DependencyGroups::from_group(group_name.clone());
(extras, groups)
}
};
let defaulted_extras = extras.with_defaults(DefaultExtras::default());
let defaulted_groups;
let mut 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"
);
}
let script = match script {
ScriptPath::Script(script) => script,
ScriptPath::Path(path) => {
let requires_python = init_script_python_requirement(
python.as_deref(),
&install_mirrors,
project_dir,
false,
python_preference,
python_downloads,
no_config,
&client_builder,
cache,
&reporter,
preview,
)
.await?;
Pep723Script::init(&path, requires_python.specifiers()).await?
}
};
defaulted_groups = groups.with_defaults(DefaultGroups::default());
let interpreter = ScriptInterpreter::discover(
(&script).into(),
python.as_deref().map(PythonRequest::parse),
&client_builder,
python_preference,
python_downloads,
&install_mirrors,
false,
no_config,
active,
cache,
printer,
preview,
)
.await?
.into_interpreter();
AddTarget::Script(script, Box::new(interpreter))
} else {
let project = if let Some(package) = package {
VirtualProject::discover_with_package(
project_dir,
&DiscoveryOptions::default(),
&WorkspaceCache::default(),
package,
)
.await?
} else {
VirtualProject::discover(
project_dir,
&DiscoveryOptions::default(),
&WorkspaceCache::default(),
)
.await?
};
if project.is_non_project() {
match dependency_type {
DependencyType::Production => {
bail!(
"Project is missing a `[project]` table; add a `[project]` table to use production dependencies, or run `{}` instead",
"uv add --dev".green()
)
}
DependencyType::Optional(_) => {
bail!(
"Project is missing a `[project]` table; add a `[project]` table to use optional dependencies, or run `{}` instead",
"uv add --dev".green()
)
}
DependencyType::Group(_) => {}
DependencyType::Dev => (),
}
}
defaulted_groups =
groups.with_defaults(default_dependency_groups(project.pyproject_toml())?);
if frozen.is_some() || no_sync {
let workspace_python = WorkspacePython::from_request(
python.as_deref().map(PythonRequest::parse),
Some(project.workspace()),
&defaulted_groups,
project_dir,
no_config,
)
.await?;
let interpreter = ProjectInterpreter::discover(
project.workspace(),
&defaulted_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(),
&defaulted_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)))
}
};
let _lock = target
.acquire_lock()
.await
.inspect_err(|err| {
warn!("Failed to acquire environment lock: {err}");
})
.ok();
let client_builder = client_builder
.clone()
.keyring(settings.resolver.keyring_provider);
let RequirementsSpecification {
requirements,
constraints,
..
} = RequirementsSpecification::from_sources(
&requirements,
&constraints,
&[],
&[],
None,
&client_builder,
)
.await?;
let state = PlatformState::default();
let requirements = {
let (mut requirements, unnamed): (Vec<_>, Vec<_>) = requirements
.into_iter()
.map(|spec| {
spec.requirement.augment_requirement(
rev.as_deref(),
tag.as_deref(),
branch.as_deref(),
lfs.into(),
marker,
)
})
.partition_map(|requirement| match requirement {
UnresolvedRequirement::Named(requirement) => itertools::Either::Left(requirement),
UnresolvedRequirement::Unnamed(requirement) => {
itertools::Either::Right(requirement)
}
});
if !unnamed.is_empty() {
let build_constraints = Constraints::default();
let build_hasher = HashStrategy::default();
let hasher = HashStrategy::default();
let sources = NoSources::None;
let client = RegistryClientBuilder::new(client_builder.clone(), cache.clone())
.index_locations(settings.resolver.index_locations.clone())
.index_strategy(settings.resolver.index_strategy)
.markers(target.interpreter().markers())
.platform(target.interpreter().platform())
.build()?;
let environment;
let build_isolation = match &settings.resolver.build_isolation {
uv_configuration::BuildIsolation::Isolate => BuildIsolation::Isolated,
uv_configuration::BuildIsolation::Shared => {
environment = PythonEnvironment::from_interpreter(target.interpreter().clone());
BuildIsolation::Shared(&environment)
}
uv_configuration::BuildIsolation::SharedPackage(packages) => {
environment = PythonEnvironment::from_interpreter(target.interpreter().clone());
BuildIsolation::SharedPackage(&environment, packages)
}
};
let flat_index = {
let client =
FlatIndexClient::new(client.cached_client(), client.connectivity(), cache);
let entries = client
.fetch_all(
settings
.resolver
.index_locations
.flat_indexes()
.map(Index::url),
)
.await?;
FlatIndex::from_entries(entries, None, &hasher, &settings.resolver.build_options)
};
let extra_build_requires = if let AddTarget::Project(project, _) = &target {
LoweredExtraBuildDependencies::from_workspace(
settings.resolver.extra_build_dependencies.clone(),
project.workspace(),
&settings.resolver.index_locations,
&settings.resolver.sources,
client.credentials_cache(),
)?
} else {
LoweredExtraBuildDependencies::from_non_lowered(
settings.resolver.extra_build_dependencies.clone(),
)
}
.into_inner();
let extra_build_variables = settings.resolver.extra_build_variables.clone();
let build_dispatch = BuildDispatch::new(
&client,
cache,
&build_constraints,
target.interpreter(),
&settings.resolver.index_locations,
&flat_index,
&settings.resolver.dependency_metadata,
state.clone().into_inner(),
settings.resolver.index_strategy,
&settings.resolver.config_setting,
&settings.resolver.config_settings_package,
build_isolation,
&extra_build_requires,
&extra_build_variables,
settings.resolver.link_mode,
&settings.resolver.build_options,
&build_hasher,
settings.resolver.exclude_newer.clone(),
sources,
SourceTreeEditablePolicy::Project,
WorkspaceCache::default(),
concurrency.clone(),
preview,
);
requirements.extend(
NamedRequirementsResolver::new(
&hasher,
state.index(),
DistributionDatabase::new(
&client,
&build_dispatch,
concurrency.downloads_semaphore.clone(),
),
)
.with_reporter(Arc::new(ResolverReporter::from(printer)))
.resolve(unnamed.into_iter())
.await?,
);
}
requirements
};
if matches!(dependency_type, DependencyType::Production) {
if let AddTarget::Project(project, _) = &target {
if let Some(project_name) = project.project_name() {
for requirement in &requirements {
if requirement.name == *project_name {
bail!(
"Requirement name `{}` matches project name `{}`, but self-dependencies are not permitted without the `--dev` or `--optional` flags. If your project name (`{}`) is shadowing that of a third-party dependency, consider renaming the project.",
requirement.name.cyan(),
project_name.cyan(),
project_name.cyan(),
);
}
}
}
}
}
let snapshot = target.snapshot().await?;
let index = indexes
.first()
.as_ref()
.and_then(|index| index.name.as_ref())
.filter(|_| indexes.len() == 1)
.inspect(|index| {
debug!("Pinning all requirements to index: `{index}`");
});
let mut modified = false;
let use_workspace = match workspace {
Some(workspace) => workspace,
None => {
if let AddTarget::Project(ref project, _) = target {
let workspace_root = project.workspace().install_path();
requirements.iter().any(|req| {
if let RequirementSource::Directory { install_path, .. } = &req.source {
let absolute_path = if install_path.is_absolute() {
install_path.to_path_buf()
} else {
project.root().join(install_path)
};
absolute_path.starts_with(workspace_root)
} else {
false
}
})
} else {
false
}
}
};
if use_workspace {
let AddTarget::Project(project, python_target) = target else {
unreachable!("`--workspace` and `--script` are conflicting options");
};
let mut toml = PyProjectTomlMut::from_toml(
&project.workspace().pyproject_toml().raw,
DependencyTarget::PyProjectToml,
)?;
for requirement in &requirements {
if let RequirementSource::Directory { install_path, .. } = &requirement.source {
let absolute_path = if install_path.is_absolute() {
install_path.to_path_buf()
} else {
project.root().join(install_path)
};
let use_workspace = workspace.unwrap_or_else(|| {
absolute_path.starts_with(project.workspace().install_path())
});
if !use_workspace {
continue;
}
if project.workspace().includes(&absolute_path)? {
continue;
}
let relative_path = absolute_path
.strip_prefix(project.workspace().install_path())
.unwrap_or(&absolute_path);
toml.add_workspace(relative_path)?;
modified |= true;
writeln!(
printer.stderr(),
"Added `{}` to workspace members",
relative_path.user_display().cyan()
)?;
}
}
target = if modified {
let workspace_content = toml.to_string();
fs_err::write(
project.workspace().install_path().join("pyproject.toml"),
&workspace_content,
)?;
AddTarget::Project(
VirtualProject::discover(
project.root(),
&DiscoveryOptions::default(),
&WorkspaceCache::default(),
)
.await?,
python_target,
)
} else {
AddTarget::Project(project, python_target)
}
}
let mut toml = match &target {
AddTarget::Script(script, _) => {
PyProjectTomlMut::from_toml(&script.metadata.raw, DependencyTarget::Script)
}
AddTarget::Project(project, _) => PyProjectTomlMut::from_toml(
&project.pyproject_toml().raw,
DependencyTarget::PyProjectToml,
),
}?;
let edits = edits(
requirements,
&target,
editable,
&dependency_type,
raw,
rev.as_deref(),
tag.as_deref(),
branch.as_deref(),
lfs,
&extras_of_dependency,
index,
&mut toml,
)?;
if edits.is_empty() {
match &dependency_type {
DependencyType::Group(group) => {
toml.ensure_dependency_group(group)?;
}
DependencyType::Optional(extra) => {
toml.ensure_optional_dependency(extra)?;
}
_ => {}
}
}
let mut valid_indexes = Vec::with_capacity(indexes.len());
for index in indexes {
if let IndexUrl::Path(url) = &index.url {
let path = url
.to_file_path()
.map_err(|()| anyhow::anyhow!("Invalid file path in index URL: {url}"))?;
if !path.is_dir() {
bail!("Directory not found for index: {url}");
}
if fs_err::read_dir(&path)?.next().is_none() {
warn_user_once!("Index directory `{url}` is empty, skipping");
continue;
}
}
valid_indexes.push(index);
}
let indexes = valid_indexes;
if !raw {
let urls = IndexUrls::from_indexes(indexes);
let mut indexes = urls.defined_indexes().collect::<Vec<_>>();
indexes.reverse();
for index in indexes {
toml.add_index(index)?;
}
}
let content = toml.to_string();
modified |= target.write(&content)?;
if frozen.is_some() {
return Ok(ExitStatus::Success);
}
let dry_run = if let AddTarget::Script(ref script, _) = target {
!LockTarget::from(script).lock_path().is_file()
} else {
false
};
let target = target.update(&content)?;
let _ = ctrlc::set_handler({
let snapshot = snapshot.clone();
move || {
if modified {
let _ = snapshot.revert();
}
#[expect(clippy::exit, clippy::cast_possible_wrap)]
std::process::exit(if cfg!(windows) {
0xC000_013A_u32 as i32
} else {
130
});
}
});
let lock_state = state.fork();
let sync_state = state;
match Box::pin(lock_and_sync(
target,
&mut toml,
&edits,
lock_state,
sync_state,
lock_check,
no_install_project,
only_install_project,
no_install_workspace,
only_install_workspace,
no_install_local,
only_install_local,
no_install_package.clone(),
only_install_package.clone(),
&defaulted_extras,
&defaulted_groups,
raw,
bounds,
dry_run,
constraints,
&settings,
&client_builder,
installer_metadata,
&concurrency,
cache,
printer,
preview,
))
.await
{
Ok(()) => Ok(ExitStatus::Success),
Err(err) => {
if modified {
let _ = snapshot.revert();
}
match err {
ProjectError::Operation(err) => diagnostics::OperationDiagnostic::with_system_certs(client_builder.system_certs()).with_hint(format!("If you want to add the package regardless of the failed resolution, provide the `{}` flag to skip locking and syncing.", "--frozen".green()))
.report(err)
.map_or(Ok(ExitStatus::Failure), |err| Err(err.into())),
err => Err(err.into()),
}
}
}
}
fn edits(
requirements: Vec<Requirement>,
target: &AddTarget,
editable: Option<bool>,
dependency_type: &DependencyType,
raw: bool,
rev: Option<&str>,
tag: Option<&str>,
branch: Option<&str>,
lfs: GitLfsSetting,
extras: &[ExtraName],
index: Option<&IndexName>,
toml: &mut PyProjectTomlMut,
) -> Result<Vec<DependencyEdit>> {
let mut edits = Vec::<DependencyEdit>::with_capacity(requirements.len());
for mut requirement in requirements {
let mut ex = requirement.extras.to_vec();
ex.extend(extras.iter().cloned());
ex.sort_unstable();
ex.dedup();
requirement.extras = ex.into_boxed_slice();
let (requirement, source) = match target {
AddTarget::Script(_, _) | AddTarget::Project(_, _) if raw => {
(uv_pep508::Requirement::from(requirement), None)
}
AddTarget::Script(script, _) => {
let script_path = std::path::absolute(&script.path)?;
let script_dir = script_path.parent().expect("script path has no parent");
let existing_sources = Some(script.sources());
resolve_requirement(
requirement,
false,
editable,
index.cloned(),
rev.map(ToString::to_string),
tag.map(ToString::to_string),
branch.map(ToString::to_string),
lfs,
script_dir,
existing_sources,
)?
}
AddTarget::Project(project, _) => {
let existing_sources = project
.pyproject_toml()
.tool
.as_ref()
.and_then(|tool| tool.uv.as_ref())
.and_then(|uv| uv.sources.as_ref())
.map(ToolUvSources::inner);
let is_workspace_member = project
.workspace()
.packages()
.contains_key(&requirement.name);
resolve_requirement(
requirement,
is_workspace_member,
editable,
index.cloned(),
rev.map(ToString::to_string),
tag.map(ToString::to_string),
branch.map(ToString::to_string),
lfs,
project.root(),
existing_sources,
)?
}
};
let source = match source {
Some(Source::Git {
mut git,
subdirectory,
rev,
tag,
branch,
lfs,
marker,
extra,
group,
}) => {
let credentials = uv_auth::Credentials::from_url(&git);
if let Some(credentials) = credentials {
debug!("Caching credentials for: {git}");
GIT_STORE.insert(RepositoryUrl::new(&git), credentials);
git.remove_credentials();
}
Some(Source::Git {
git,
subdirectory,
rev,
tag,
branch,
lfs,
marker,
extra,
group,
})
}
_ => source,
};
let dependency_type = match &dependency_type {
DependencyType::Dev => {
let existing = toml.find_dependency(&requirement.name, None);
if existing.iter().any(|dependency_type| matches!(dependency_type, DependencyType::Group(group) if group == &*DEV_DEPENDENCIES)) {
DependencyType::Group(DEV_DEPENDENCIES.clone())
} else if existing.iter().any(|dependency_type| matches!(dependency_type, DependencyType::Dev)) {
DependencyType::Dev
} else {
match (toml.has_dev_dependencies(), toml.has_dependency_group(&DEV_DEPENDENCIES)) {
(true, false) => DependencyType::Dev,
(false, true) => DependencyType::Group(DEV_DEPENDENCIES.clone()),
(true, true) => DependencyType::Group(DEV_DEPENDENCIES.clone()),
(false, false) => DependencyType::Group(DEV_DEPENDENCIES.clone()),
}
}
}
DependencyType::Group(group) if group == &*DEV_DEPENDENCIES => {
let existing = toml.find_dependency(&requirement.name, None);
if existing.iter().any(|dependency_type| matches!(dependency_type, DependencyType::Group(group) if group == &*DEV_DEPENDENCIES)) {
DependencyType::Group(DEV_DEPENDENCIES.clone())
} else if existing.iter().any(|dependency_type| matches!(dependency_type, DependencyType::Dev)) {
DependencyType::Dev
} else {
DependencyType::Group(DEV_DEPENDENCIES.clone())
}
}
DependencyType::Production => DependencyType::Production,
DependencyType::Optional(extra) => DependencyType::Optional(extra.clone()),
DependencyType::Group(group) => DependencyType::Group(group.clone()),
};
let edit = match &dependency_type {
DependencyType::Production => {
toml.add_dependency(&requirement, source.as_ref(), raw)?
}
DependencyType::Dev => toml.add_dev_dependency(&requirement, source.as_ref(), raw)?,
DependencyType::Optional(extra) => {
toml.add_optional_dependency(extra, &requirement, source.as_ref(), raw)?
}
DependencyType::Group(group) => {
toml.add_dependency_group_requirement(group, &requirement, source.as_ref(), raw)?
}
};
if let ArrayEdit::Add(index) = &edit {
for edit in &mut edits {
if edit.dependency_type == dependency_type {
match &mut edit.edit {
ArrayEdit::Add(existing) => {
if *existing >= *index {
*existing += 1;
}
}
ArrayEdit::Update(existing) => {
if *existing >= *index {
*existing += 1;
}
}
}
}
}
}
edits.push(DependencyEdit {
dependency_type,
requirement,
source,
edit,
});
}
Ok(edits)
}
#[expect(clippy::fn_params_excessive_bools)]
async fn lock_and_sync(
mut target: AddTarget,
toml: &mut PyProjectTomlMut,
edits: &[DependencyEdit],
lock_state: UniversalState,
sync_state: PlatformState,
lock_check: LockCheck,
no_install_project: bool,
only_install_project: bool,
no_install_workspace: bool,
only_install_workspace: bool,
no_install_local: bool,
only_install_local: bool,
no_install_package: Vec<PackageName>,
only_install_package: Vec<PackageName>,
extras: &ExtrasSpecificationWithDefaults,
groups: &DependencyGroupsWithDefaults,
raw: bool,
bound_kind: Option<AddBoundsKind>,
dry_run: bool,
constraints: Vec<NameRequirementSpecification>,
settings: &ResolverInstallerSettings,
client_builder: &BaseClientBuilder<'_>,
installer_metadata: bool,
concurrency: &Concurrency,
cache: &Cache,
printer: Printer,
preview: Preview,
) -> Result<(), ProjectError> {
let mut lock = Box::pin(
project::lock::LockOperation::new(
if let LockCheck::Enabled(lock_check) = lock_check {
LockMode::Locked(target.interpreter(), lock_check)
} else if dry_run {
LockMode::DryRun(target.interpreter())
} else {
LockMode::Write(target.interpreter())
},
&settings.resolver,
client_builder,
&lock_state,
Box::new(DefaultResolveLogger),
concurrency,
cache,
&WorkspaceCache::default(),
printer,
preview,
)
.with_constraints(constraints)
.execute((&target).into()),
)
.await?
.into_lock();
if !raw {
let mut minimum_version =
FxHashMap::with_capacity_and_hasher(lock.packages().len(), FxBuildHasher);
for dist in lock.packages() {
let name = dist.name();
let Some(version) = dist.version() else {
continue;
};
match minimum_version.entry(name) {
Entry::Vacant(entry) => {
entry.insert(version);
}
Entry::Occupied(mut entry) => {
if version < *entry.get() {
entry.insert(version);
}
}
}
}
let mut modified = false;
for edit in edits {
let ArrayEdit::Add(index) = &edit.edit else {
continue;
};
if edit
.source
.as_ref()
.is_some_and(|source| !matches!(source, Source::Registry { .. }))
{
continue;
}
let is_empty = match edit.requirement.version_or_url.as_ref() {
Some(VersionOrUrl::VersionSpecifier(version)) => version.is_empty(),
Some(VersionOrUrl::Url(_)) => false,
None => true,
};
if !is_empty {
if let Some(bound_kind) = bound_kind {
writeln!(
printer.stderr(),
"{} Using explicit requirement `{}` over bounds preference `{}`",
"note:".bold(),
edit.requirement,
bound_kind
)?;
}
continue;
}
let Some(minimum) = minimum_version.get(&edit.requirement.name) else {
continue;
};
let minimum = (*minimum).clone().without_local();
toml.set_dependency_bound(
&edit.dependency_type,
*index,
minimum,
bound_kind.unwrap_or_default(),
)?;
modified = true;
}
if modified {
let content = toml.to_string();
target.write(&content)?;
target = target.update(&content)?;
if let AddTarget::Project(VirtualProject::Project(ref project), _) = target {
let url = DisplaySafeUrl::from_file_path(project.project_root())
.expect("project root is a valid URL");
let distribution_id = url.distribution_id();
let existing = lock_state.index().distributions().remove(&distribution_id);
debug_assert!(existing.is_some(), "distribution should exist");
}
lock = Box::pin(
project::lock::LockOperation::new(
if let LockCheck::Enabled(lock_check) = lock_check {
LockMode::Locked(target.interpreter(), lock_check)
} else if dry_run {
LockMode::DryRun(target.interpreter())
} else {
LockMode::Write(target.interpreter())
},
&settings.resolver,
client_builder,
&lock_state,
Box::new(SummaryResolveLogger),
concurrency,
cache,
&WorkspaceCache::default(),
printer,
preview,
)
.execute((&target).into()),
)
.await?
.into_lock();
}
}
let AddTarget::Project(project, environment) = target else {
return Ok(());
};
let PythonTarget::Environment(venv) = &*environment else {
return Ok(());
};
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,
},
};
project::sync::do_sync(
target,
venv,
extras,
groups,
None,
InstallOptions::new(
no_install_project,
only_install_project,
no_install_workspace,
only_install_workspace,
no_install_local,
only_install_local,
no_install_package,
only_install_package,
),
Modifications::Sufficient,
None,
settings.into(),
client_builder,
&sync_state,
Box::new(DefaultInstallLogger),
installer_metadata,
concurrency,
cache,
&WorkspaceCache::default(),
DryRun::Disabled,
printer,
preview,
)
.await?;
Ok(())
}
fn resolve_requirement(
requirement: Requirement,
workspace: bool,
editable: Option<bool>,
index: Option<IndexName>,
rev: Option<String>,
tag: Option<String>,
branch: Option<String>,
lfs: GitLfsSetting,
root: &Path,
existing_sources: Option<&BTreeMap<PackageName, Sources>>,
) -> Result<(uv_pep508::Requirement, Option<Source>), anyhow::Error> {
let result = Source::from_requirement(
&requirement.name,
requirement.source.clone(),
workspace,
editable,
index,
rev,
tag,
branch,
lfs,
root,
existing_sources,
);
let source = match result {
Ok(source) => source,
Err(SourceError::UnresolvedReference(rev)) => {
bail!(
"Cannot resolve Git reference `{rev}` for requirement `{name}`. Specify the reference with one of `--tag`, `--branch`, or `--rev`, or use the `--raw-sources` flag.",
name = requirement.name
)
}
Err(err) => return Err(err.into()),
};
let mut processed_requirement = uv_pep508::Requirement::from(requirement);
processed_requirement.clear_url();
Ok((processed_requirement, source))
}
#[derive(Debug, Clone)]
#[expect(clippy::large_enum_variant)]
pub(super) enum PythonTarget {
Interpreter(Interpreter),
Environment(PythonEnvironment),
}
impl PythonTarget {
fn interpreter(&self) -> &Interpreter {
match self {
Self::Interpreter(interpreter) => interpreter,
Self::Environment(venv) => venv.interpreter(),
}
}
}
#[derive(Debug, Clone)]
#[expect(clippy::large_enum_variant)]
pub(super) enum AddTarget {
Script(Pep723Script, Box<Interpreter>),
Project(VirtualProject, Box<PythonTarget>),
}
impl<'lock> From<&'lock AddTarget> for LockTarget<'lock> {
fn from(value: &'lock AddTarget) -> Self {
match value {
AddTarget::Script(script, _) => Self::Script(script),
AddTarget::Project(project, _) => Self::Workspace(project.workspace()),
}
}
}
impl AddTarget {
pub(super) async fn acquire_lock(&self) -> Result<LockedFile, LockedFileError> {
match self {
Self::Script(_, interpreter) => interpreter.lock().await,
Self::Project(_, python_target) => python_target.interpreter().lock().await,
}
}
pub(super) fn interpreter(&self) -> &Interpreter {
match self {
Self::Script(_, interpreter) => interpreter,
Self::Project(_, venv) => venv.interpreter(),
}
}
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, interpreter) => {
script.metadata = Pep723Metadata::from_str(content)
.map_err(ProjectError::Pep723ScriptTomlParse)?;
Ok(Self::Script(script, interpreter))
}
Self::Project(project, venv) => {
let project = project
.update_member(
toml::from_str(content).map_err(ProjectError::PyprojectTomlParse)?,
)?
.ok_or(ProjectError::PyprojectTomlUpdate)?;
Ok(Self::Project(project, venv))
}
}
}
async fn snapshot(&self) -> Result<AddTargetSnapshot, io::Error> {
let target = match self {
Self::Script(script, _) => LockTarget::from(script),
Self::Project(project, _) => LockTarget::Workspace(project.workspace()),
};
let lock = target.read_bytes().await?;
match self {
Self::Script(script, _) => Ok(AddTargetSnapshot::Script(script.clone(), lock)),
Self::Project(project, _) => Ok(AddTargetSnapshot::Project(project.clone(), lock)),
}
}
}
#[derive(Debug, Clone)]
#[expect(clippy::large_enum_variant)]
enum AddTargetSnapshot {
Script(Pep723Script, Option<Vec<u8>>),
Project(VirtualProject, Option<Vec<u8>>),
}
impl AddTargetSnapshot {
fn revert(&self) -> Result<(), io::Error> {
match self {
Self::Script(script, lock) => {
debug!("Reverting changes to PEP 723 script block");
script.write(&script.metadata.raw)?;
let target = LockTarget::from(script);
if let Some(lock) = lock {
debug!("Reverting changes to `uv.lock`");
fs_err::write(target.lock_path(), lock)?;
} else {
debug!("Removing `uv.lock`");
fs_err::remove_file(target.lock_path())?;
}
Ok(())
}
Self::Project(project, lock) => {
let workspace = project.workspace();
if workspace.install_path() != project.root() {
debug!("Reverting changes to workspace `pyproject.toml`");
fs_err::write(
workspace.install_path().join("pyproject.toml"),
workspace.pyproject_toml().as_ref(),
)?;
}
debug!("Reverting changes to `pyproject.toml`");
fs_err::write(
project.root().join("pyproject.toml"),
project.pyproject_toml().as_ref(),
)?;
let target = LockTarget::from(project.workspace());
if let Some(lock) = lock {
debug!("Reverting changes to `uv.lock`");
fs_err::write(target.lock_path(), lock)?;
} else {
debug!("Removing `uv.lock`");
fs_err::remove_file(target.lock_path())?;
}
Ok(())
}
}
}
}
#[derive(Debug, Clone)]
struct DependencyEdit {
dependency_type: DependencyType,
requirement: uv_pep508::Requirement,
source: Option<Source>,
edit: ArrayEdit,
}