Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix pypi dependencies parsing errors #479

Merged
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/lock_file/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,10 @@ pub fn lock_file_up_to_date(project: &Project, lock_file: &CondaLock) -> miette:
let platforms = project.platforms();

// TODO: Add support for python dependencies
if !project.pypi_dependencies().is_empty() {
if project
.pypi_dependencies()
.is_some_and(|deps| !deps.is_empty())
{
tracing::warn!("Checking if a lock-file is up to date with `pypi-dependencies` in the mix is not yet implemented.");
return Ok(false);
}
Expand Down
16 changes: 10 additions & 6 deletions src/lock_file/python.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,15 @@ pub async fn resolve_pypi_dependencies<'p>(
platform: Platform,
conda_packages: &[RepoDataRecord],
) -> miette::Result<Vec<PinnedPackage<'p>>> {
let requirements = project.pypi_dependencies();
if requirements.is_empty() {
// If there are no requirements we can skip this function.
return Ok(vec![]);
}
let dependencies = match project.pypi_dependencies() {
Some(deps) if !deps.is_empty() => deps,
_ => return Ok(vec![]),
};

let requirements = dependencies
.iter()
.map(|(name, req)| req.as_pep508(name))
.collect::<Vec<pep508_rs::Requirement>>();

// Determine the python packages that are installed by the conda packages
let conda_python_packages =
Expand Down Expand Up @@ -64,7 +68,7 @@ pub async fn resolve_pypi_dependencies<'p>(
// Resolve the PyPi dependencies
let mut result = rip::resolve(
project.pypi_package_db()?,
&requirements.as_pep508(),
&requirements,
&marker_environment,
Some(&compatible_tags),
conda_python_packages
Expand Down
4 changes: 2 additions & 2 deletions src/project/manifest.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::project::python::PypiDependencies;
use crate::project::python::PyPiRequirement;
use crate::project::SpecType;
use crate::utils::spanned::PixiSpanned;
use crate::{consts, task::Task};
Expand Down Expand Up @@ -70,7 +70,7 @@ pub struct ProjectManifest {

/// Optional python requirements
#[serde(default, rename = "pypi-dependencies")]
pub pypi_dependencies: PypiDependencies,
pub pypi_dependencies: Option<IndexMap<rip::PackageName, PyPiRequirement>>,
}

impl ProjectManifest {
Expand Down
12 changes: 8 additions & 4 deletions src/project/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ use std::{
sync::Arc,
};

use crate::project::python::PypiDependencies;
use crate::project::python::PyPiRequirement;
use crate::{
consts::{self, PROJECT_MANIFEST},
default_client,
Expand Down Expand Up @@ -332,7 +332,11 @@ impl Project {
)?;

// Notify the user that pypi-dependencies are still experimental
if !manifest.pypi_dependencies.is_empty() {
if manifest
.pypi_dependencies
.as_ref()
.map_or(false, |deps| !deps.is_empty())
{
tracing::warn!("ALPHA feature enabled!\n\nIt looks like your project contains `[pypi-dependencies]`. This feature is currently still in an ALPHA state!\n\nYou may encounter bugs or weird behavior. Please report any and all issues you encounter on our github repository:\n\n\thttps://github.com/prefix-dev/pixi.\n");
}

Expand Down Expand Up @@ -447,8 +451,8 @@ impl Project {
Ok(dependencies)
}

pub fn pypi_dependencies(&self) -> &PypiDependencies {
&self.manifest.pypi_dependencies
pub fn pypi_dependencies(&self) -> Option<&IndexMap<rip::PackageName, PyPiRequirement>> {
self.manifest.pypi_dependencies.as_ref()
}

/// Returns the Python index URLs to use for this project.
Expand Down
155 changes: 62 additions & 93 deletions src/project/python.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
use indexmap::IndexMap;
use pep440_rs::VersionSpecifiers;
use pep508_rs::VersionOrUrl;
use serde::de::{Error, MapAccess, Visitor};
Expand All @@ -16,7 +15,7 @@ pub struct PyPiRequirement {
/// The type of parse error that occurred when parsing match spec.
#[derive(Debug, Clone, Error)]
pub enum ParsePyPiRequirementError {
#[error("invalid PEP440")]
#[error("invalid PEP440: https://peps.python.org/pep-0440/")]
Pep440Error(#[from] pep440_rs::Pep440Error),
}

Expand All @@ -43,35 +42,15 @@ impl FromStr for PyPiRequirement {
}
}

/// Represents a set of python dependencies on which a project can depend. The dependencies are
/// formatted using a custom version specifier.
#[derive(Default, Debug, Deserialize, Clone, Eq, PartialEq)]
pub struct PypiDependencies {
#[serde(flatten)]
requirements: IndexMap<rip::PackageName, PyPiRequirement>,
}

impl PypiDependencies {
/// Returns `true` if no requirements have been specified
pub fn is_empty(&self) -> bool {
self.requirements.is_empty()
}

impl PyPiRequirement {
/// Returns the requirements as [`pep508_rs::Requirement`]s.
pub fn as_pep508(&self) -> Vec<pep508_rs::Requirement> {
self.requirements
.iter()
.map(|(name, req)| {
let version = req.version.clone().map(VersionOrUrl::VersionSpecifier);

pep508_rs::Requirement {
name: name.as_str().to_string(),
extras: req.extras.clone(),
version_or_url: version,
marker: None,
}
})
.collect()
pub fn as_pep508(&self, name: &rip::PackageName) -> pep508_rs::Requirement {
pep508_rs::Requirement {
name: name.as_str().to_string(),
extras: self.extras.clone(),
version_or_url: self.version.clone().map(VersionOrUrl::VersionSpecifier),
marker: None,
}
}
}
impl<'de> Deserialize<'de> for PyPiRequirement {
Expand Down Expand Up @@ -117,93 +96,83 @@ impl<'de> Deserialize<'de> for PyPiRequirement {
#[cfg(test)]
mod test {
use super::*;
use indexmap::IndexMap;

#[test]
fn test_only_version() {
let requirement: PypiDependencies = toml_edit::de::from_str(r#"foo = ">=3.12""#).unwrap();
let requirement: IndexMap<rip::PackageName, PyPiRequirement> =
toml_edit::de::from_str(r#"foo = ">=3.12""#).unwrap();
assert_eq!(
requirement,
PypiDependencies {
requirements: IndexMap::from([(
rip::PackageName::from_str("foo").unwrap(),
PyPiRequirement {
version: Some(VersionSpecifiers::from_str(">=3.12").unwrap()),
extras: None
}
),])
requirement.first().unwrap().0,
&rip::PackageName::from_str("foo").unwrap()
);
assert_eq!(
requirement.first().unwrap().1,
&PyPiRequirement {
version: Some(VersionSpecifiers::from_str(">=3.12").unwrap()),
extras: None
}
);
let requirement: PypiDependencies = toml_edit::de::from_str(r#"foo = "==3.12.0""#).unwrap();
let requirement: IndexMap<rip::PackageName, PyPiRequirement> =
toml_edit::de::from_str(r#"foo = "==3.12.0""#).unwrap();
assert_eq!(
requirement,
PypiDependencies {
requirements: IndexMap::from([(
rip::PackageName::from_str("foo").unwrap(),
PyPiRequirement {
version: Some(VersionSpecifiers::from_str("==3.12.0").unwrap()),
extras: None
}
),])
requirement.first().unwrap().1,
&PyPiRequirement {
version: Some(VersionSpecifiers::from_str("==3.12.0").unwrap()),
extras: None
}
);
let requirement: PypiDependencies = toml_edit::de::from_str(r#"foo = "~=2.1.3""#).unwrap();

let requirement: IndexMap<rip::PackageName, PyPiRequirement> =
toml_edit::de::from_str(r#"foo = "~=2.1.3""#).unwrap();
assert_eq!(
requirement,
PypiDependencies {
requirements: IndexMap::from([(
rip::PackageName::from_str("foo").unwrap(),
PyPiRequirement {
version: Some(VersionSpecifiers::from_str("~=2.1.3").unwrap()),
extras: None
}
),])
requirement.first().unwrap().1,
&PyPiRequirement {
version: Some(VersionSpecifiers::from_str("~=2.1.3").unwrap()),
extras: None
}
);
let requirement: PypiDependencies = toml_edit::de::from_str(r#"foo = "*""#).unwrap();

let requirement: IndexMap<rip::PackageName, PyPiRequirement> =
toml_edit::de::from_str(r#"foo = "*""#).unwrap();
assert_eq!(
requirement,
PypiDependencies {
requirements: IndexMap::from([(
rip::PackageName::from_str("foo").unwrap(),
PyPiRequirement {
version: None,
extras: None
}
),])
requirement.first().unwrap().1,
&PyPiRequirement {
version: None,
extras: None
}
);
}

#[test]
fn test_extended() {
let requirement: PypiDependencies =
toml::de::from_str(r#"foo = { version=">=3.12", extras = ["bar"] }"#).unwrap();
let requirement: IndexMap<rip::PackageName, PyPiRequirement> =
toml_edit::de::from_str(r#"foo = { version=">=3.12", extras = ["bar"] }"#).unwrap();
assert_eq!(
requirement,
PypiDependencies {
requirements: IndexMap::from([(
rip::PackageName::from_str("foo").unwrap(),
PyPiRequirement {
version: Some(VersionSpecifiers::from_str(">=3.12").unwrap()),
extras: Some(vec!("bar".to_string()))
}
),])
requirement.first().unwrap().0,
&rip::PackageName::from_str("foo").unwrap()
);
assert_eq!(
requirement.first().unwrap().1,
&PyPiRequirement {
version: Some(VersionSpecifiers::from_str(">=3.12").unwrap()),
extras: Some(vec!("bar".to_string()))
}
);

let requirement: PypiDependencies =
toml::de::from_str(r#"bar = { version=">=3.12,<3.13.0", extras = ["bar", "foo"] }"#)
.unwrap();
let requirement: IndexMap<rip::PackageName, PyPiRequirement> = toml_edit::de::from_str(
r#"bar = { version=">=3.12,<3.13.0", extras = ["bar", "foo"] }"#,
)
.unwrap();
assert_eq!(
requirement.first().unwrap().0,
&rip::PackageName::from_str("bar").unwrap()
);
assert_eq!(
requirement,
PypiDependencies {
requirements: IndexMap::from([(
rip::PackageName::from_str("bar").unwrap(),
PyPiRequirement {
version: Some(VersionSpecifiers::from_str(">=3.12,<3.13.0").unwrap()),
extras: Some(vec!("bar".to_string(), "foo".to_string()))
}
),])
requirement.first().unwrap().1,
&PyPiRequirement {
version: Some(VersionSpecifiers::from_str(">=3.12,<3.13.0").unwrap()),
extras: Some(vec!("bar".to_string(), "foo".to_string()))
}
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
---
source: src/project/manifest.rs
assertion_line: 481
expression: "toml_edit::de::from_str::<ProjectManifest>(&contents).expect(\"parsing should succeed!\")"
---
ProjectManifest {
Expand Down Expand Up @@ -97,7 +96,5 @@ ProjectManifest {
),
},
),
pypi_dependencies: PypiDependencies {
requirements: {},
},
pypi_dependencies: None,
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
---
source: src/project/manifest.rs
assertion_line: 442
expression: "toml_edit::de::from_str::<ProjectManifest>(&contents).expect(\"parsing should succeed!\")"
---
ProjectManifest {
Expand Down Expand Up @@ -95,7 +94,5 @@ ProjectManifest {
),
target: {},
activation: None,
pypi_dependencies: PypiDependencies {
requirements: {},
},
pypi_dependencies: None,
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
---
source: src/project/manifest.rs
assertion_line: 421
expression: "toml_edit::de::from_str::<ProjectManifest>(&contents).expect(\"parsing should succeed!\")"
---
ProjectManifest {
Expand Down Expand Up @@ -89,7 +88,5 @@ ProjectManifest {
build_dependencies: None,
target: {},
activation: None,
pypi_dependencies: PypiDependencies {
requirements: {},
},
pypi_dependencies: None,
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ ProjectManifest {
build_dependencies: None,
target: {},
activation: None,
pypi_dependencies: PypiDependencies {
requirements: {
pypi_dependencies: Some(
{
PackageName {
source: "foo",
normalized: "foo",
Expand Down Expand Up @@ -101,5 +101,5 @@ ProjectManifest {
),
},
},
},
),
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
---
source: src/project/manifest.rs
assertion_line: 406
expression: "toml_edit::de::from_str::<ProjectManifest>(&contents).expect(\"parsing should succeed!\")"
---
ProjectManifest {
Expand Down Expand Up @@ -112,7 +111,5 @@ ProjectManifest {
},
},
activation: None,
pypi_dependencies: PypiDependencies {
requirements: {},
},
pypi_dependencies: None,
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
---
source: src/project/manifest.rs
assertion_line: 502
expression: "toml_edit::de::from_str::<ProjectManifest>(&contents).expect(\"parsing should succeed!\")"
---
ProjectManifest {
Expand Down Expand Up @@ -84,7 +83,5 @@ ProjectManifest {
},
},
activation: None,
pypi_dependencies: PypiDependencies {
requirements: {},
},
pypi_dependencies: None,
}
Loading