Skip to content
2 changes: 1 addition & 1 deletion docs/reference/cli/pixi/import.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ pixi import [OPTIONS] <FILE>
## Options
- <a id="arg---format" href="#arg---format">`--format <FORMAT>`</a>
: Which format to interpret the file as
<br>**options**: `conda-env`
<br>**options**: `conda-env`, `pypi-txt`
- <a id="arg---platform" href="#arg---platform">`--platform (-p) <PLATFORM>`</a>
: The platforms for the imported environment
<br>May be provided more than once.
Expand Down
132 changes: 99 additions & 33 deletions src/cli/import.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,12 @@ use clap::{Parser, ValueEnum};
use pixi_config::{Config, ConfigCli};
use pixi_manifest::{EnvironmentName, FeatureName, HasFeaturesIter, PrioritizedChannel};
use pixi_utils::conda_environment_file::CondaEnvFile;
use pixi_uv_conversions::convert_uv_requirements_to_pep508;
use rattler_conda_types::Platform;

use uv_client::BaseClientBuilder;
use uv_requirements_txt::RequirementsTxt;

use miette::{Diagnostic, IntoDiagnostic, Result};
use thiserror::Error;

Expand All @@ -19,14 +23,15 @@ use crate::{

#[derive(Parser, Debug, Clone, PartialEq, ValueEnum)]
pub enum ImportFileFormat {
// TODO: implement conda-lock, conda-txt, pypi-txt
// TODO: implement conda-lock, conda-txt
CondaEnv,
PypiTxt,
}

/// Imports a file into an environment in an existing workspace.
///
/// If `--format` isn't provided, `import` will try to guess the format based on the file extension.
#[derive(Parser, Debug, Default)]
#[derive(Parser, Debug, Default, Clone)]
#[clap(arg_required_else_help = true, verbatim_doc_comment)]
pub struct Args {
#[clap(flatten)]
Expand Down Expand Up @@ -64,32 +69,29 @@ pub struct Args {

pub async fn execute(args: Args) -> miette::Result<()> {
if let Some(format) = &args.format {
if *format != ImportFileFormat::CondaEnv {
miette::bail!(
"Only the conda environment.yml format is supported currently. Please pass `conda-env` to `format`."
);
}
import_conda_env(args).await
import(args.clone(), format).await
} else if let Ok(result) = import(args.clone(), &ImportFileFormat::CondaEnv).await {
return Ok(result);
} else if let Ok(result) = import(args, &ImportFileFormat::PypiTxt).await {
return Ok(result);
} else {
import_conda_env(args).await // .or_else(...)
miette::bail!(
"Tried all formats for input file, but none were successful. Pass a `format` argument to see the specific error for that format."
)
}
}

#[derive(Debug, Error, Diagnostic)]
#[error("Missing name: provide --feature or --environment, or set `name:` in input file.")]
#[error(
"Missing name: provide --feature or --environment, or set `name:` in input file for the conda-env format."
)]
struct MissingEnvironmentName;

fn get_feature_and_environment(
feature_arg: &Option<String>,
environment_arg: &Option<String>,
file: &CondaEnvFile,
fallback: impl Fn() -> Result<String, MissingEnvironmentName>,
) -> Result<(String, String), miette::Report> {
let fallback = || {
file.name()
.map(|s| s.to_string())
.ok_or(MissingEnvironmentName)
};

let feature_string = match (feature_arg, environment_arg) {
(Some(f), _) => f.clone(),
(_, Some(e)) => e.clone(),
Expand All @@ -105,8 +107,32 @@ fn get_feature_and_environment(
Ok((feature_string, environment_string))
}

async fn import_conda_env(args: Args) -> miette::Result<()> {
let (file, platforms, workspace_config) = (args.file, args.platforms, args.workspace_config);
// TODO: better errors
fn convert_uv_requirements_txt_to_pep508(
reqs_txt: uv_requirements_txt::RequirementsTxt,
) -> Result<Vec<pep508_rs::Requirement>, miette::Error> {
let mut uv_requirements: Vec<uv_pep508::Requirement<uv_pypi_types::VerbatimParsedUrl>> =
reqs_txt
.requirements
.into_iter()
.map(|r| match r.requirement {
uv_requirements_txt::RequirementsTxtRequirement::Named(req) => Ok(req),
uv_requirements_txt::RequirementsTxtRequirement::Unnamed(_) => {
Err(miette::miette!("Unnamed requirements unsupported."))
}
})
.collect::<Result<_, _>>()?;
uv_requirements.extend(reqs_txt.constraints);
Comment thread
lucascolley marked this conversation as resolved.
Outdated

let requirements =
convert_uv_requirements_to_pep508(uv_requirements.iter()).into_diagnostic()?;

Ok(requirements)
}

async fn import(args: Args, format: &ImportFileFormat) -> miette::Result<()> {
let (input_file, platforms, workspace_config) =
(args.file, args.platforms, args.workspace_config);
let config = Config::from(args.config);

let workspace = WorkspaceLocator::for_cli()
Expand All @@ -120,9 +146,29 @@ async fn import_conda_env(args: Args) -> miette::Result<()> {

// TODO: add dry_run logic to import

let file = CondaEnvFile::from_path(&file)?;
let (feature_string, environment_string) =
get_feature_and_environment(&args.feature, &args.environment, &file)?;
let (env_file, feature_string, environment_string) = match format {
Comment thread
lucascolley marked this conversation as resolved.
Outdated
Comment thread
lucascolley marked this conversation as resolved.
Outdated
ImportFileFormat::CondaEnv => {
// TODO: refactor to reduce duplication — custom enum for env_file?
let env_file = CondaEnvFile::from_path(&input_file)?;
let fallback = || {
env_file
.name()
.map(|s| s.to_string())
.ok_or(MissingEnvironmentName)
};
let (feature_string, environment_string) =
get_feature_and_environment(&args.feature, &args.environment, fallback)?;
(Some(env_file), feature_string, environment_string)
}
ImportFileFormat::PypiTxt => {
let (feature_string, environment_string) =
get_feature_and_environment(&args.feature, &args.environment, || {
Err(MissingEnvironmentName)
})?;
(None, feature_string, environment_string)
}
};

let feature_name = FeatureName::from(feature_string.clone());

// Add the platforms if they are not already present
Expand All @@ -132,17 +178,37 @@ async fn import_conda_env(args: Args) -> miette::Result<()> {
.add_platforms(platforms.iter(), &feature_name)?;
}

// TODO: handle `variables` section
// let env_vars = file.variables();

// TODO: Improve this:
// - Use .condarc as channel config
let (conda_deps, pypi_deps, channels) = file.to_manifest(&config.clone())?;
workspace.manifest().add_channels(
channels.iter().map(|c| PrioritizedChannel::from(c.clone())),
&feature_name,
false,
)?;
let (conda_deps, pypi_deps) = match format {
ImportFileFormat::CondaEnv => {
// TODO: handle `variables` section
// let env_vars = file.variables();

// TODO: Improve this:
// - Use .condarc as channel config
let env_file = env_file.expect("Some is returned for CondaEnv");
let (conda_deps, pypi_deps, channels) = env_file.to_manifest(&config.clone())?;
workspace.manifest().add_channels(
channels.iter().map(|c| PrioritizedChannel::from(c.clone())),
&feature_name,
false,
)?;

(conda_deps, pypi_deps)
}
ImportFileFormat::PypiTxt => {
let reqs_txt = RequirementsTxt::parse(
&input_file,
workspace.workspace().root(),
&BaseClientBuilder::new(),
)
.await
// TODO: proper error handling
.expect("file should parse");
let pypi_deps = convert_uv_requirements_txt_to_pep508(reqs_txt)?;

(vec![], pypi_deps)
}
};

workspace.add_specs(conda_deps, pypi_deps, &platforms, &feature_name)?;

Expand Down
Loading
Loading