use crate::{PackageHashes, SourceData, UrlOrPath, Verbatim};
use pep440_rs::VersionSpecifiers;
use pep508_rs::{PackageName, Requirement};
use rattler_digest::{Sha256, digest::Digest};
use std::fs;
use std::path::Path;
#[derive(Eq, PartialEq, Clone, Debug, Hash)]
pub enum PypiPackageData {
Distribution(Box<PypiDistributionData>),
Source(Box<PypiSourceData>),
}
#[derive(Eq, PartialEq, Clone, Debug, Hash)]
pub struct PypiDistributionData {
pub name: PackageName,
pub version: pep440_rs::Version,
pub location: Verbatim<UrlOrPath>,
pub index_url: Option<url::Url>,
pub hash: Option<PackageHashes>,
pub requires_dist: Vec<Requirement>,
pub requires_python: Option<VersionSpecifiers>,
}
#[derive(Eq, PartialEq, Clone, Debug, Hash)]
pub struct PypiSourceData {
pub name: PackageName,
pub location: Verbatim<UrlOrPath>,
pub requires_dist: Vec<Requirement>,
pub requires_python: Option<VersionSpecifiers>,
pub source_data: SourceData,
}
impl PypiPackageData {
pub fn name(&self) -> &PackageName {
match self {
Self::Distribution(w) => &w.name,
Self::Source(s) => &s.name,
}
}
pub fn version(&self) -> Option<&pep440_rs::Version> {
self.as_wheel().map(|w| &w.version)
}
pub fn version_string(&self) -> String {
self.version().map_or_else(
|| String::from("<unknown>"),
std::string::ToString::to_string,
)
}
pub fn location(&self) -> &Verbatim<UrlOrPath> {
match self {
Self::Distribution(w) => &w.location,
Self::Source(s) => &s.location,
}
}
pub fn requires_dist(&self) -> &[Requirement] {
match self {
PypiPackageData::Distribution(w) => &w.requires_dist,
PypiPackageData::Source(s) => &s.requires_dist,
}
}
pub fn requires_python(&self) -> Option<&VersionSpecifiers> {
match self {
PypiPackageData::Distribution(w) => w.requires_python.as_ref(),
PypiPackageData::Source(s) => s.requires_python.as_ref(),
}
}
pub fn satisfies(&self, spec: &Requirement) -> bool {
if spec.name != *self.name() {
return false;
}
match &spec.version_or_url {
None => true,
Some(pep508_rs::VersionOrUrl::Url(_)) => false,
Some(pep508_rs::VersionOrUrl::VersionSpecifier(spec)) => match self {
Self::Distribution(w) => spec.contains(&w.version),
Self::Source(_) => true,
},
}
}
pub fn as_wheel(&self) -> Option<&PypiDistributionData> {
match self {
Self::Distribution(w) => Some(w),
Self::Source(_) => None,
}
}
pub fn as_source(&self) -> Option<&PypiSourceData> {
match self {
Self::Distribution(_) => None,
Self::Source(s) => Some(s),
}
}
pub fn as_wheel_mut(&mut self) -> Option<&mut PypiDistributionData> {
match self {
Self::Distribution(w) => Some(w),
Self::Source(_) => None,
}
}
pub fn as_source_mut(&mut self) -> Option<&mut PypiSourceData> {
match self {
Self::Distribution(_) => None,
Self::Source(s) => Some(s),
}
}
pub fn into_wheel(self) -> Option<PypiDistributionData> {
match self {
Self::Distribution(w) => Some(*w),
Self::Source(_) => None,
}
}
pub fn into_source(self) -> Option<PypiSourceData> {
match self {
Self::Distribution(_) => None,
Self::Source(s) => Some(*s),
}
}
}
impl From<PypiDistributionData> for PypiPackageData {
fn from(value: PypiDistributionData) -> Self {
Self::Distribution(Box::new(value))
}
}
impl From<PypiSourceData> for PypiPackageData {
fn from(value: PypiSourceData) -> Self {
Self::Source(Box::new(value))
}
}
pub struct PypiSourceTreeHashable {
pub pyproject_toml: Option<String>,
pub setup_py: Option<String>,
pub setup_cfg: Option<String>,
}
fn ignore_not_found<C>(result: std::io::Result<C>) -> std::io::Result<Option<C>> {
match result {
Ok(content) => Ok(Some(content)),
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(err) => Err(err),
}
}
fn normalize_file_contents(contents: &str) -> String {
contents.replace("\r\n", "\n")
}
impl PypiSourceTreeHashable {
pub fn from_directory(directory: impl AsRef<Path>) -> std::io::Result<Self> {
let directory = directory.as_ref();
let pyproject_toml =
ignore_not_found(fs::read_to_string(directory.join("pyproject.toml")))?;
let setup_py = ignore_not_found(fs::read_to_string(directory.join("setup.py")))?;
let setup_cfg = ignore_not_found(fs::read_to_string(directory.join("setup.cfg")))?;
Ok(Self {
pyproject_toml: pyproject_toml.as_deref().map(normalize_file_contents),
setup_py: setup_py.as_deref().map(normalize_file_contents),
setup_cfg: setup_cfg.as_deref().map(normalize_file_contents),
})
}
pub fn hash(&self) -> PackageHashes {
let mut hasher = Sha256::new();
if let Some(pyproject_toml) = &self.pyproject_toml {
hasher.update(pyproject_toml.as_bytes());
}
if let Some(setup_py) = &self.setup_py {
hasher.update(setup_py.as_bytes());
}
if let Some(setup_cfg) = &self.setup_cfg {
hasher.update(setup_cfg.as_bytes());
}
PackageHashes::Sha256(hasher.finalize())
}
}