uv/crates/uv-requirements/src/specification.rs

399 lines
17 KiB
Rust

//! Collecting the requirements to compile, sync or install.
//!
//! # `requirements.txt` format
//!
//! The `requirements.txt` format (also known as `requirements.in`) is static except for the
//! possibility of making network requests.
//!
//! All entries are stored as `requirements` and `editables` or `constraints` depending on the kind
//! of inclusion (`uv pip install -r` and `uv pip compile` vs. `uv pip install -c` and
//! `uv pip compile -c`).
//!
//! # `pyproject.toml` and directory source.
//!
//! `pyproject.toml` files come in two forms: PEP 621 compliant with static dependencies and non-PEP 621
//! compliant or PEP 621 compliant with dynamic metadata. There are different ways how the requirements are evaluated:
//! * `uv pip install -r pyproject.toml` or `uv pip compile requirements.in`: The `pyproject.toml`
//! must be valid (in other circumstances we allow invalid `dependencies` e.g. for hatch's
//! relative path support), but it can be dynamic. We set the `project` from the `name` entry. If it is static, we add
//! all `dependencies` from the pyproject.toml as `requirements` (and drop the directory). If it
//! is dynamic, we add the directory to `source_trees`.
//! * `uv pip install .` in a directory with `pyproject.toml` or `uv pip compile requirements.in`
//! where the `requirements.in` points to that directory: The directory is listed in
//! `requirements`. The lookahead resolver reads the static metadata from `pyproject.toml` if
//! available, otherwise it calls PEP 517 to resolve.
//! * `uv pip install -e`: We add the directory in `editables` instead of `requirements`. The
//! lookahead resolver resolves it the same.
//! * `setup.py` or `setup.cfg` instead of `pyproject.toml`: Directory is an entry in
//! `source_trees`.
use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
use rustc_hash::FxHashSet;
use tracing::instrument;
use uv_cache_key::CanonicalUrl;
use uv_client::BaseClientBuilder;
use uv_configuration::{NoBinary, NoBuild};
use uv_distribution_types::{
IndexUrl, NameRequirementSpecification, UnresolvedRequirement,
UnresolvedRequirementSpecification,
};
use uv_fs::{Simplified, CWD};
use uv_normalize::{ExtraName, PackageName};
use uv_pep508::{MarkerTree, UnnamedRequirement, UnnamedRequirementUrl};
use uv_pypi_types::Requirement;
use uv_pypi_types::VerbatimParsedUrl;
use uv_requirements_txt::{RequirementsTxt, RequirementsTxtRequirement};
use uv_warnings::warn_user;
use uv_workspace::pyproject::PyProjectToml;
use crate::RequirementsSource;
#[derive(Debug, Default, Clone)]
pub struct RequirementsSpecification {
/// The name of the project specifying requirements.
pub project: Option<PackageName>,
/// The requirements for the project.
pub requirements: Vec<UnresolvedRequirementSpecification>,
/// The constraints for the project.
pub constraints: Vec<NameRequirementSpecification>,
/// The overrides for the project.
pub overrides: Vec<UnresolvedRequirementSpecification>,
/// The source trees from which to extract requirements.
pub source_trees: Vec<PathBuf>,
/// The extras used to collect requirements.
pub extras: FxHashSet<ExtraName>,
/// The index URL to use for fetching packages.
pub index_url: Option<IndexUrl>,
/// The extra index URLs to use for fetching packages.
pub extra_index_urls: Vec<IndexUrl>,
/// Whether to disallow index usage.
pub no_index: bool,
/// The `--find-links` locations to use for fetching packages.
pub find_links: Vec<IndexUrl>,
/// The `--no-binary` flags to enforce when selecting distributions.
pub no_binary: NoBinary,
/// The `--no-build` flags to enforce when selecting distributions.
pub no_build: NoBuild,
}
impl RequirementsSpecification {
/// Read the requirements and constraints from a source.
#[instrument(skip_all, level = tracing::Level::DEBUG, fields(source = % source))]
pub async fn from_source(
source: &RequirementsSource,
client_builder: &BaseClientBuilder<'_>,
) -> Result<Self> {
Ok(match source {
RequirementsSource::Package(name) => {
let requirement = RequirementsTxtRequirement::parse(name, &*CWD, false)
.with_context(|| format!("Failed to parse: `{name}`"))?;
Self {
requirements: vec![UnresolvedRequirementSpecification::from(requirement)],
..Self::default()
}
}
RequirementsSource::Editable(name) => {
let requirement = RequirementsTxtRequirement::parse(name, &*CWD, true)
.with_context(|| format!("Failed to parse: `{name}`"))?;
Self {
requirements: vec![UnresolvedRequirementSpecification::from(
requirement.into_editable()?,
)],
..Self::default()
}
}
RequirementsSource::RequirementsTxt(path) => {
if !(path == Path::new("-")
|| path.starts_with("http://")
|| path.starts_with("https://")
|| path.exists())
{
return Err(anyhow::anyhow!("File not found: `{}`", path.user_display()));
}
let requirements_txt = RequirementsTxt::parse(path, &*CWD, client_builder).await?;
if requirements_txt == RequirementsTxt::default() {
if path == Path::new("-") {
warn_user!("No dependencies found in stdin");
} else {
warn_user!(
"Requirements file `{}` does not contain any dependencies",
path.user_display()
);
}
}
Self {
requirements: requirements_txt
.requirements
.into_iter()
.map(UnresolvedRequirementSpecification::from)
.chain(
requirements_txt
.editables
.into_iter()
.map(UnresolvedRequirementSpecification::from),
)
.collect(),
constraints: requirements_txt
.constraints
.into_iter()
.map(Requirement::from)
.map(NameRequirementSpecification::from)
.collect(),
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(IndexUrl::from)
.collect(),
no_binary: requirements_txt.no_binary,
no_build: requirements_txt.only_binary,
..Self::default()
}
}
RequirementsSource::PyprojectToml(path) => {
let contents = match fs_err::tokio::read_to_string(&path).await {
Ok(contents) => contents,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
return Err(anyhow::anyhow!("File not found: `{}`", path.user_display()));
}
Err(err) => {
return Err(anyhow::anyhow!(
"Failed to read `{}`: {}",
path.user_display(),
err
));
}
};
let _ = toml::from_str::<PyProjectToml>(&contents)
.with_context(|| format!("Failed to parse: `{}`", path.user_display()))?;
Self {
source_trees: vec![path.clone()],
..Self::default()
}
}
RequirementsSource::SetupPy(path) | RequirementsSource::SetupCfg(path) => {
if !path.is_file() {
return Err(anyhow::anyhow!("File not found: `{}`", path.user_display()));
}
Self {
source_trees: vec![path.clone()],
..Self::default()
}
}
RequirementsSource::SourceTree(path) => {
if !path.is_dir() {
return Err(anyhow::anyhow!(
"Directory not found: `{}`",
path.user_display()
));
}
Self {
project: None,
requirements: vec![UnresolvedRequirementSpecification {
requirement: UnresolvedRequirement::Unnamed(UnnamedRequirement {
url: VerbatimParsedUrl::parse_absolute_path(path)?,
extras: vec![],
marker: MarkerTree::TRUE,
origin: None,
}),
hashes: vec![],
}],
..Self::default()
}
}
})
}
/// Read the combined requirements and constraints from a set of sources.
pub async fn from_sources(
requirements: &[RequirementsSource],
constraints: &[RequirementsSource],
overrides: &[RequirementsSource],
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, client_builder).await?;
spec.requirements.extend(source.requirements);
spec.constraints.extend(source.constraints);
spec.overrides.extend(source.overrides);
spec.extras.extend(source.extras);
spec.source_trees.extend(source.source_trees);
// Use the first project name discovered.
if spec.project.is_none() {
spec.project = source.project;
}
if let Some(index_url) = source.index_url {
if let Some(existing) = spec.index_url {
if CanonicalUrl::new(index_url.url()) != CanonicalUrl::new(existing.url()) {
return Err(anyhow::anyhow!(
"Multiple index URLs specified: `{existing}` vs. `{index_url}`",
));
}
}
spec.index_url = Some(index_url);
}
spec.no_index |= source.no_index;
spec.extra_index_urls.extend(source.extra_index_urls);
spec.find_links.extend(source.find_links);
spec.no_binary.extend(source.no_binary);
spec.no_build.extend(source.no_build);
}
// Read all constraints, treating both requirements _and_ constraints as constraints.
// Overrides are ignored.
for source in constraints {
let source = Self::from_source(source, client_builder).await?;
for entry in source.requirements {
match entry.requirement {
UnresolvedRequirement::Named(requirement) => {
spec.constraints.push(NameRequirementSpecification {
requirement,
hashes: entry.hashes,
});
}
UnresolvedRequirement::Unnamed(requirement) => {
return Err(anyhow::anyhow!(
"Unnamed requirements are not allowed as constraints (found: `{requirement}`)"
));
}
}
}
spec.constraints.extend(source.constraints);
if let Some(index_url) = source.index_url {
if let Some(existing) = spec.index_url {
if CanonicalUrl::new(index_url.url()) != CanonicalUrl::new(existing.url()) {
return Err(anyhow::anyhow!(
"Multiple index URLs specified: `{existing}` vs. `{index_url}`",
));
}
}
spec.index_url = Some(index_url);
}
spec.no_index |= source.no_index;
spec.extra_index_urls.extend(source.extra_index_urls);
spec.find_links.extend(source.find_links);
spec.no_binary.extend(source.no_binary);
spec.no_build.extend(source.no_build);
}
// Read all overrides, treating both requirements _and_ overrides as overrides.
// Constraints are ignored.
for source in overrides {
let source = Self::from_source(source, client_builder).await?;
spec.overrides.extend(source.requirements);
spec.overrides.extend(source.overrides);
if let Some(index_url) = source.index_url {
if let Some(existing) = spec.index_url {
if CanonicalUrl::new(index_url.url()) != CanonicalUrl::new(existing.url()) {
return Err(anyhow::anyhow!(
"Multiple index URLs specified: `{existing}` vs. `{index_url}`",
));
}
}
spec.index_url = Some(index_url);
}
spec.no_index |= source.no_index;
spec.extra_index_urls.extend(source.extra_index_urls);
spec.find_links.extend(source.find_links);
spec.no_binary.extend(source.no_binary);
spec.no_build.extend(source.no_build);
}
Ok(spec)
}
/// Parse an individual package requirement.
pub fn parse_package(name: &str) -> Result<UnresolvedRequirementSpecification> {
let requirement = RequirementsTxtRequirement::parse(name, &*CWD, false)
.with_context(|| format!("Failed to parse: `{name}`"))?;
Ok(UnresolvedRequirementSpecification::from(requirement))
}
/// Read the requirements from a set of sources.
pub async fn from_simple_sources(
requirements: &[RequirementsSource],
client_builder: &BaseClientBuilder<'_>,
) -> Result<Self> {
Self::from_sources(requirements, &[], &[], client_builder).await
}
/// Initialize a [`RequirementsSpecification`] from a list of [`Requirement`].
pub fn from_requirements(requirements: Vec<Requirement>) -> Self {
Self {
requirements: requirements
.into_iter()
.map(UnresolvedRequirementSpecification::from)
.collect(),
..Self::default()
}
}
/// Initialize a [`RequirementsSpecification`] from a list of [`Requirement`], including
/// constraints.
pub fn from_constraints(requirements: Vec<Requirement>, constraints: Vec<Requirement>) -> Self {
Self {
requirements: requirements
.into_iter()
.map(UnresolvedRequirementSpecification::from)
.collect(),
constraints: constraints
.into_iter()
.map(NameRequirementSpecification::from)
.collect(),
..Self::default()
}
}
/// Initialize a [`RequirementsSpecification`] from a list of [`Requirement`], including
/// constraints and overrides.
pub fn from_overrides(
requirements: Vec<Requirement>,
constraints: Vec<Requirement>,
overrides: Vec<Requirement>,
) -> Self {
Self {
requirements: requirements
.into_iter()
.map(UnresolvedRequirementSpecification::from)
.collect(),
constraints: constraints
.into_iter()
.map(NameRequirementSpecification::from)
.collect(),
overrides: overrides
.into_iter()
.map(UnresolvedRequirementSpecification::from)
.collect(),
..Self::default()
}
}
/// Return true if the specification does not include any requirements to install.
pub fn is_empty(&self) -> bool {
self.requirements.is_empty() && self.source_trees.is_empty() && self.overrides.is_empty()
}
}