Skip to content

Commit

Permalink
added env._.python.venv directive (#1706)
Browse files Browse the repository at this point in the history
  • Loading branch information
jdx authored Feb 24, 2024
1 parent 1f359a6 commit 055dd80
Show file tree
Hide file tree
Showing 10 changed files with 207 additions and 23 deletions.
6 changes: 4 additions & 2 deletions e2e/test_python
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@ export MISE_EXPERIMENTAL=1
export MISE_PYTHON_DEFAULT_PACKAGES_FILE="$ROOT/e2e/.default-python-packages"

cat >.e2e.mise.toml <<EOF
[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')}}"
EOF

mise i
mise x -- python -m venv "$MISE_DATA_DIR/venv"
#mise x -- python -m venv "$MISE_DATA_DIR/venv"
assert_contains "mise x [email protected] -- python --version" "Python 3.12.0"
assert_contains "mise env -s bash | grep VIRTUAL_ENV" "$MISE_DATA_DIR/venv"
assert "mise x -- which python" "$MISE_DATA_DIR/venv/bin/python"
Expand Down
27 changes: 27 additions & 0 deletions schema/mise.json
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,33 @@
}
}
]
},
"python": {
"type": "object",
"description": "python environment",
"properties": {
"venv": {
"oneOf": [
{"description": "path to python virtual environment to use", "type": "string"},
{
"description": "virtualenv options",
"type": "object",
"required": ["path"],
"properties": {
"path": {
"description": "path to python virtual environment to use",
"type": "string"
},
"create": {
"description": "create a new virtual environment if one does not exist",
"type": "boolean",
"default": false
}
}
}
]
}
}
}
}
}
Expand Down
2 changes: 0 additions & 2 deletions src/cli/settings/ls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,6 @@ mod tests {
python_compile = false
python_default_packages_file = "~/.default-python-packages"
python_pyenv_repo = "https://github.com/pyenv/pyenv.git"
python_venv_auto_create = false
quiet = false
raw = false
trusted_config_paths = []
Expand Down Expand Up @@ -129,7 +128,6 @@ mod tests {
python_compile
python_default_packages_file
python_pyenv_repo
python_venv_auto_create
quiet
raw
status
Expand Down
1 change: 0 additions & 1 deletion src/cli/settings/set.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,6 @@ pub mod tests {
python_compile = false
python_default_packages_file = "~/.default-python-packages"
python_pyenv_repo = "https://github.com/pyenv/pyenv.git"
python_venv_auto_create = false
quiet = false
raw = false
trusted_config_paths = []
Expand Down
1 change: 0 additions & 1 deletion src/cli/settings/unset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,6 @@ mod tests {
python_compile = false
python_default_packages_file = "~/.default-python-packages"
python_pyenv_repo = "https://github.com/pyenv/pyenv.git"
python_venv_auto_create = false
quiet = false
raw = false
trusted_config_paths = []
Expand Down
88 changes: 88 additions & 0 deletions src/config/config_file/mise_toml.rs
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,18 @@ impl<'de> de::Deserialize<'de> for EnvList {
while let Some(key) = map.next_key::<String>()? {
match key.as_str() {
"_" | "mise" => {
struct EnvDirectivePythonVenv {
path: PathBuf,
create: bool,
}

#[derive(Deserialize, Default)]
#[serde(deny_unknown_fields)]
struct EnvDirectivePython {
#[serde(default)]
venv: Option<EnvDirectivePythonVenv>,
}

#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct EnvDirectives {
Expand All @@ -435,7 +447,77 @@ impl<'de> de::Deserialize<'de> for EnvList {
file: Vec<PathBuf>,
#[serde(default, deserialize_with = "deserialize_arr")]
source: Vec<PathBuf>,
#[serde(default)]
python: EnvDirectivePython,
}

impl<'de> de::Deserialize<'de> for EnvDirectivePythonVenv {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
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<E>(self, v: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(EnvDirectivePythonVenv {
path: v.into(),
create: false,
})
}

fn visit_map<M>(
self,
mut map: M,
) -> Result<Self::Value, M::Error>
where
M: de::MapAccess<'de>,
{
let mut path = None;
let mut create = false;
while let Some(key) = map.next_key::<String>()? {
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::<EnvDirectives>()?;
// TODO: parse these in the order they're defined somehow
for path in directives.path {
Expand All @@ -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 {
Expand Down
77 changes: 70 additions & 7 deletions src/config/env_directive.rs
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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 {
Expand All @@ -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(())
}
}
}
}
Expand Down Expand Up @@ -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::<Vec<_>>();
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 {
Expand Down Expand Up @@ -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();
Expand Down
18 changes: 14 additions & 4 deletions src/config/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,6 @@ pub struct Settings {
pub python_precompiled_os: Option<String>,
#[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")]
Expand Down Expand Up @@ -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)]
Expand Down Expand Up @@ -349,8 +349,18 @@ impl Settings {
}

pub fn hidden_configs() -> &'static HashSet<&'static str> {
static HIDDEN_CONFIGS: Lazy<HashSet<&'static str>> =
Lazy::new(|| ["ci", "cd", "debug", "env_file", "trace", "log_level"].into());
static HIDDEN_CONFIGS: Lazy<HashSet<&'static str>> = Lazy::new(|| {
[
"ci",
"cd",
"debug",
"env_file",
"trace",
"log_level",
"python_venv_auto_create",
]
.into()
});
&HIDDEN_CONFIGS
}

Expand Down
2 changes: 0 additions & 2 deletions src/plugins/core/python.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
8 changes: 4 additions & 4 deletions tests/cli/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
}
}
Expand Down

0 comments on commit 055dd80

Please sign in to comment.