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: add import yml flag #792

Merged
merged 12 commits into from
Feb 22, 2024
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ serde-untagged = "0.1.5"
serde_json = "1.0.111"
serde_spanned = "0.6.5"
serde_with = { version = "3.5.0", features = ["indexmap"] }
serde_yaml = "0.9.31"
shlex = "1.3.0"
spdx = "0.10.3"
strsim = "0.10.0"
Expand Down
185 changes: 163 additions & 22 deletions src/cli/init.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
use crate::cli::add::{add_conda_specs_to_project, add_pypi_specs_to_project};
use crate::project::manifest::PyPiRequirement;
use crate::Project;
use crate::{config::get_default_author, consts};
use clap::Parser;
use miette::IntoDiagnostic;
use minijinja::{context, Environment};
use rattler_conda_types::Platform;
use rattler_conda_types::{MatchSpec, Platform};
use rip::types::PackageName;
use serde::Deserialize;
use std::fs::File;
use std::io::{Error, ErrorKind, Write};
use std::path::Path;
use std::str::FromStr;
use std::{fs, path::PathBuf};

/// Creates a new project
Expand All @@ -15,12 +22,30 @@ pub struct Args {
pub path: PathBuf,

/// Channels to use in the project.
#[arg(short, long = "channel", id = "channel")]
#[arg(short, long = "channel", id = "channel", conflicts_with = "env_file")]
pub channels: Option<Vec<String>>,

/// Platforms that the project supports.
#[arg(short, long = "platform", id = "platform")]
pub platforms: Vec<String>,

/// Environment.yml file to bootstrap the project.
#[arg(short = 'i', long = "import")]
pub env_file: Option<PathBuf>,
}

#[derive(Deserialize, Debug)]
pub struct CondaEnvFile {
name: String,
channels: Vec<String>,
dependencies: Vec<CondaEnvDep>,
}

#[derive(Deserialize, Debug)]
#[serde(untagged)]
pub enum CondaEnvDep {
Conda(String),
Pip { pip: Vec<String> },
}

