use std::path::Path;
use anstream::print;
use anyhow::{Error, Result};
use futures::StreamExt;
use uv_cache::{Cache, Refresh};
use uv_cache_info::Timestamp;
use uv_client::{BaseClientBuilder, RegistryClientBuilder};
use uv_configuration::{Concurrency, DependencyGroups, TargetTriple};
use uv_distribution_types::IndexCapabilities;
use uv_normalize::DefaultGroups;
use uv_normalize::PackageName;
use uv_preview::Preview;
use uv_python::{PythonDownloads, PythonPreference, PythonRequest, PythonVersion};
use uv_resolver::{PackageMap, TreeDisplay};
use uv_scripts::Pep723Script;
use uv_settings::PythonInstallMirrors;
use uv_workspace::{DiscoveryOptions, Workspace, WorkspaceCache};
use crate::commands::pip::latest::LatestClient;
use crate::commands::pip::loggers::DefaultResolveLogger;
use crate::commands::pip::resolution_markers;
use crate::commands::project::lock::{LockMode, LockOperation};
use crate::commands::project::lock_target::LockTarget;
use crate::commands::project::{
ProjectError, ProjectInterpreter, ScriptInterpreter, UniversalState, WorkspacePython,
default_dependency_groups,
};
use crate::commands::reporters::LatestVersionReporter;
use crate::commands::{ExitStatus, diagnostics};
use crate::printer::Printer;
use crate::settings::FrozenSource;
use crate::settings::LockCheck;
use crate::settings::ResolverSettings;
#[expect(clippy::fn_params_excessive_bools)]
pub(crate) async fn tree(
project_dir: &Path,
groups: DependencyGroups,
lock_check: LockCheck,
frozen: Option<FrozenSource>,
universal: bool,
depth: u8,
prune: Vec<PackageName>,
package: Vec<PackageName>,
no_dedupe: bool,
invert: bool,
outdated: bool,
show_sizes: bool,
python_version: Option<PythonVersion>,
python_platform: Option<TargetTriple>,
python: Option<String>,
install_mirrors: PythonInstallMirrors,
settings: ResolverSettings,
client_builder: &BaseClientBuilder<'_>,
script: Option<Pep723Script>,
python_preference: PythonPreference,
python_downloads: PythonDownloads,
concurrency: Concurrency,
no_config: bool,
cache: &Cache,
printer: Printer,
preview: Preview,
) -> Result<ExitStatus> {
let workspace_cache = WorkspaceCache::default();
let workspace;
let target = if let Some(script) = script.as_ref() {
LockTarget::Script(script)
} else {
workspace =
Workspace::discover(project_dir, &DiscoveryOptions::default(), &workspace_cache)
.await?;
LockTarget::Workspace(&workspace)
};
let default_groups = match target {
LockTarget::Workspace(workspace) => default_dependency_groups(workspace.pyproject_toml())?,
LockTarget::Script(_) => DefaultGroups::default(),
};
let groups = groups.with_defaults(default_groups);
let interpreter = if frozen.is_some() && universal {
None
} else {
Some(match target {
LockTarget::Script(script) => ScriptInterpreter::discover(
script.into(),
python.as_deref().map(PythonRequest::parse),
client_builder,
python_preference,
python_downloads,
&install_mirrors,
false,
no_config,
Some(false),
cache,
printer,
preview,
)
.await?
.into_interpreter(),
LockTarget::Workspace(workspace) => {
let workspace_python = WorkspacePython::from_request(
python.as_deref().map(PythonRequest::parse),
Some(workspace),
&groups,
project_dir,
no_config,
)
.await?;
ProjectInterpreter::discover(
workspace,
&groups,
workspace_python,
client_builder,
python_preference,
python_downloads,
&install_mirrors,
false,
Some(false),
cache,
printer,
preview,
)
.await?
.into_interpreter()
}
})
};
let mode = if let Some(frozen_source) = frozen {
LockMode::Frozen(frozen_source.into())
} else if let LockCheck::Enabled(lock_check) = lock_check {
LockMode::Locked(interpreter.as_ref().unwrap(), lock_check)
} else if matches!(target, LockTarget::Script(_)) && !target.lock_path().is_file() {
LockMode::DryRun(interpreter.as_ref().unwrap())
} else {
LockMode::Write(interpreter.as_ref().unwrap())
};
let state = UniversalState::default();
let lock = match Box::pin(
LockOperation::new(
mode,
&settings,
client_builder,
&state,
Box::new(DefaultResolveLogger),
&concurrency,
cache,
&workspace_cache,
printer,
preview,
)
.execute(target),
)
.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 markers = (!universal).then(|| {
resolution_markers(
python_version.as_ref(),
python_platform.as_ref(),
interpreter.as_ref().unwrap(),
)
});
let latest = if outdated {
let packages = lock
.packages()
.iter()
.filter_map(|package| {
let index = match package.index(target.install_path()) {
Ok(Some(index)) => index,
Ok(None) => return None,
Err(err) => return Some(Err(err)),
};
Some(Ok((package, index)))
})
.collect::<Result<Vec<_>, _>>()?;
if packages.is_empty() {
PackageMap::default()
} else {
let ResolverSettings {
index_locations,
index_strategy: _,
keyring_provider,
resolution: _,
prerelease: _,
fork_strategy: _,
dependency_metadata: _,
config_setting: _,
config_settings_package: _,
build_isolation: _,
extra_build_dependencies: _,
extra_build_variables: _,
exclude_newer: _,
link_mode: _,
upgrade: _,
build_options: _,
sources: _,
torch_backend: _,
} = &settings;
let capabilities = IndexCapabilities::default();
let client = RegistryClientBuilder::new(
client_builder.clone(),
cache.clone().with_refresh(Refresh::All(Timestamp::now())),
)
.index_locations(index_locations.clone())
.keyring(*keyring_provider)
.build()?;
let download_concurrency = concurrency.downloads_semaphore.clone();
let exclude_newer = lock.exclude_newer();
let client = LatestClient {
client: &client,
capabilities: &capabilities,
prerelease: lock.prerelease_mode(),
exclude_newer: &exclude_newer,
index_locations,
requires_python: Some(lock.requires_python()),
tags: None,
};
let reporter = LatestVersionReporter::from(printer).with_length(packages.len() as u64);
let download_concurrency = &download_concurrency;
let mut fetches = futures::stream::iter(packages)
.map(async |(package, index)| {
let Some(filename) = client
.find_latest(package.name(), Some(&index), download_concurrency)
.await?
else {
return Ok(None);
};
Ok::<Option<_>, Error>(Some((package, filename.into_version())))
})
.buffer_unordered(concurrency.downloads);
let mut map = PackageMap::default();
while let Some(entry) = fetches.next().await.transpose()? {
let Some((package, version)) = entry else {
reporter.on_fetch_progress();
continue;
};
reporter.on_fetch_version(package.name(), &version);
if package.version().is_some_and(|package| version > *package) {
map.insert(package.clone(), version);
}
}
reporter.on_fetch_complete();
map
}
} else {
PackageMap::default()
};
let tree = TreeDisplay::new(
&lock,
markers.as_ref(),
&latest,
depth.into(),
&prune,
&package,
&groups,
no_dedupe,
invert,
show_sizes,
);
print!("{tree}");
Ok(ExitStatus::Success)
}