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

feat: Implement more functions for pixi add --pypi #1244

Merged
merged 14 commits into from
May 10, 2024
3 changes: 3 additions & 0 deletions docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,9 @@ pixi add --manifest-path ~/myproject/pixi.toml numpy
pixi add --host "python>=3.9.0"
pixi add --build cmake
pixi add --pypi requests[security]
pixi add --pypi "boltons @ https://files.pythonhosted.org/packages/46/35/e50d4a115f93e2a3fbf52438435bb2efcf14c11d4fcd6bdcd77a6fc399c9/boltons-24.0.0-py3-none-any.whl"
pixi add --pypi "exchangelib @ git+https://github.com/ecederstrand/exchangelib"
pixi add --pypi "project @ file:///absolute/path/to/project"
pixi add --platform osx-64 --build clang
pixi add --no-install numpy
pixi add --no-lock file-update numpy
Expand Down
19 changes: 15 additions & 4 deletions src/cli/add.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ use clap::Parser;
use itertools::{Either, Itertools};

use crate::project::grouped_environment::GroupedEnvironment;
// use crate::project::manifest::python::PyPiPackageName;
// use crate::project::manifest::PyPiRequirement;
use indexmap::IndexMap;
use miette::{IntoDiagnostic, WrapErr};
use rattler_conda_types::{
Expand Down Expand Up @@ -93,6 +95,10 @@ pub struct Args {

#[clap(flatten)]
pub config: ConfigCli,

/// Whether the pypi requirement should be editable
#[arg(long, requires = "pypi")]
pub editable: bool,
}

impl DependencyType {
Expand Down Expand Up @@ -170,6 +176,7 @@ pub async fn execute(args: Args) -> miette::Result<()> {
spec_platforms,
args.no_lockfile_update,
args.no_install,
Some(args.editable),
)
.await
}
Expand Down Expand Up @@ -213,19 +220,23 @@ pub async fn add_pypi_requirements_to_project(
platforms: &[Platform],
no_update_lockfile: bool,
no_install: bool,
editable: Option<bool>,
) -> miette::Result<()> {
for requirement in &requirements {
// TODO: Get best version
// Add the dependency to the project
if platforms.is_empty() {
project
.manifest
.add_pypi_dependency(requirement, None, feature_name)?;
.add_pypi_dependency(requirement, None, feature_name, editable)?;
} else {
for platform in platforms.iter() {
project
.manifest
.add_pypi_dependency(requirement, Some(*platform), feature_name)?;
project.manifest.add_pypi_dependency(
requirement,
Some(*platform),
feature_name,
editable,
)?;
}
}
}
Expand Down
1 change: 1 addition & 0 deletions src/cli/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ pub async fn execute(args: Args) -> miette::Result<()> {
&requirement,
Some(platform.parse().into_diagnostic()?),
&FeatureName::default(),
None,
)?;
}
}
Expand Down
3 changes: 2 additions & 1 deletion src/project/manifest/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -333,10 +333,11 @@ impl Manifest {
requirement: &pep508_rs::Requirement,
platform: Option<Platform>,
feature_name: &FeatureName,
editable: Option<bool>,
) -> miette::Result<()> {
// Add the pypi dependency to the manifest
self.get_or_insert_target_mut(platform, Some(feature_name))
.try_add_pypi_dependency(requirement)?;
.try_add_pypi_dependency(requirement, editable)?;
// and to the TOML document
self.document
.add_pypi_dependency(requirement, platform, feature_name)?;
Expand Down
13 changes: 9 additions & 4 deletions src/project/manifest/pyproject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ impl From<PyProjectManifest> for ProjectManifest {
// Add pyproject dependencies as pypi dependencies
if let Some(deps) = &pyproject.dependencies {
for requirement in deps.iter() {
target.add_pypi_dependency(requirement);
target.add_pypi_dependency(requirement, None);
}
}

Expand All @@ -110,7 +110,7 @@ impl From<PyProjectManifest> for ProjectManifest {
for requirement in reqs.iter() {
// filter out any self references in groups of extra dependencies
if project_name != requirement.name {
target.add_pypi_dependency(requirement);
target.add_pypi_dependency(requirement, None);
}
}
}
Expand Down Expand Up @@ -409,7 +409,7 @@ mod tests {
// Add numpy to pyproject
let requirement = pep508_rs::Requirement::from_str("numpy>=3.12").unwrap();
manifest
.add_pypi_dependency(&requirement, None, &FeatureName::Default)
.add_pypi_dependency(&requirement, None, &FeatureName::Default, None)
.unwrap();

assert!(manifest
Expand All @@ -426,7 +426,12 @@ mod tests {
// Add numpy to feature in pyproject
let requirement = pep508_rs::Requirement::from_str("pytest>=3.12").unwrap();
manifest
.add_pypi_dependency(&requirement, None, &FeatureName::Named("test".to_string()))
.add_pypi_dependency(
&requirement,
None,
&FeatureName::Named("test".to_string()),
None,
)
.unwrap();
assert!(manifest
.feature(&FeatureName::Named("test".to_string()))
Expand Down
201 changes: 174 additions & 27 deletions src/project/manifest/python.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,18 @@ impl PyPiRequirement {
| PyPiRequirement::Url { .. }
)
}

/// Define whether the requirement is editable.
pub fn set_editable(&mut self, editable: bool) {
match self {
PyPiRequirement::Path { editable: e, .. } => {
*e = Some(editable);
}
_ => {
tracing::error!("Cannot add editable to a non path like dependency.");
}
}
}
}

impl Default for PyPiRequirement {
Expand Down Expand Up @@ -209,24 +221,68 @@ impl From<PyPiRequirement> for toml_edit::Value {
toml_edit::Value::InlineTable(table.to_owned())
}
PyPiRequirement::Git {
git: _,
branch: _,
tag: _,
rev: _,
git,
branch,
tag,
rev,
subdirectory: _,
extras: _,
extras,
} => {
todo!("git")
let mut table = toml_edit::Table::new().into_inline_table();
table.insert(
"git",
toml_edit::Value::String(toml_edit::Formatted::new(git.to_string())),
);
if let Some(branch) = branch {
table.insert(
"branch",
toml_edit::Value::String(toml_edit::Formatted::new(branch.clone())),
);
}
if let Some(tag) = tag {
table.insert(
"tag",
toml_edit::Value::String(toml_edit::Formatted::new(tag.clone())),
);
}
if let Some(rev) = rev {
table.insert(
"rev",
toml_edit::Value::String(toml_edit::Formatted::new(rev.clone())),
);
}
insert_extras(&mut table, extras);
toml_edit::Value::InlineTable(table.to_owned())
}
PyPiRequirement::Path {
path: _,
editable: _,
extras: _,
path,
editable,
extras,
} => {
todo!("path")
let mut table = toml_edit::Table::new().into_inline_table();
table.insert(
"path",
toml_edit::Value::String(toml_edit::Formatted::new(
path.to_string_lossy().to_string(),
)),
);
if editable == &Some(true) {
table.insert(
"editable",
toml_edit::Value::Boolean(toml_edit::Formatted::new(true)),
);
}
insert_extras(&mut table, extras);
toml_edit::Value::InlineTable(table.to_owned())
}
PyPiRequirement::Url { url: _, extras: _ } => {
unimplemented!("url")
PyPiRequirement::Url { url, extras } => {
let mut table = toml_edit::Table::new().into_inline_table();
table.insert(
"url",
toml_edit::Value::String(toml_edit::Formatted::new(url.to_string())),
);
insert_extras(&mut table, extras);
toml_edit::Value::InlineTable(table.to_owned())
}
PyPiRequirement::RawVersion(version) => {
toml_edit::Value::String(toml_edit::Formatted::new(version.to_string()))
Expand All @@ -245,23 +301,50 @@ impl From<pep508_rs::Requirement> for PyPiRequirement {
extras: req.extras,
},
pep508_rs::VersionOrUrl::Url(u) => {
let url = u.to_url();
// Have a different code path when the url is a file.
// i.e. package @ file:///path/to/package
if url.scheme() == "file" {
// Convert the file url to a path.
let file = url
.to_file_path()
.expect("could not convert to file url to path");
PyPiRequirement::Path {
path: file,
editable: None,
extras: req.extras,
// If serialization starts with `git+` then it is a git url.
if let Some(stripped_url) = u.to_string().strip_prefix("git+") {
if let Some((url, version)) = stripped_url.split_once('@') {
let url = Url::parse(url)
.expect("expect proper url as it is previously parsed");
PyPiRequirement::Git {
git: url,
branch: None,
tag: None,
rev: Some(version.to_string()),
subdirectory: None,
extras: req.extras,
}
} else {
let url = Url::parse(stripped_url)
.expect("expect proper url as it is previously parsed");
PyPiRequirement::Git {
git: url,
branch: None,
tag: None,
rev: None,
subdirectory: None,
extras: req.extras,
}
}
} else {
PyPiRequirement::Url {
url,
extras: req.extras,
let url = u.to_url();
// Have a different code path when the url is a file.
// i.e. package @ file:///path/to/package
if url.scheme() == "file" {
// Convert the file url to a path.
let file = url
.to_file_path()
.expect("could not convert to file url to path");
PyPiRequirement::Path {
path: file,
editable: None,
extras: req.extras,
}
} else {
PyPiRequirement::Url {
url,
extras: req.extras,
}
}
}
}
Expand Down Expand Up @@ -791,5 +874,69 @@ mod tests {
let as_pypi_req: PyPiRequirement = pypi.into();
// convert to toml and snapshot
assert_snapshot!(as_pypi_req.to_string());

let pypi: Requirement = "exchangelib @ git+https://github.com/ecederstrand/exchangelib"
.parse()
.unwrap();
let as_pypi_req: PyPiRequirement = pypi.into();
assert_eq!(
as_pypi_req,
PyPiRequirement::Git {
git: Url::parse("https://github.com/ecederstrand/exchangelib").unwrap(),
branch: None,
tag: None,
rev: None,
subdirectory: None,
extras: vec![]
}
);

let pypi: Requirement = "exchangelib @ git+https://github.com/ecederstrand/exchangelib@b283011c6df4a9e034baca9aea19aa8e5a70e3ab".parse().unwrap();
let as_pypi_req: PyPiRequirement = pypi.into();
assert_eq!(
as_pypi_req,
PyPiRequirement::Git {
git: Url::parse("https://github.com/ecederstrand/exchangelib").unwrap(),
branch: None,
tag: None,
rev: Some("b283011c6df4a9e034baca9aea19aa8e5a70e3ab".to_string()),
subdirectory: None,
extras: vec![]
}
);

let pypi: Requirement = "boltons @ https://files.pythonhosted.org/packages/46/35/e50d4a115f93e2a3fbf52438435bb2efcf14c11d4fcd6bdcd77a6fc399c9/boltons-24.0.0-py3-none-any.whl".parse().unwrap();
let as_pypi_req: PyPiRequirement = pypi.into();
assert_eq!(as_pypi_req, PyPiRequirement::Url{url: Url::parse("https://files.pythonhosted.org/packages/46/35/e50d4a115f93e2a3fbf52438435bb2efcf14c11d4fcd6bdcd77a6fc399c9/boltons-24.0.0-py3-none-any.whl").unwrap(), extras: vec![] });

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: PyPiRequirement = pypi.into();
assert_eq!(as_pypi_req, PyPiRequirement::Url{url: Url::parse("https://files.pythonhosted.org/packages/46/35/e50d4a115f93e2a3fbf52438435bb2efcf14c11d4fcd6bdcd77a6fc399c9/boltons-24.0.0-py3-none-any.whl").unwrap(), extras: vec![ExtraName::new("nichita".to_string()).unwrap()] });

#[cfg(target_os = "windows")]
let pypi: Requirement = "boltons @ file:///C:/path/to/boltons".parse().unwrap();
#[cfg(not(target_os = "windows"))]
let pypi: Requirement = "boltons @ file:///path/to/boltons".parse().unwrap();

let as_pypi_req: PyPiRequirement = pypi.into();

#[cfg(target_os = "windows")]
assert_eq!(
as_pypi_req,
PyPiRequirement::Path {
path: PathBuf::from("C:/path/to/boltons"),
editable: None,
extras: vec![]
}
);
#[cfg(not(target_os = "windows"))]
assert_eq!(
as_pypi_req,
PyPiRequirement::Path {
path: PathBuf::from("/path/to/boltons"),
editable: None,
extras: vec![]
}
);
}
}
Loading
Loading