use std::borrow::Cow;
use std::fmt::Formatter;
use std::path::{Component, Path, PathBuf};
use owo_colors::OwoColorize;
use url::Url;
use uv_configuration::{
DependencyGroupsWithDefaults, EditableMode, ExtrasSpecificationWithDefaults, InstallOptions,
};
use uv_distribution_filename::{DistExtension, SourceDistExtension};
use uv_fs::Simplified;
use uv_git_types::GitReference;
use uv_normalize::PackageName;
use uv_pypi_types::{ParsedArchiveUrl, ParsedGitUrl};
use uv_redacted::DisplaySafeUrl;
use crate::lock::export::{ExportableRequirement, ExportableRequirements};
use crate::lock::{Package, PackageId, Source};
use crate::{Installable, LockError};
#[derive(Debug)]
pub struct RequirementsTxtExport<'lock> {
nodes: Vec<ExportableRequirement<'lock>>,
hashes: bool,
editable: Option<EditableMode>,
}
impl<'lock> RequirementsTxtExport<'lock> {
pub fn from_lock(
target: &impl Installable<'lock>,
prune: &[PackageName],
extras: &ExtrasSpecificationWithDefaults,
dev: &DependencyGroupsWithDefaults,
annotate: bool,
editable: Option<EditableMode>,
hashes: bool,
install_options: &'lock InstallOptions,
) -> Result<Self, LockError> {
let ExportableRequirements(mut nodes) = ExportableRequirements::from_lock(
target,
prune,
extras,
dev,
annotate,
install_options,
)?;
nodes.sort_unstable_by(|a, b| {
RequirementComparator::from(a.package).cmp(&RequirementComparator::from(b.package))
});
Ok(Self {
nodes,
hashes,
editable,
})
}
}
impl std::fmt::Display for RequirementsTxtExport<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
for ExportableRequirement {
package,
marker,
dependents,
} in &self.nodes
{
match &package.id.source {
Source::Registry(_) => {
let version = package
.id
.version
.as_ref()
.expect("registry package without version");
write!(f, "{}=={}", package.id.name, version)?;
}
Source::Git(url, git) => {
let mut url = url.to_url().map_err(|_| std::fmt::Error)?;
url.set_fragment(None);
url.set_query(None);
let git_url = uv_git_types::GitUrl::from_commit(
url,
GitReference::from(git.kind.clone()),
git.precise,
git.lfs,
)
.expect("Internal Git URLs must have supported schemes");
let url = DisplaySafeUrl::from(ParsedGitUrl {
url: git_url.clone(),
subdirectory: git.subdirectory.clone(),
});
write!(f, "{} @ {}", package.id.name, url)?;
}
Source::Direct(url, direct) => {
let url = DisplaySafeUrl::from(ParsedArchiveUrl {
url: url.to_url().map_err(|_| std::fmt::Error)?,
subdirectory: direct.subdirectory.clone(),
ext: DistExtension::Source(SourceDistExtension::TarGz),
});
write!(
f,
"{} @ {}",
package.id.name,
url.displayable_with_credentials()
)?;
}
Source::Path(path) | Source::Directory(path) => {
if path.is_absolute() {
write!(
f,
"{}",
Url::from_file_path(path).map_err(|()| std::fmt::Error)?
)?;
} else {
write!(f, "{}", anchor(path).portable_display())?;
}
}
Source::Editable(path) => match self.editable {
None | Some(EditableMode::Editable) => {
write!(f, "-e {}", anchor(path).portable_display())?;
}
Some(EditableMode::NonEditable) => {
if path.is_absolute() {
write!(
f,
"{}",
Url::from_file_path(path).map_err(|()| std::fmt::Error)?
)?;
} else {
write!(f, "{}", anchor(path).portable_display())?;
}
}
},
Source::Virtual(_) => {
continue;
}
}
if let Some(contents) = marker.contents() {
write!(f, " ; {contents}")?;
}
if self.hashes {
let mut hashes = package.hashes();
hashes.sort_unstable();
if !hashes.is_empty() {
for hash in hashes.iter() {
writeln!(f, " \\")?;
write!(f, " --hash=")?;
write!(f, "{hash}")?;
}
}
}
writeln!(f)?;
match dependents.as_slice() {
[] => {}
[dependent] => {
writeln!(f, "{}", format!(" # via {}", dependent.id.name).green())?;
}
_ => {
writeln!(f, "{}", " # via".green())?;
for &dependent in dependents {
writeln!(f, "{}", format!(" # {}", dependent.id.name).green())?;
}
}
}
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
enum RequirementComparator<'lock> {
Editable(&'lock Path),
Path(&'lock Path),
Package(&'lock PackageId),
}
impl<'lock> From<&'lock Package> for RequirementComparator<'lock> {
fn from(value: &'lock Package) -> Self {
match &value.id.source {
Source::Path(path) | Source::Directory(path) => Self::Path(path),
Source::Editable(path) => Self::Editable(path),
_ => Self::Package(&value.id),
}
}
}
fn anchor(path: &Path) -> Cow<'_, Path> {
match path.components().next() {
None => Cow::Owned(PathBuf::from(".")),
Some(Component::CurDir | Component::ParentDir) => Cow::Borrowed(path),
_ => Cow::Owned(PathBuf::from("./").join(path)),
}
}