211 lines
8.2 KiB
Rust
211 lines
8.2 KiB
Rust
//! A standard interface for working with heterogeneous sources of requirements.
|
|
|
|
use std::path::PathBuf;
|
|
use std::str::FromStr;
|
|
|
|
use anyhow::{Context, Result};
|
|
use fs_err as fs;
|
|
use rustc_hash::FxHashSet;
|
|
|
|
use pep508_rs::Requirement;
|
|
use puffin_normalize::{ExtraName, PackageName};
|
|
use requirements_txt::{EditableRequirement, RequirementsTxt};
|
|
|
|
#[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 From<PathBuf> for RequirementsSource {
|
|
fn from(path: PathBuf) -> Self {
|
|
if path.ends_with("pyproject.toml") {
|
|
Self::PyprojectToml(path)
|
|
} else {
|
|
Self::RequirementsTxt(path)
|
|
}
|
|
}
|
|
}
|
|
|
|
#[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<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>,
|
|
/// The extras used to collect requirements.
|
|
pub(crate) extras: FxHashSet<ExtraName>,
|
|
}
|
|
|
|
impl RequirementsSpecification {
|
|
/// Read the requirements and constraints from a source.
|
|
pub(crate) fn from_source(
|
|
source: &RequirementsSource,
|
|
extras: &ExtrasSpecification,
|
|
) -> Result<Self> {
|
|
Ok(match source {
|
|
RequirementsSource::Package(name) => {
|
|
let requirement = Requirement::from_str(name)
|
|
.with_context(|| format!("Failed to parse `{name}`"))?;
|
|
Self {
|
|
project: None,
|
|
requirements: vec![requirement],
|
|
constraints: vec![],
|
|
overrides: vec![],
|
|
editables: vec![],
|
|
extras: FxHashSet::default(),
|
|
}
|
|
}
|
|
RequirementsSource::Editable(name) => {
|
|
let requirement = EditableRequirement::from_str(name)
|
|
.with_context(|| format!("Failed to parse `{name}`"))?;
|
|
Self {
|
|
project: None,
|
|
requirements: vec![],
|
|
constraints: vec![],
|
|
overrides: vec![],
|
|
editables: vec![requirement],
|
|
extras: FxHashSet::default(),
|
|
}
|
|
}
|
|
RequirementsSource::RequirementsTxt(path) => {
|
|
let requirements_txt = RequirementsTxt::parse(path, std::env::current_dir()?)?;
|
|
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(),
|
|
}
|
|
}
|
|
RequirementsSource::PyprojectToml(path) => {
|
|
let contents = fs::read_to_string(path)?;
|
|
let pyproject_toml = toml::from_str::<pyproject_toml::PyProjectToml>(&contents)
|
|
.with_context(|| format!("Failed to read `{}`", path.display()))?;
|
|
let mut used_extras = FxHashSet::default();
|
|
let mut requirements = Vec::new();
|
|
let mut project_name = None;
|
|
if let Some(project) = pyproject_toml.project {
|
|
requirements.extend(project.dependencies.unwrap_or_default());
|
|
// Include any optional dependencies specified in `extras`
|
|
if !matches!(extras, ExtrasSpecification::None) {
|
|
for (name, optional_requirements) in
|
|
project.optional_dependencies.unwrap_or_default()
|
|
{
|
|
// TODO(konstin): It's not ideal that pyproject-toml doesn't use
|
|
// `ExtraName`
|
|
let normalized_name = ExtraName::new(name)?;
|
|
if extras.contains(&normalized_name) {
|
|
used_extras.insert(normalized_name);
|
|
requirements.extend(optional_requirements);
|
|
}
|
|
}
|
|
}
|
|
// Parse the project name
|
|
project_name = Some(PackageName::new(project.name).with_context(|| {
|
|
format!("Invalid `project.name` in {}", path.display())
|
|
})?);
|
|
}
|
|
|
|
Self {
|
|
project: project_name,
|
|
requirements,
|
|
constraints: vec![],
|
|
overrides: vec![],
|
|
editables: vec![],
|
|
extras: used_extras,
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
/// Read the combined requirements and constraints from a set of sources.
|
|
pub(crate) fn from_sources(
|
|
requirements: &[RequirementsSource],
|
|
constraints: &[RequirementsSource],
|
|
overrides: &[RequirementsSource],
|
|
extras: &ExtrasSpecification,
|
|
) -> 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)?;
|
|
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;
|
|
}
|
|
}
|
|
|
|
// Read all constraints, treating _everything_ as a constraint.
|
|
for source in constraints {
|
|
let source = Self::from_source(source, extras)?;
|
|
spec.constraints.extend(source.requirements);
|
|
spec.constraints.extend(source.constraints);
|
|
spec.constraints.extend(source.overrides);
|
|
}
|
|
|
|
// Read all overrides, treating both requirements _and_ constraints as overrides.
|
|
for source in overrides {
|
|
let source = Self::from_source(source, extras)?;
|
|
spec.overrides.extend(source.requirements);
|
|
spec.overrides.extend(source.constraints);
|
|
spec.overrides.extend(source.overrides);
|
|
}
|
|
|
|
Ok(spec)
|
|
}
|
|
|
|
/// Read the requirements from a set of sources.
|
|
pub(crate) fn requirements_and_editables(
|
|
requirements: &[RequirementsSource],
|
|
) -> Result<(Vec<Requirement>, Vec<EditableRequirement>)> {
|
|
let specification = Self::from_sources(requirements, &[], &[], &ExtrasSpecification::None)?;
|
|
Ok((specification.requirements, specification.editables))
|
|
}
|
|
}
|