Remove unused `requirements.rs` file (#2603)

This was accidentally duplicated in a rebase.
This commit is contained in:
Charlie Marsh 2024-03-21 22:21:36 -04:00 committed by GitHub
parent 7d285148b2
commit 12192dd872
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
1 changed files with 0 additions and 744 deletions

View File

@ -1,744 +0,0 @@
//! A standard interface for working with heterogeneous sources of requirements.
use std::path::{Path, PathBuf};
use std::str::FromStr;
use anyhow::{Context, Result};
use configparser::ini::Ini;
use console::Term;
use futures::{StreamExt, TryStreamExt};
use indexmap::IndexMap;
use once_cell::sync::Lazy;
use regex::Regex;
use rustc_hash::FxHashSet;
use serde::Deserialize;
use tracing::{debug, instrument, Level};
use distribution_filename::{SourceDistFilename, WheelFilename};
use distribution_types::{FlatIndexLocation, IndexUrl, RemoteSource};
use pep508_rs::{Requirement, RequirementsTxtRequirement, UnnamedRequirement, VersionOrUrl};
use pypi_types::Metadata10;
use requirements_txt::{EditableRequirement, FindLink, RequirementsTxt};
use uv_cache::Cache;
use uv_client::{BaseClientBuilder, Connectivity};
use uv_distribution::download_and_extract_archive;
use uv_fs::Simplified;
use uv_normalize::{ExtraName, PackageName};
use uv_resolver::{Preference, PreferenceError};
use uv_warnings::warn_user;
use crate::commands::Upgrade;
use crate::confirm;
#[derive(Debug)]
pub(crate) enum RequirementsSource {
/// A package was provided on the command line (e.g., `pip install flask`).
Package(String),
/// An editable path was provided on the command line (e.g., `pip install -e ../flask`).
Editable(String),
/// Dependencies were provided via a `requirements.txt` file (e.g., `pip install -r requirements.txt`).
RequirementsTxt(PathBuf),
/// Dependencies were provided via a `pyproject.toml` file (e.g., `pip-compile pyproject.toml`).
PyprojectToml(PathBuf),
}
impl RequirementsSource {
/// Parse a [`RequirementsSource`] from a [`PathBuf`].
pub(crate) fn from_path(path: PathBuf) -> Self {
if path.ends_with("pyproject.toml") {
Self::PyprojectToml(path)
} else {
Self::RequirementsTxt(path)
}
}
/// Parse a [`RequirementsSource`] from a user-provided string, assumed to be a package.
///
/// If the user provided a value that appears to be a `requirements.txt` file or a local
/// directory, prompt them to correct it (if the terminal is interactive).
pub(crate) fn from_package(name: String) -> Self {
// If the user provided a `requirements.txt` file without `-r` (as in
// `uv pip install requirements.txt`), prompt them to correct it.
#[allow(clippy::case_sensitive_file_extension_comparisons)]
if (name.ends_with(".txt") || name.ends_with(".in")) && Path::new(&name).is_file() {
let term = Term::stderr();
if term.is_term() {
let prompt = format!(
"`{name}` looks like a requirements file but was passed as a package name. Did you mean `-r {name}`?"
);
let confirmation = confirm::confirm(&prompt, &term, true).unwrap();
if confirmation {
return Self::RequirementsTxt(name.into());
}
}
}
Self::Package(name)
}
}
impl std::fmt::Display for RequirementsSource {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Editable(path) => write!(f, "-e {path}"),
Self::RequirementsTxt(path) | Self::PyprojectToml(path) => {
write!(f, "{}", path.display())
}
Self::Package(package) => write!(f, "{package}"),
}
}
}
#[derive(Debug, Default, Clone)]
pub(crate) enum ExtrasSpecification<'a> {
#[default]
None,
All,
Some(&'a [ExtraName]),
}
impl ExtrasSpecification<'_> {
/// Returns true if a name is included in the extra specification.
fn contains(&self, name: &ExtraName) -> bool {
match self {
ExtrasSpecification::All => true,
ExtrasSpecification::None => false,
ExtrasSpecification::Some(extras) => extras.contains(name),
}
}
}
#[derive(Debug, Default)]
pub(crate) struct RequirementsSpecification {
/// The name of the project specifying requirements.
pub(crate) project: Option<PackageName>,
/// The requirements for the project.
pub(crate) requirements: Vec<RequirementsTxtRequirement>,
/// The constraints for the project.
pub(crate) constraints: Vec<Requirement>,
/// The overrides for the project.
pub(crate) overrides: Vec<Requirement>,
/// Package to install as editable installs
pub(crate) editables: Vec<EditableRequirement>,
/// The extras used to collect requirements.
pub(crate) extras: FxHashSet<ExtraName>,
/// The index URL to use for fetching packages.
pub(crate) index_url: Option<IndexUrl>,
/// The extra index URLs to use for fetching packages.
pub(crate) extra_index_urls: Vec<IndexUrl>,
/// Whether to disallow index usage.
pub(crate) no_index: bool,
/// The `--find-links` locations to use for fetching packages.
pub(crate) find_links: Vec<FlatIndexLocation>,
}
impl RequirementsSpecification {
/// Read the requirements and constraints from a source.
#[instrument(skip_all, level = Level::DEBUG, fields(source = % source))]
pub(crate) async fn from_source(
source: &RequirementsSource,
extras: &ExtrasSpecification<'_>,
client_builder: &BaseClientBuilder<'_>,
) -> Result<Self> {
Ok(match source {
RequirementsSource::Package(name) => {
let requirement = RequirementsTxtRequirement::parse(name, std::env::current_dir()?)
.with_context(|| format!("Failed to parse `{name}`"))?;
Self {
project: None,
requirements: vec![requirement],
constraints: vec![],
overrides: vec![],
editables: vec![],
extras: FxHashSet::default(),
index_url: None,
extra_index_urls: vec![],
no_index: false,
find_links: vec![],
}
}
RequirementsSource::Editable(name) => {
let requirement = EditableRequirement::parse(name, std::env::current_dir()?)
.with_context(|| format!("Failed to parse `{name}`"))?;
Self {
project: None,
requirements: vec![],
constraints: vec![],
overrides: vec![],
editables: vec![requirement],
extras: FxHashSet::default(),
index_url: None,
extra_index_urls: vec![],
no_index: false,
find_links: vec![],
}
}
RequirementsSource::RequirementsTxt(path) => {
let requirements_txt =
RequirementsTxt::parse(path, std::env::current_dir()?, client_builder).await?;
Self {
project: None,
requirements: requirements_txt
.requirements
.into_iter()
.map(|entry| entry.requirement)
.collect(),
constraints: requirements_txt.constraints,
editables: requirements_txt.editables,
overrides: vec![],
extras: FxHashSet::default(),
index_url: requirements_txt.index_url.map(IndexUrl::from),
extra_index_urls: requirements_txt
.extra_index_urls
.into_iter()
.map(IndexUrl::from)
.collect(),
no_index: requirements_txt.no_index,
find_links: requirements_txt
.find_links
.into_iter()
.map(|link| match link {
FindLink::Url(url) => FlatIndexLocation::Url(url),
FindLink::Path(path) => FlatIndexLocation::Path(path),
})
.collect(),
}
}
RequirementsSource::PyprojectToml(path) => {
let contents = uv_fs::read_to_string(path).await?;
let pyproject_toml = toml::from_str::<pyproject_toml::PyProjectToml>(&contents)
.with_context(|| format!("Failed to parse `{}`", path.user_display()))?;
let mut used_extras = FxHashSet::default();
let mut requirements = Vec::new();
let mut project_name = None;
if let Some(project) = pyproject_toml.project {
// Parse the project name.
let parsed_project_name =
PackageName::new(project.name).with_context(|| {
format!("Invalid `project.name` in {}", path.user_display())
})?;
// Include the default dependencies.
requirements.extend(project.dependencies.unwrap_or_default());
// Include any optional dependencies specified in `extras`.
if !matches!(extras, ExtrasSpecification::None) {
if let Some(optional_dependencies) = project.optional_dependencies {
for (extra_name, optional_requirements) in &optional_dependencies {
// TODO(konstin): It's not ideal that pyproject-toml doesn't use
// `ExtraName`
let normalized_name = ExtraName::from_str(extra_name)?;
if extras.contains(&normalized_name) {
used_extras.insert(normalized_name);
requirements.extend(flatten_extra(
&parsed_project_name,
optional_requirements,
&optional_dependencies,
)?);
}
}
}
}
project_name = Some(parsed_project_name);
}
if requirements.is_empty()
&& pyproject_toml.build_system.is_some_and(|build_system| {
build_system
.requires
.iter()
.any(|v| v.name.as_dist_info_name().starts_with("poetry"))
})
{
warn_user!("`{}` does not contain any dependencies (hint: specify dependencies in the `project.dependencies` section; `tool.poetry.dependencies` is not currently supported)", path.user_display());
}
Self {
project: project_name,
requirements: requirements
.into_iter()
.map(RequirementsTxtRequirement::Pep508)
.collect(),
constraints: vec![],
overrides: vec![],
editables: vec![],
extras: used_extras,
index_url: None,
extra_index_urls: vec![],
no_index: false,
find_links: vec![],
}
}
})
}
/// Read the combined requirements and constraints from a set of sources.
pub(crate) async fn from_sources(
requirements: &[RequirementsSource],
constraints: &[RequirementsSource],
overrides: &[RequirementsSource],
extras: &ExtrasSpecification<'_>,
client_builder: &BaseClientBuilder<'_>,
) -> Result<Self> {
let mut spec = Self::default();
// Read all requirements, and keep track of all requirements _and_ constraints.
// A `requirements.txt` can contain a `-c constraints.txt` directive within it, so reading
// a requirements file can also add constraints.
for source in requirements {
let source = Self::from_source(source, extras, client_builder).await?;
spec.requirements.extend(source.requirements);
spec.constraints.extend(source.constraints);
spec.overrides.extend(source.overrides);
spec.extras.extend(source.extras);
spec.editables.extend(source.editables);
// Use the first project name discovered.
if spec.project.is_none() {
spec.project = source.project;
}
if let Some(url) = source.index_url {
if let Some(existing) = spec.index_url {
return Err(anyhow::anyhow!(
"Multiple index URLs specified: `{existing}` vs.` {url}",
));
}
spec.index_url = Some(url);
}
spec.no_index |= source.no_index;
spec.extra_index_urls.extend(source.extra_index_urls);
spec.find_links.extend(source.find_links);
}
// Read all constraints, treating _everything_ as a constraint.
for source in constraints {
let source = Self::from_source(source, extras, &client_builder).await?;
for requirement in source.requirements {
match requirement {
RequirementsTxtRequirement::Pep508(requirement) => {
spec.constraints.push(requirement);
}
RequirementsTxtRequirement::Unnamed(requirement) => {
return Err(anyhow::anyhow!(
"Unnamed requirements are not allowed as constraints (found: `{requirement}`)"
));
}
}
}
spec.constraints.extend(source.constraints);
spec.constraints.extend(source.overrides);
if let Some(url) = source.index_url {
if let Some(existing) = spec.index_url {
return Err(anyhow::anyhow!(
"Multiple index URLs specified: `{existing}` vs.` {url}",
));
}
spec.index_url = Some(url);
}
spec.no_index |= source.no_index;
spec.extra_index_urls.extend(source.extra_index_urls);
spec.find_links.extend(source.find_links);
}
// Read all overrides, treating both requirements _and_ constraints as overrides.
for source in overrides {
let source = Self::from_source(source, extras, &client_builder).await?;
for requirement in source.requirements {
match requirement {
RequirementsTxtRequirement::Pep508(requirement) => {
spec.overrides.push(requirement);
}
RequirementsTxtRequirement::Unnamed(requirement) => {
return Err(anyhow::anyhow!(
"Unnamed requirements are not allowed as overrides (found: `{requirement}`)"
));
}
}
}
spec.overrides.extend(source.constraints);
spec.overrides.extend(source.overrides);
if let Some(url) = source.index_url {
if let Some(existing) = spec.index_url {
return Err(anyhow::anyhow!(
"Multiple index URLs specified: `{existing}` vs.` {url}",
));
}
spec.index_url = Some(url);
}
spec.no_index |= source.no_index;
spec.extra_index_urls.extend(source.extra_index_urls);
spec.find_links.extend(source.find_links);
}
Ok(spec)
}
/// Read the requirements from a set of sources.
pub(crate) async fn from_simple_sources(
requirements: &[RequirementsSource],
client_builder: &BaseClientBuilder<'_>,
) -> Result<Self> {
Self::from_sources(
requirements,
&[],
&[],
&ExtrasSpecification::None,
client_builder,
)
.await
}
}
/// Given an extra in a project that may contain references to the project
/// itself, flatten it into a list of requirements.
///
/// For example:
/// ```toml
/// [project]
/// name = "my-project"
/// version = "0.0.1"
/// dependencies = [
/// "tomli",
/// ]
///
/// [project.optional-dependencies]
/// test = [
/// "pep517",
/// ]
/// dev = [
/// "my-project[test]",
/// ]
/// ```
fn flatten_extra(
project_name: &PackageName,
requirements: &[Requirement],
extras: &IndexMap<String, Vec<Requirement>>,
) -> Result<Vec<Requirement>> {
fn inner(
project_name: &PackageName,
requirements: &[Requirement],
extras: &IndexMap<String, Vec<Requirement>>,
seen: &mut FxHashSet<ExtraName>,
) -> Result<Vec<Requirement>> {
let mut flattened = Vec::with_capacity(requirements.len());
for requirement in requirements {
if requirement.name == *project_name {
for extra in &requirement.extras {
// Avoid infinite recursion on mutually recursive extras.
if !seen.insert(extra.clone()) {
continue;
}
// Flatten the extra requirements.
for (name, extra_requirements) in extras {
let normalized_name = ExtraName::from_str(name)?;
if normalized_name == *extra {
flattened.extend(inner(
project_name,
extra_requirements,
extras,
seen,
)?);
}
}
}
} else {
flattened.push(requirement.clone());
}
}
Ok(flattened)
}
inner(
project_name,
requirements,
extras,
&mut FxHashSet::default(),
)
}
/// Load the preferred requirements from an existing lockfile, applying the upgrade strategy.
pub(crate) async fn read_lockfile(
output_file: Option<&Path>,
upgrade: Upgrade,
) -> Result<Vec<Preference>> {
// As an optimization, skip reading the lockfile is we're upgrading all packages anyway.
let Some(output_file) = output_file
.filter(|_| !upgrade.is_all())
.filter(|output_file| output_file.exists())
else {
return Ok(Vec::new());
};
// Parse the requirements from the lockfile.
let requirements_txt = RequirementsTxt::parse(
output_file,
std::env::current_dir()?,
&BaseClientBuilder::new().connectivity(Connectivity::Offline),
)
.await?;
let preferences = requirements_txt
.requirements
.into_iter()
.filter(|entry| !entry.editable)
.map(Preference::from_entry)
.collect::<Result<Vec<_>, PreferenceError>>()?;
// Apply the upgrade strategy to the requirements.
Ok(match upgrade {
// Respect all pinned versions from the existing lockfile.
Upgrade::None => preferences,
// Ignore all pinned versions from the existing lockfile.
Upgrade::All => vec![],
// Ignore pinned versions for the specified packages.
Upgrade::Packages(packages) => preferences
.into_iter()
.filter(|preference| !packages.contains(preference.name()))
.collect(),
})
}
/// Like [`RequirementsSpecification`], but with concrete names for all requirements.
#[derive(Debug, Default)]
pub(crate) struct NamedRequirements {
/// The requirements for the project.
pub(crate) requirements: Vec<Requirement>,
/// The constraints for the project.
pub(crate) constraints: Vec<Requirement>,
/// The overrides for the project.
pub(crate) overrides: Vec<Requirement>,
/// Package to install as editable installs
pub(crate) editables: Vec<EditableRequirement>,
}
impl NamedRequirements {
/// Convert a [`RequirementsSpecification`] into a [`NamedRequirements`].
pub(crate) async fn from_spec(
requirements: Vec<RequirementsTxtRequirement>,
constraints: Vec<Requirement>,
overrides: Vec<Requirement>,
editables: Vec<EditableRequirement>,
cache: &Cache,
client: &RegistryClient,
) -> Result<Self> {
// Resolve all unnamed references.
let requirements = futures::stream::iter(requirements)
.map(|requirement| async {
match requirement {
RequirementsTxtRequirement::Pep508(requirement) => Ok(requirement),
RequirementsTxtRequirement::Unnamed(requirement) => {
Self::name_requirement(requirement, cache, client).await
}
}
})
.buffered(50)
.try_collect()
.await?;
Ok(Self {
requirements,
constraints,
overrides,
editables,
})
}
/// Infer the package name for a given "unnamed" requirement.
async fn name_requirement(
requirement: UnnamedRequirement,
cache: &Cache,
client: &RegistryClient,
) -> Result<Requirement> {
// 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.path())
.extension()
.is_some_and(|ext| ext.eq_ignore_ascii_case("whl"))
{
let filename = WheelFilename::from_str(&requirement.url.filename()?)?;
return Ok(Requirement {
name: filename.name,
extras: requirement.extras,
version_or_url: Some(VersionOrUrl::Url(requirement.url)),
marker: requirement.marker,
});
}
// 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
.filename()
.ok()
.and_then(|filename| SourceDistFilename::parsed_normalized_filename(&filename).ok())
{
return Ok(Requirement {
name: filename.name,
extras: requirement.extras,
version_or_url: Some(VersionOrUrl::Url(requirement.url)),
marker: requirement.marker,
});
}
// Download the archive and attempt to infer the package name from the archive contents.
let source = download_and_extract_archive(&requirement.url, cache, client)
.await
.with_context(|| {
format!("Unable to infer package name for the unnamed requirement: {requirement}")
})?;
// Extract the path to the root of the distribution.
let path = source.path();
// Attempt to read a `PKG-INFO` from the directory.
if let Some(metadata) = fs_err::read(path.join("PKG-INFO"))
.ok()
.and_then(|contents| Metadata10::parse_pkg_info(&contents).ok())
{
debug!(
"Found PKG-INFO metadata for {path} ({name})",
path = path.display(),
name = metadata.name
);
return Ok(Requirement {
name: metadata.name,
extras: requirement.extras,
version_or_url: Some(VersionOrUrl::Url(requirement.url)),
marker: requirement.marker,
});
}
// Attempt to read a `pyproject.toml` file.
if let Some(pyproject) = fs_err::read_to_string(path.join("pyproject.toml"))
.ok()
.and_then(|contents| toml::from_str::<PyProjectToml>(&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 = path.display(),
name = project.name
);
return Ok(Requirement {
name: project.name,
extras: requirement.extras,
version_or_url: Some(VersionOrUrl::Url(requirement.url)),
marker: requirement.marker,
});
}
// 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 = path.display(),
name = name
);
return Ok(Requirement {
name,
extras: requirement.extras,
version_or_url: Some(VersionOrUrl::Url(requirement.url)),
marker: requirement.marker,
});
}
}
}
}
// Attempt to read a `setup.cfg` from the directory.
if let Some(setup_cfg) = fs_err::read_to_string(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 = path.display(),
name = name
);
return Ok(Requirement {
name,
extras: requirement.extras,
version_or_url: Some(VersionOrUrl::Url(requirement.url)),
marker: requirement.marker,
});
}
}
}
}
// Attempt to read a `setup.py` from the directory.
if let Ok(setup_py) = fs_err::read_to_string(path.join("setup.py")) {
static SETUP_PY_NAME: Lazy<Regex> =
Lazy::new(|| Regex::new(r#"name\s*[=:]\s*['"](?P<name>[^'"]+)['"]"#).unwrap());
if let Some(name) = SETUP_PY_NAME
.captures(&setup_py)
.and_then(|captures| captures.name("name"))
.map(|name| name.as_str())
{
if let Ok(name) = PackageName::from_str(name) {
debug!(
"Found setuptools metadata for {path} in `setup.py` ({name})",
path = path.display(),
name = name
);
return Ok(Requirement {
name,
extras: requirement.extras,
version_or_url: Some(VersionOrUrl::Url(requirement.url)),
marker: requirement.marker,
});
}
}
}
// TODO(charlie): If this is common, consider running the PEP 517 build hooks.
Err(anyhow::anyhow!(
"Unable to infer package name for the unnamed requirement: {requirement}"
))
}
}
/// A pyproject.toml as specified in PEP 517.
#[derive(Deserialize, Debug)]
#[serde(rename_all = "kebab-case")]
struct PyProjectToml {
project: Option<Project>,
tool: Option<Tool>,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "kebab-case")]
struct Project {
name: PackageName,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "kebab-case")]
struct Tool {
poetry: Option<ToolPoetry>,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "kebab-case")]
struct ToolPoetry {
name: Option<PackageName>,
}