From 3e1b3326591a9e5acfc8b2dca3d1685b3b79d7d4 Mon Sep 17 00:00:00 2001 From: Jeff Dickey <216188+jdx@users.noreply.github.com> Date: Sat, 24 Feb 2024 00:56:58 -0500 Subject: [PATCH] added env._.python.venv directive --- e2e/test_python | 6 +- schema/mise.json | 27 +++++++++ src/cli/settings/ls.rs | 2 - src/cli/settings/set.rs | 1 - src/cli/settings/unset.rs | 1 - src/config/config_file/mise_toml.rs | 88 +++++++++++++++++++++++++++++ src/config/env_directive.rs | 77 ++++++++++++++++++++++--- src/config/settings.rs | 18 ++++-- src/plugins/core/python.rs | 2 - tests/cli/install.rs | 8 +-- 10 files changed, 207 insertions(+), 23 deletions(-) diff --git a/e2e/test_python b/e2e/test_python index d108f3be8..211a6e165 100755 --- a/e2e/test_python +++ b/e2e/test_python @@ -7,12 +7,14 @@ export MISE_EXPERIMENTAL=1 export MISE_PYTHON_DEFAULT_PACKAGES_FILE="$ROOT/e2e/.default-python-packages" cat >.e2e.mise.toml < de::Deserialize<'de> for EnvList { while let Some(key) = map.next_key::()? { match key.as_str() { "_" | "mise" => { + struct EnvDirectivePythonVenv { + path: PathBuf, + create: bool, + } + + #[derive(Deserialize, Default)] + #[serde(deny_unknown_fields)] + struct EnvDirectivePython { + #[serde(default)] + venv: Option, + } + #[derive(Deserialize)] #[serde(deny_unknown_fields)] struct EnvDirectives { @@ -435,7 +447,77 @@ impl<'de> de::Deserialize<'de> for EnvList { file: Vec, #[serde(default, deserialize_with = "deserialize_arr")] source: Vec, + #[serde(default)] + python: EnvDirectivePython, + } + + impl<'de> de::Deserialize<'de> for EnvDirectivePythonVenv { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct EnvDirectivePythonVenvVisitor; + + impl<'de> Visitor<'de> for EnvDirectivePythonVenvVisitor { + type Value = EnvDirectivePythonVenv; + fn expecting( + &self, + formatter: &mut Formatter, + ) -> std::fmt::Result + { + formatter.write_str("python venv directive") + } + + fn visit_str(self, v: &str) -> Result + where + E: de::Error, + { + Ok(EnvDirectivePythonVenv { + path: v.into(), + create: false, + }) + } + + fn visit_map( + self, + mut map: M, + ) -> Result + where + M: de::MapAccess<'de>, + { + let mut path = None; + let mut create = false; + while let Some(key) = map.next_key::()? { + match key.as_str() { + "path" => { + path = Some(map.next_value()?); + } + "create" => { + create = map.next_value()?; + } + _ => { + return Err(de::Error::unknown_field( + &key, + &["path", "create"], + )); + } + } + } + let path = path + .ok_or_else(|| de::Error::missing_field("path"))?; + Ok(EnvDirectivePythonVenv { path, create }) + } + } + + const FIELDS: &[&str] = &["path", "create"]; + deserializer.deserialize_struct( + "PythonVenv", + FIELDS, + EnvDirectivePythonVenvVisitor, + ) + } } + let directives = map.next_value::()?; // TODO: parse these in the order they're defined somehow for path in directives.path { @@ -447,6 +529,12 @@ impl<'de> de::Deserialize<'de> for EnvList { for source in directives.source { env.push(EnvDirective::Source(source)); } + if let Some(venv) = directives.python.venv { + env.push(EnvDirective::PythonVenv { + path: venv.path, + create: venv.create, + }); + } } _ => { enum Val { diff --git a/src/config/env_directive.rs b/src/config/env_directive.rs index 98aa70dc8..b3d27c253 100644 --- a/src/config/env_directive.rs +++ b/src/config/env_directive.rs @@ -1,14 +1,18 @@ +use std::collections::{BTreeSet, HashMap}; +use std::fmt::Display; +use std::path::{Path, PathBuf}; + +use eyre::Context; +use indexmap::IndexMap; + +use crate::cmd::CmdLineRunner; use crate::config::config_file::trust_check; -use crate::config::Settings; +use crate::config::{Config, Settings}; use crate::env_diff::{EnvDiff, EnvDiffOperation}; use crate::file::display_path; use crate::tera::{get_tera, BASE_CONTEXT}; +use crate::toolset::ToolsetBuilder; use crate::{dirs, env}; -use eyre::Context; -use indexmap::IndexMap; -use std::collections::{BTreeSet, HashMap}; -use std::fmt::Display; -use std::path::{Path, PathBuf}; #[derive(Debug, Clone)] pub enum EnvDirective { @@ -22,6 +26,10 @@ pub enum EnvDirective { Path(PathBuf), /// run a bash script and apply the resulting env diff Source(PathBuf), + PythonVenv { + path: PathBuf, + create: bool, + }, } impl From<(String, String)> for EnvDirective { @@ -44,6 +52,13 @@ impl Display for EnvDirective { EnvDirective::File(path) => write!(f, "dotenv {}", display_path(path)), EnvDirective::Path(path) => write!(f, "path_add {}", display_path(path)), EnvDirective::Source(path) => write!(f, "source {}", display_path(path)), + EnvDirective::PythonVenv { path, create } => { + write!(f, "python venv path={}", display_path(path))?; + if *create { + write!(f, " create")?; + } + Ok(()) + } } } } @@ -143,6 +158,53 @@ impl EnvResults { } } } + EnvDirective::PythonVenv { path, create } => { + trace!("python venv: {} create={create}", display_path(&path)); + trust_check(&source)?; + let venv = r.parse_template(&ctx, &source, path.to_string_lossy().as_ref())?; + let venv = normalize_path(venv.into()); + if !venv.exists() && create { + // TODO: the toolset stuff doesn't feel like it's in the right place here + let config = Config::get(); + let ts = ToolsetBuilder::new().build(&config)?; + let path = ts + .list_paths() + .into_iter() + .chain(env::split_paths(&env_vars["PATH"])) + .collect::>(); + let cmd = CmdLineRunner::new("python3") + .args(["-m", "venv", &venv.to_string_lossy()]) + .envs(&env_vars) + .env( + "PATH", + env::join_paths(&path)?.to_string_lossy().to_string(), + ); + if ts + .list_missing_versions() + .iter() + .any(|tv| tv.forge.name == "python") + { + debug!("python not installed, skipping venv creation"); + } else { + info!("creating venv at: {}", display_path(&venv)); + cmd.execute()?; + } + } + if venv.exists() { + r.env_paths.push(venv.join("bin")); + env.insert( + "VIRTUAL_ENV".into(), + (venv.to_string_lossy().to_string(), Some(source.clone())), + ); + } else { + warn!( + "no venv found at: {p}\n\n\ + To create a virtualenv manually, run:\n\ + python -m venv {p}", + p = display_path(&venv) + ); + } + } }; } for (k, (v, source)) in env { @@ -173,9 +235,10 @@ impl EnvResults { #[cfg(test)] mod tests { - use super::*; use crate::test::replace_path; + use super::*; + #[test] fn test_env_path() { let mut env = HashMap::new(); diff --git a/src/config/settings.rs b/src/config/settings.rs index 791ef98cd..d661f22d4 100644 --- a/src/config/settings.rs +++ b/src/config/settings.rs @@ -102,8 +102,6 @@ pub struct Settings { pub python_precompiled_os: Option, #[config(env = "MISE_PYENV_REPO", default = "https://github.com/pyenv/pyenv.git")] pub python_pyenv_repo: String, - #[config(env = "MISE_PYTHON_VENV_AUTO_CREATE", default = false)] - pub python_venv_auto_create: bool, #[config(env = "MISE_RAW", default = false)] pub raw: bool, #[config(env = "MISE_SHORTHANDS_FILE")] @@ -135,6 +133,8 @@ pub struct Settings { pub trace: bool, #[config(env = "MISE_LOG_LEVEL", default = "info")] pub log_level: String, + #[config(env = "MISE_PYTHON_VENV_AUTO_CREATE", default = false)] + pub python_venv_auto_create: bool, } #[derive(Config, Default, Debug, Clone, Serialize)] @@ -349,8 +349,18 @@ impl Settings { } pub fn hidden_configs() -> &'static HashSet<&'static str> { - static HIDDEN_CONFIGS: Lazy> = - Lazy::new(|| ["ci", "cd", "debug", "env_file", "trace", "log_level"].into()); + static HIDDEN_CONFIGS: Lazy> = Lazy::new(|| { + [ + "ci", + "cd", + "debug", + "env_file", + "trace", + "log_level", + "python_venv_auto_create", + ] + .into() + }); &HIDDEN_CONFIGS } diff --git a/src/plugins/core/python.rs b/src/plugins/core/python.rs index 7bdc2e52c..2f623a7de 100644 --- a/src/plugins/core/python.rs +++ b/src/plugins/core/python.rs @@ -249,8 +249,6 @@ impl PythonPlugin { } else { warn!( "no venv found at: {p}\n\n\ - To have mise automatically create virtualenvs, run:\n\ - mise settings set python_venv_auto_create true\n\n\ To create a virtualenv manually, run:\n\ python -m venv {p}", p = display_path(&virtualenv) diff --git a/tests/cli/install.rs b/tests/cli/install.rs index 0e59397ab..00b81cab9 100644 --- a/tests/cli/install.rs +++ b/tests/cli/install.rs @@ -196,12 +196,12 @@ fn test_python() -> Result<()> { fn python_config_fixture() -> File { File { path: ".mise.toml".into(), - content: toml::toml!{ - [settings] - python_venv_auto_create = true + content: toml::toml! { + [env] + "_".python.venv = {path="{{env.MISE_DATA_DIR}}/venv", create=true} [tools] - python = {version = "{{exec(command='echo 3.12.0')}}", virtualenv="{{env.MISE_DATA_DIR}}/venv"} + python = "{{exec(command='echo 3.12.0')}}" }.to_string(), } }