/// The default channels to use for a new project.
Expand Down Expand Up @@ -70,28 +95,40 @@ pub async fn execute(args: Args) -> miette::Result<()> {
// Fail silently if it already exists or cannot be created.
fs::create_dir_all(&dir).ok();

// Write pixi.toml
let name = dir
.file_name()
.ok_or_else(|| {
miette::miette!(
"Cannot get file or directory name from the path: {}",
dir.to_string_lossy()
)
})?
.to_string_lossy();
let version = "0.1.0";
let author = get_default_author();
let channels = if let Some(channels) = args.channels {
channels
let (name, channels, conda_deps, pip_deps) = if let Some(env_file) = args.env_file {
let env_info = read_env_yml(env_file)?;
let name = env_info.name;
let channels = env_info.channels;
let (conda_deps, pip_deps) = parse_dependencies(env_info.dependencies)?;

(name, channels, conda_deps, pip_deps)
} else {
DEFAULT_CHANNELS
.iter()
.copied()
.map(ToOwned::to_owned)
.collect()
let name = dir
.file_name()
.ok_or_else(|| {
miette::miette!(
"Cannot get file or directory name from the path: {}",
dir.to_string_lossy()
)
})?
.to_string_lossy()
.to_string();

let channels = if let Some(channels) = args.channels {
channels
} else {
DEFAULT_CHANNELS
.iter()
.copied()
.map(ToOwned::to_owned)
.collect()
};

(name, channels, vec![], vec![])
};

let version = "0.1.0";
let author = get_default_author();
let platforms = if args.platforms.is_empty() {
vec![Platform::current().to_string()]
} else {
Expand All @@ -111,7 +148,26 @@ pub async fn execute(args: Args) -> miette::Result<()> {
},
)
.unwrap();
fs::write(&manifest_path, rv).into_diagnostic()?;

if conda_deps.is_empty() && pip_deps.is_empty() {
fs::write(&manifest_path, rv).into_diagnostic()?;
} else {
let mut project = Project::from_str(&dir, &rv)?;

add_conda_specs_to_project(
&mut project,
conda_deps,
crate::SpecType::Run,
true,
true,
&vec![],
)
.await?;

add_pypi_specs_to_project(&mut project, pip_deps, &vec![], true, true).await?;

project.save()?;
}

// create a .gitignore if one is missing
if let Err(e) = create_or_append_file(&gitignore_path, GITIGNORE_TEMPLATE) {
Expand Down Expand Up @@ -176,6 +232,39 @@ fn get_dir(path: PathBuf) -> Result<PathBuf, Error> {
}
}

type PipReq = (PackageName, PyPiRequirement);

fn parse_dependencies(deps: Vec<CondaEnvDep>) -> miette::Result<(Vec<MatchSpec>, Vec<PipReq>)> {
let mut conda_deps = vec![];
let mut pip_deps = vec![];

for dep in deps {
match dep {
CondaEnvDep::Conda(d) => conda_deps.push(MatchSpec::from_str(&d).into_diagnostic()?),
CondaEnvDep::Pip { pip } => pip_deps.extend(
pip.into_iter()
.map(|d| {
let req = pep508_rs::Requirement::from_str(&d).into_diagnostic()?;
let name = rip::types::PackageName::from_str(req.name.as_str())?;
let requirement = PyPiRequirement::from(req);
Ok((name, requirement))
})
.collect::<miette::Result<Vec<_>>>()?,
),
}
}

if !pip_deps.is_empty() {
conda_deps.push(MatchSpec::from_str("pip").into_diagnostic()?);
}

Ok((conda_deps, pip_deps))
}

fn read_env_yml(path: PathBuf) -> miette::Result<CondaEnvFile> {
baszalmstra marked this conversation as resolved.
Show resolved Hide resolved
serde_yaml::from_reader(File::open(path).into_diagnostic()?).into_diagnostic()
}

#[cfg(test)]
mod tests {
use super::*;
Expand All @@ -184,6 +273,58 @@ mod tests {
use std::path::{Path, PathBuf};
use tempfile::tempdir;

#[test]
fn test_parse_conda_env_file() {
let example_conda_env_file = r#"
name: pixi_example_project
channels:
- conda-forge
dependencies:
- python
- pip:
- requests
"#;
let conda_env_file_data: CondaEnvFile =
serde_yaml::from_str(example_conda_env_file).unwrap();

assert_eq!(conda_env_file_data.name, "pixi_example_project");
assert_eq!(
conda_env_file_data.channels,
vec!["conda-forge".to_string()]
);

let (conda_deps, pip_deps) = parse_dependencies(conda_env_file_data.dependencies).unwrap();

println!("{conda_deps:?}");
assert_eq!(
conda_deps,
vec![
MatchSpec::from_str("python").unwrap(),
MatchSpec::from_str("pip").unwrap()
]
);

assert_eq!(
conda_deps,
vec![
MatchSpec::from_str("python").unwrap(),
MatchSpec::from_str("pip").unwrap()
]
);

assert_eq!(
ruben-arts marked this conversation as resolved.
Show resolved Hide resolved
pip_deps,
vec![(
PackageName::from_str("requests").unwrap(),
PyPiRequirement {
version: None,
extras: None,
index: None,
},
)]
);
}

#[test]
fn test_get_name() {
assert_eq!(
Expand Down
4 changes: 2 additions & 2 deletions tests/common/builders.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,10 @@ impl InitBuilder {

impl IntoFuture for InitBuilder {
type Output = miette::Result<()>;
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send + 'static>>;
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + 'static>>;

fn into_future(self) -> Self::IntoFuture {
Box::pin(init::execute(self.args))
init::execute(self.args).boxed_local()
}
}

Expand Down
2 changes: 2 additions & 0 deletions tests/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ impl PixiControl {
path: self.project_path().to_path_buf(),
channels: None,
platforms: Vec::new(),
env_file: None,
},
}
}
Expand All @@ -196,6 +197,7 @@ impl PixiControl {
path: self.project_path().to_path_buf(),
channels: None,
platforms,
env_file: None,
},
}
}
Expand Down
Loading