Skip to content
Open
Show file tree
Hide file tree
Changes from 6 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
3 changes: 2 additions & 1 deletion crates/pixi/tests/integration_rust/add_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -544,7 +544,8 @@ index-url = "{index_url}"
source: PixiPypiSource::Registry {
version: VersionOrStar::from_str("==24.8.0").unwrap(),
index: None,
}
},
env_markers: pep508_rs::MarkerTree::default(),
}
);
}
Expand Down
68 changes: 68 additions & 0 deletions crates/pixi/tests/integration_rust/pypi_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,74 @@ test = {{features = ["test"]}}
);
}

#[tokio::test]
async fn pyproject_environment_markers_resolved() {
setup_tracing();

let simple = PyPIDatabase::new()
.with(PyPIPackage::new("numpy", "1.0.0").with_tag("cp311", "cp311", "manylinux1_x86_64"))
.into_simple_index()
.unwrap();

let platform = Platform::current();
let platform_str = match platform {
Platform::Linux64 => "\"linux-64\"".into(),
_ => format!("\"linux-64\", \"{}\"", Platform::current().as_str()),
};

let mut package_db = MockRepoData::default();
package_db.add_package(
Package::build("python", "3.11.0")
.with_subdir(platform)
.finish(),
);
if platform != Platform::Linux64 {
package_db.add_package(
Package::build("python", "3.11.0")
.with_subdir(Platform::Linux64)
.finish(),
);
}
let channel = package_db.into_channel().await.unwrap();
let channel_url = channel.url();
let index_url = simple.index_url();

let pyproject = format!(
r#"
[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"

[project]
name = "environment-markers"
dependencies = [
"numpy; sys_platform == 'linux'"
]

[tool.pixi.workspace]
channels = ["{channel_url}"]
platforms = [{platform_str}]
conda-pypi-map = {{}}

[tool.pixi.dependencies]
python = "==3.11.0"

[tool.pixi.pypi-options]
index-url = "{index_url}"
"#,
);

let pixi = PixiControl::from_pyproject_manifest(&pyproject).unwrap();

let lock = pixi.update_lock_file().await.unwrap();

let numpy_req = Requirement::from_str("numpy; sys_platform == 'linux'").unwrap();
assert!(
lock.contains_pep508_requirement("default", platform, numpy_req.clone()),
"default environment should include numpy"
);
}

#[tokio::test]
async fn test_flat_links_based_index_returns_path() {
setup_tracing();
Expand Down
10 changes: 9 additions & 1 deletion crates/pixi_cli/src/workspace/export/conda_environment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,9 @@ fn format_pip_extras(extras: &[ExtraName]) -> String {

fn format_pip_dependency(name: &PypiPackageName, requirement: &PixiPypiSpec) -> String {
let extras = &requirement.extras;
let markers = &requirement.env_markers;

match &requirement.source {
let mut dependency = match &requirement.source {
PixiPypiSource::Git { git: git_url } => {
let mut git_string = format!(
"{name}{extras} @ git+{url}",
Expand Down Expand Up @@ -115,7 +116,14 @@ fn format_pip_dependency(name: &PypiPackageName, requirement: &PixiPypiSpec) ->
extras = format_pip_extras(extras)
),
},
};

let marker_str = markers.try_to_string();
if let Some(marker_str) = marker_str {
dependency.push_str(&format!("; {}", marker_str));
}

dependency
}

fn build_env_yaml(
Expand Down
84 changes: 77 additions & 7 deletions crates/pixi_pypi_spec/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use std::{
};

use pep440_rs::VersionSpecifiers;
use pep508_rs::ExtraName;
use pep508_rs::{ExtraName, MarkerTree};
use pixi_spec::GitSpec;
use serde::Serialize;
use thiserror::Error;
Expand Down Expand Up @@ -119,6 +119,16 @@ impl Default for PixiPypiSource {
}
}

/// Serialize a `pep508_rs::MarkerTree` into a string representation
fn serialize_markertree<S>(value: &MarkerTree, s: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
// `.unwrap()` succeeds because we don't serialize when
// `value.is_true()`.
value.contents().unwrap().serialize(s)
}

/// A complete PyPI dependency specification.
///
/// This is the main type used throughout pixi for PyPI dependencies. It combines
Expand All @@ -131,6 +141,14 @@ pub struct PixiPypiSpec {
/// Optional package extras to install.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub extras: Vec<ExtraName>,
/// The environment markers that decide if/when this package gets installed
#[serde(
default,
// Needed because `pep508_rs::MarkerTree` doesn't implement `serde::Serialize`
serialize_with = "serialize_markertree",
skip_serializing_if = "MarkerTree::is_true"
)]
pub env_markers: MarkerTree,
/// The source for this package.
#[serde(flatten)]
pub source: PixiPypiSource,
Expand Down Expand Up @@ -181,6 +199,7 @@ impl From<PixiPypiSource> for PixiPypiSpec {
PixiPypiSpec {
extras: Vec::new(),
source,
env_markers: MarkerTree::default(),
}
}
}
Expand All @@ -191,12 +210,21 @@ impl PixiPypiSpec {
PixiPypiSpec {
extras: Vec::new(),
source,
env_markers: MarkerTree::default(),
}
}

/// Creates a new spec with the given source and extras.
pub fn with_extras(source: PixiPypiSource, extras: Vec<ExtraName>) -> Self {
PixiPypiSpec { extras, source }
pub fn with_extras_and_markers(
source: PixiPypiSource,
extras: Vec<ExtraName>,
env_markers: MarkerTree,
) -> Self {
PixiPypiSpec {
extras,
source,
env_markers,
}
}

/// Returns a reference to the source.
Expand Down Expand Up @@ -253,6 +281,11 @@ impl PixiPypiSpec {
&self.extras
}

/// Returns the environment markers for this spec.
pub fn env_markers(&self) -> &MarkerTree {
&self.env_markers
}

/// Returns the editability setting from the manifest.
/// Only `Path` specs can be editable. Returns `None` for non-path specs
/// or if editability is not explicitly specified.
Expand Down Expand Up @@ -293,6 +326,8 @@ impl PixiPypiSpec {
updated.extras = self.extras.clone();
}

updated.env_markers.or(requirement.marker.clone());

Ok(updated)
}
}
Expand Down Expand Up @@ -359,7 +394,7 @@ mod tests {
let extra = ExtraName::new("test".to_string()).unwrap();

// Spec with extras
let spec = PixiPypiSpec::with_extras(
let spec = PixiPypiSpec::with_extras_and_markers(
PixiPypiSource::Git {
git: GitSpec {
git: Url::parse("https://github.com/example/repo").unwrap(),
Expand All @@ -368,17 +403,43 @@ mod tests {
},
},
vec![extra.clone()],
MarkerTree::default(),
);
assert_eq!(spec.extras(), std::slice::from_ref(&extra));

// Spec without extras
// Spec without extras and markers
let spec = PixiPypiSpec::new(PixiPypiSource::Registry {
version: VersionOrStar::Star,
index: None,
});
assert!(spec.extras().is_empty());
}

#[test]
fn test_env_markers_accessor() {
let markers = MarkerTree::from_str("python_version >= '3.12'").unwrap();
// Spec with markers
let spec = PixiPypiSpec::with_extras_and_markers(
PixiPypiSource::Git {
git: GitSpec {
git: Url::parse("https://github.com/example/repo").unwrap(),
rev: None,
subdirectory: None,
},
},
vec![],
markers.clone(),
);
assert_eq!(spec.env_markers(), &markers);

// Spec without extras and markers
let spec = PixiPypiSpec::new(PixiPypiSource::Registry {
version: VersionOrStar::Star,
index: None,
});
assert!(spec.env_markers().is_true());
}

#[test]
fn test_source_accessor() {
let spec = PixiPypiSpec::new(PixiPypiSource::Path {
Expand Down Expand Up @@ -447,12 +508,14 @@ mod tests {
let spec: PixiPypiSpec = source.clone().into();
assert_eq!(spec.source, source);
assert!(spec.extras.is_empty());
assert!(spec.env_markers.is_true());
}

#[test]
fn test_default_spec() {
let spec = PixiPypiSpec::default();
assert!(spec.extras.is_empty());
assert!(spec.env_markers.is_true());
assert!(matches!(
spec.source,
PixiPypiSource::Registry {
Expand All @@ -469,7 +532,7 @@ mod tests {
let pypi = PixiPypiSpec::try_from(req).unwrap();
assert_eq!(
pypi.to_string(),
"{ version = \"==1.0.0\", extras = [\"testing\"] }"
"{ version = \"==1.0.0\", extras = [\"testing\"], env-markers = \"os_name == 'posix'\" }"
);

let req = pep508_rs::Requirement::from_str("numpy").unwrap();
Expand Down Expand Up @@ -525,7 +588,11 @@ mod tests {

let pypi: Requirement = "boltons[nichita] @ https://files.pythonhosted.org/packages/46/35/e50d4a115f93e2a3fbf52438435bb2efcf14c11d4fcd6bdcd77a6fc399c9/boltons-24.0.0-py3-none-any.whl".parse().unwrap();
let as_pypi_req: PixiPypiSpec = pypi.try_into().unwrap();
assert_eq!(as_pypi_req, PixiPypiSpec::with_extras(PixiPypiSource::Url { url: Url::parse("https://files.pythonhosted.org/packages/46/35/e50d4a115f93e2a3fbf52438435bb2efcf14c11d4fcd6bdcd77a6fc399c9/boltons-24.0.0-py3-none-any.whl").unwrap(), subdirectory: None }, vec![ExtraName::new("nichita".to_string()).unwrap()]));
assert_eq!(as_pypi_req, PixiPypiSpec::with_extras_and_markers(PixiPypiSource::Url { url: Url::parse("https://files.pythonhosted.org/packages/46/35/e50d4a115f93e2a3fbf52438435bb2efcf14c11d4fcd6bdcd77a6fc399c9/boltons-24.0.0-py3-none-any.whl").unwrap(), subdirectory: None }, vec![ExtraName::new("nichita".to_string()).unwrap()], MarkerTree::default()));

let pypi: Requirement = "potato[habbasi]; sys_platform == 'linux'".parse().unwrap();
let as_pypi_req: PixiPypiSpec = pypi.try_into().unwrap();
assert_snapshot!(as_pypi_req);

#[cfg(target_os = "windows")]
let pypi: Requirement = "boltons @ file:///C:/path/to/boltons".parse().unwrap();
Expand Down Expand Up @@ -601,6 +668,8 @@ mod tests {
r#"pkg = { git = "https://github.com/prefix-dev/rattler-build", "rev" = "123456" }"#,
r#"pkg = { git = "https://github.com/prefix-dev/rattler-build", "subdirectory" = "pyrattler" }"#,
r#"pkg = { git = "https://github.com/prefix-dev/rattler-build", "extras" = ["test"] }"#,
r#"pkg = { version = "*", "env-markers" = "sys_platform == 'win32'" }"#,
r#"pkg = { git = "https://github.com/prefix-dev/rattler-build", "extras" = ["test"], "env-markers" = "sys_platform == 'linux'" }"#,
];

#[derive(Serialize)]
Expand Down Expand Up @@ -639,6 +708,7 @@ mod tests {
r#"pkg = "~/path/style""#,
r#"pkg = "https://example.com""#,
r#"pkg = "https://github.com/conda-forge/21cmfast-feedstock""#,
r#"pkg = { version = "*", "env-markers" = "potato == 'potato'" }"#,
];

struct Snapshot {
Expand Down
35 changes: 23 additions & 12 deletions crates/pixi_pypi_spec/src/pep508.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,16 @@ impl TryFrom<pep508_rs::Requirement> for PixiPypiSpec {
fn try_from(req: pep508_rs::Requirement) -> Result<Self, Self::Error> {
let converted = if let Some(version_or_url) = req.version_or_url {
match version_or_url {
pep508_rs::VersionOrUrl::VersionSpecifier(v) => PixiPypiSpec::with_extras(
PixiPypiSource::Registry {
version: v.into(),
index: None,
},
req.extras,
),
pep508_rs::VersionOrUrl::VersionSpecifier(v) => {
PixiPypiSpec::with_extras_and_markers(
PixiPypiSource::Registry {
version: v.into(),
index: None,
},
req.extras,
req.marker,
)
}
pep508_rs::VersionOrUrl::Url(u) => {
let url = u.to_url();
if let Some((prefix, ..)) = url.scheme().split_once('+') {
Expand All @@ -30,9 +33,10 @@ impl TryFrom<pep508_rs::Requirement> for PixiPypiSpec {
subdirectory,
};

PixiPypiSpec::with_extras(
PixiPypiSpec::with_extras_and_markers(
PixiPypiSource::Git { git: git_spec },
req.extras,
req.marker,
)
}
"bzr" => {
Expand Down Expand Up @@ -75,35 +79,42 @@ impl TryFrom<pep508_rs::Requirement> for PixiPypiSpec {
rev: Some(git_url.reference().clone().into()),
subdirectory,
};
PixiPypiSpec::with_extras(PixiPypiSource::Git { git: git_spec }, req.extras)
PixiPypiSpec::with_extras_and_markers(
PixiPypiSource::Git { git: git_spec },
req.extras,
req.marker,
)
} else if url.scheme().eq_ignore_ascii_case("file") {
// Convert the file url to a path.
let file = url.to_file_path().map_err(|_| {
Pep508ToPyPiRequirementError::PathUrlIntoPath(url.clone())
})?;
PixiPypiSpec::with_extras(
PixiPypiSpec::with_extras_and_markers(
PixiPypiSource::Path {
path: file,
editable: None,
},
req.extras,
req.marker,
)
} else {
let subdirectory = extract_directory_from_url(&url);
PixiPypiSpec::with_extras(
PixiPypiSpec::with_extras_and_markers(
PixiPypiSource::Url { url, subdirectory },
req.extras,
req.marker,
)
}
}
}
} else if !req.extras.is_empty() {
PixiPypiSpec::with_extras(
PixiPypiSpec::with_extras_and_markers(
PixiPypiSource::Registry {
version: VersionOrStar::Star,
index: None,
},
req.extras,
req.marker,
)
} else {
PixiPypiSpec::new(PixiPypiSource::Registry {
Expand Down
Loading
Loading