use std::borrow::Cow; use std::path::Path; use std::str::FromStr; use std::sync::Arc; use configparser::ini::Ini; use futures::{TryStreamExt, stream::FuturesOrdered}; use serde::Deserialize; use tracing::debug; use url::Host; use uv_distribution::{DistributionDatabase, Reporter}; use uv_distribution_filename::{DistExtension, SourceDistFilename, WheelFilename}; use uv_distribution_types::{ BuildableSource, DirectSourceUrl, DirectorySourceUrl, GitSourceUrl, PathSourceUrl, RemoteSource, Requirement, SourceUrl, VersionId, }; use uv_normalize::PackageName; use uv_pep508::{UnnamedRequirement, VersionOrUrl}; use uv_pypi_types::Metadata10; use uv_pypi_types::{ParsedUrl, VerbatimParsedUrl}; use uv_resolver::{InMemoryIndex, MetadataResponse}; use uv_types::{BuildContext, HashStrategy}; use crate::Error; /// Like [`RequirementsSpecification`], but with concrete names for all requirements. pub struct NamedRequirementsResolver<'a, Context: BuildContext> { /// Whether to check hashes for distributions. hasher: &'a HashStrategy, /// The in-memory index for resolving dependencies. index: &'a InMemoryIndex, /// The database for fetching and building distributions. database: DistributionDatabase<'a, Context>, } impl<'a, Context: BuildContext> NamedRequirementsResolver<'a, Context> { /// Instantiate a new [`NamedRequirementsResolver`]. pub fn new( hasher: &'a HashStrategy, index: &'a InMemoryIndex, database: DistributionDatabase<'a, Context>, ) -> Self { Self { hasher, index, database, } } /// Set the [`Reporter`] to use for this resolver. #[must_use] pub fn with_reporter(self, reporter: Arc) -> Self { Self { database: self.database.with_reporter(reporter), ..self } } /// Resolve any unnamed requirements in the specification. pub async fn resolve( self, requirements: impl Iterator>, ) -> Result, Error> { let Self { hasher, index, database, } = self; requirements .map(async |requirement| { Self::resolve_requirement(requirement, hasher, index, &database) .await .map(Requirement::from) }) .collect::>() .try_collect() .await } /// Infer the package name for a given "unnamed" requirement. async fn resolve_requirement( requirement: UnnamedRequirement, hasher: &HashStrategy, index: &InMemoryIndex, database: &DistributionDatabase<'a, Context>, ) -> Result, Error> { // If the requirement is a wheel, extract the package name from the wheel filename. // // Ex) `anyio-4.3.0-py3-none-any.whl` if Path::new(requirement.url.verbatim.path()) .extension() .is_some_and(|ext| ext.eq_ignore_ascii_case("whl")) { let filename = WheelFilename::from_str(&requirement.url.verbatim.filename()?)?; return Ok(uv_pep508::Requirement { name: filename.name, extras: requirement.extras, version_or_url: Some(VersionOrUrl::Url(requirement.url)), marker: requirement.marker, origin: requirement.origin, }); } // If the requirement is a source archive, try to extract the package name from the archive // filename. This isn't guaranteed to work. // // Ex) `anyio-4.3.0.tar.gz` if let Some(filename) = requirement .url .verbatim .filename() .ok() .and_then(|filename| SourceDistFilename::parsed_normalized_filename(&filename).ok()) { // But ignore GitHub archives, like: // https://github.com/python/mypy/archive/refs/heads/release-1.11.zip // // These have auto-generated filenames that will almost never match the package name. if requirement.url.verbatim.host() == Some(Host::Domain("github.com")) && requirement .url .verbatim .path_segments() .is_some_and(|mut path_segments| { path_segments.any(|segment| segment == "archive") }) { debug!( "Rejecting inferred name from GitHub archive: {}", requirement.url.verbatim ); } else { return Ok(uv_pep508::Requirement { name: filename.name, extras: requirement.extras, version_or_url: Some(VersionOrUrl::Url(requirement.url)), marker: requirement.marker, origin: requirement.origin, }); } } let source = match &requirement.url.parsed_url { // If the path points to a directory, attempt to read the name from static metadata. ParsedUrl::Directory(parsed_directory_url) => { // Attempt to read a `PKG-INFO` from the directory. if let Some(metadata) = fs_err::read(parsed_directory_url.install_path.join("PKG-INFO")) .ok() .and_then(|contents| Metadata10::parse_pkg_info(&contents).ok()) { debug!( "Found PKG-INFO metadata for {path} ({name})", path = parsed_directory_url.install_path.display(), name = metadata.name ); return Ok(uv_pep508::Requirement { name: metadata.name, extras: requirement.extras, version_or_url: Some(VersionOrUrl::Url(requirement.url)), marker: requirement.marker, origin: requirement.origin, }); } // Attempt to read a `pyproject.toml` file. let project_path = parsed_directory_url.install_path.join("pyproject.toml"); if let Some(pyproject) = fs_err::read_to_string(project_path) .ok() .and_then(|contents| toml::from_str::(&contents).ok()) { // Read PEP 621 metadata from the `pyproject.toml`. if let Some(project) = pyproject.project { debug!( "Found PEP 621 metadata for {path} in `pyproject.toml` ({name})", path = parsed_directory_url.install_path.display(), name = project.name ); return Ok(uv_pep508::Requirement { name: project.name, extras: requirement.extras, version_or_url: Some(VersionOrUrl::Url(requirement.url)), marker: requirement.marker, origin: requirement.origin, }); } // Read Poetry-specific metadata from the `pyproject.toml`. if let Some(tool) = pyproject.tool { if let Some(poetry) = tool.poetry { if let Some(name) = poetry.name { debug!( "Found Poetry metadata for {path} in `pyproject.toml` ({name})", path = parsed_directory_url.install_path.display(), name = name ); return Ok(uv_pep508::Requirement { name, extras: requirement.extras, version_or_url: Some(VersionOrUrl::Url(requirement.url)), marker: requirement.marker, origin: requirement.origin, }); } } } } // Attempt to read a `setup.cfg` from the directory. if let Some(setup_cfg) = fs_err::read_to_string(parsed_directory_url.install_path.join("setup.cfg")) .ok() .and_then(|contents| { let mut ini = Ini::new_cs(); ini.set_multiline(true); ini.read(contents).ok() }) { if let Some(section) = setup_cfg.get("metadata") { if let Some(Some(name)) = section.get("name") { if let Ok(name) = PackageName::from_str(name) { debug!( "Found setuptools metadata for {path} in `setup.cfg` ({name})", path = parsed_directory_url.install_path.display(), name = name ); return Ok(uv_pep508::Requirement { name, extras: requirement.extras, version_or_url: Some(VersionOrUrl::Url(requirement.url)), marker: requirement.marker, origin: requirement.origin, }); } } } } SourceUrl::Directory(DirectorySourceUrl { url: &requirement.url.verbatim, install_path: Cow::Borrowed(&parsed_directory_url.install_path), editable: parsed_directory_url.editable, }) } ParsedUrl::Path(parsed_path_url) => { let ext = match parsed_path_url.ext { DistExtension::Source(ext) => ext, DistExtension::Wheel => unreachable!(), }; SourceUrl::Path(PathSourceUrl { url: &requirement.url.verbatim, path: Cow::Borrowed(&parsed_path_url.install_path), ext, }) } ParsedUrl::Archive(parsed_archive_url) => { let ext = match parsed_archive_url.ext { DistExtension::Source(ext) => ext, DistExtension::Wheel => unreachable!(), }; SourceUrl::Direct(DirectSourceUrl { url: &parsed_archive_url.url, subdirectory: parsed_archive_url.subdirectory.as_deref(), ext, }) } ParsedUrl::Git(parsed_git_url) => SourceUrl::Git(GitSourceUrl { url: &requirement.url.verbatim, git: &parsed_git_url.url, subdirectory: parsed_git_url.subdirectory.as_deref(), }), }; // Fetch the metadata for the distribution. let name = { let id = VersionId::from_url(source.url()); if let Some(archive) = index .distributions() .get(&id) .as_deref() .and_then(|response| { if let MetadataResponse::Found(archive) = response { Some(archive) } else { None } }) { // If the metadata is already in the index, return it. archive.metadata.name.clone() } else { // Run the PEP 517 build process to extract metadata from the source distribution. let hashes = hasher.get_url(source.url()); let source = BuildableSource::Url(source); let archive = database.build_wheel_metadata(&source, hashes).await?; let name = archive.metadata.name.clone(); // Insert the metadata into the index. index .distributions() .done(id, Arc::new(MetadataResponse::Found(archive))); name } }; Ok(uv_pep508::Requirement { name, extras: requirement.extras, version_or_url: Some(VersionOrUrl::Url(requirement.url)), marker: requirement.marker, origin: requirement.origin, }) } } /// A pyproject.toml as specified in PEP 517. #[derive(Deserialize, Debug)] #[serde(rename_all = "kebab-case")] struct PyProjectToml { project: Option, tool: Option, } #[derive(Deserialize, Debug)] #[serde(rename_all = "kebab-case")] struct Project { name: PackageName, } #[derive(Deserialize, Debug)] #[serde(rename_all = "kebab-case")] struct Tool { poetry: Option, } #[derive(Deserialize, Debug)] #[serde(rename_all = "kebab-case")] struct ToolPoetry { name: Option, }