Skip to content

Commit

Permalink
Load .env files (#894)
Browse files Browse the repository at this point in the history
  • Loading branch information
mitsuhiko authored Mar 19, 2024
1 parent eb2c60e commit b290cae
Show file tree
Hide file tree
Showing 9 changed files with 142 additions and 13 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ _Unreleased_

- Fix incorrect flag passing of `rye test` `-q` and `-v`. #880

- Rye now loads `.env` files. This applies both for Rye's own
use of environment variables but also to scripts launched via
`run`. #894

- Fix `rye add m --path ./m` causing a panic on windows. #897

<!-- released start -->
Expand Down
7 changes: 7 additions & 0 deletions Cargo.lock

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

10 changes: 9 additions & 1 deletion docs/guide/commands/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,12 @@ This is a list of all the commands that rye provides:
* [tools](tools/index.md): Helper utility to manage global tools.
* [self](self/index.md): Rye self management
* [uninstall](uninstall.md): Uninstalls a global tool (alias)
* [version](version.md): Get or set project version
* [version](version.md): Get or set project version

## Options

The toplevel `rye` command accepts the following options:

* `--env-file` `<FILE>`: This can be supplied multiple times to make rye load
a given `.env` file. Note that this file is not referenced to handle the
`RYE_HOME` variable which must be supplied as environment variable always.
12 changes: 12 additions & 0 deletions docs/guide/pyproject.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,18 @@ This key can be used to provide environment variables with a script:
devserver = { cmd = "flask run --debug", env = { FLASK_APP = "./hello.py" } }
```

### `env-file`

+++ 0.30.0

This is similar to `env` but rather than setting environment variables directly, it instead
points to a file that should be loaded (relative to the `pyproject.toml`):

```toml
[tool.rye.scripts]
devserver = { cmd = "flask run --debug", env-file = ".dev.env" }
```

### `chain`

This is a special key that can be set instead of `cmd` to make a command invoke multiple
Expand Down
1 change: 1 addition & 0 deletions rye/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ python-pkginfo = { version = "0.6.0", features = ["serde"] }
sysinfo = { version = "0.29.4", default-features = false, features = [] }
home = "0.5.9"
ctrlc = "3.4.2"
dotenvy = "0.15.7"

[target."cfg(unix)".dependencies]
xattr = "1.3.1"
Expand Down
12 changes: 12 additions & 0 deletions rye/src/cli/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use std::env;
use std::path::PathBuf;

use anyhow::{bail, Error};
use clap::Parser;
Expand Down Expand Up @@ -34,6 +35,7 @@ use crate::bootstrap::{get_self_venv_status, SELF_PYTHON_TARGET_VERSION};
use crate::config::Config;
use crate::platform::symlinks_supported;
use crate::pyproject::read_venv_marker;
use crate::utils::IoPathContext;

git_testament!(TESTAMENT);

Expand All @@ -43,6 +45,9 @@ git_testament!(TESTAMENT);
struct Args {
#[command(subcommand)]
command: Option<Command>,
/// Load one or more .env files.
#[arg(long)]
env_file: Vec<PathBuf>,
/// Print the version
#[arg(long)]
version: bool,
Expand Down Expand Up @@ -102,6 +107,13 @@ pub fn execute() -> Result<(), Error> {
}

let args = Args::try_parse()?;

// handle --env-file. As this happens here this cannot influence `RYE_HOME` or
// the behavior of the shims.
for env_file in &args.env_file {
dotenvy::from_path(env_file).path_context(env_file, "unable to load env file")?;
}

let cmd = if args.version {
return print_version();
} else if let Some(cmd) = args.command {
Expand Down
28 changes: 23 additions & 5 deletions rye/src/cli/run.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use std::collections::HashMap;
use std::env::{self, join_paths, split_paths};
use std::ffi::OsString;
use std::path::PathBuf;
Expand All @@ -10,7 +11,7 @@ use console::style;
use crate::pyproject::{PyProject, Script};
use crate::sync::{sync, SyncOptions};
use crate::tui::redirect_to_stderr;
use crate::utils::{exec_spawn, get_venv_python_bin, success_status};
use crate::utils::{exec_spawn, get_venv_python_bin, success_status, IoPathContext};

/// Runs a command installed into this package.
#[derive(Parser, Debug)]
Expand Down Expand Up @@ -62,9 +63,9 @@ fn invoke_script(
let mut env_overrides = None;

match pyproject.get_script_cmd(&args[0].to_string_lossy()) {
Some(Script::Call(entry, env_vars)) => {
Some(Script::Call(entry, env_vars, env_file)) => {
let py = OsString::from(get_venv_python_bin(&pyproject.venv_path()));
env_overrides = Some(env_vars);
env_overrides = Some(load_env_vars(pyproject, env_file, env_vars)?);
args = if let Some((module, func)) = entry.split_once(':') {
if module.is_empty() || func.is_empty() {
bail!("Python callable must be in the form <module_name>:<callable_name> or <module_name>")
Expand All @@ -86,11 +87,11 @@ fn invoke_script(
.chain(args.into_iter().skip(1))
.collect();
}
Some(Script::Cmd(script_args, env_vars)) => {
Some(Script::Cmd(script_args, env_vars, env_file)) => {
if script_args.is_empty() {
bail!("script has no arguments");
}
env_overrides = Some(env_vars);
env_overrides = Some(load_env_vars(pyproject, env_file, env_vars)?);
let script_target = venv_bin.join(&script_args[0]);
if script_target.is_file() {
args = Some(script_target.as_os_str().to_owned())
Expand Down Expand Up @@ -157,6 +158,23 @@ fn invoke_script(
}
}

fn load_env_vars(
pyproject: &PyProject,
env_file: Option<PathBuf>,
mut env_vars: HashMap<String, String>,
) -> Result<HashMap<String, String>, Error> {
if let Some(ref env_file) = env_file {
let env_file = pyproject.root_path().join(env_file);
for item in
dotenvy::from_path_iter(&env_file).path_context(&env_file, "could not load env-file")?
{
let (k, v) = item.path_context(&env_file, "invalid value in env-file")?;
env_vars.insert(k, v);
}
}
Ok(env_vars)
}

fn list_scripts(pyproject: &PyProject) -> Result<(), Error> {
let mut scripts: Vec<_> = pyproject
.list_scripts()
Expand Down
30 changes: 23 additions & 7 deletions rye/src/pyproject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -203,14 +203,15 @@ impl SourceRef {
}

type EnvVars = HashMap<String, String>;
type EnvFile = Option<PathBuf>;

/// A reference to a script
#[derive(Clone, Debug)]
pub enum Script {
/// Call python module entry
Call(String, EnvVars),
Call(String, EnvVars, EnvFile),
/// A command alias
Cmd(Vec<String>, EnvVars),
Cmd(Vec<String>, EnvVars, EnvFile),
/// A multi-script execution
Chain(Vec<Vec<String>>),
/// External script reference
Expand Down Expand Up @@ -257,25 +258,34 @@ impl Script {
env_vars
}

fn get_env_file(detailed: &dyn TableLike) -> EnvFile {
detailed
.get("env-file")
.and_then(|x| x.as_str())
.map(PathBuf::from)
}

if let Some(detailed) = item.as_table_like() {
if let Some(call) = detailed.get("call") {
let entry = call.as_str()?.to_string();
let env_vars = get_env_vars(detailed);
Some(Script::Call(entry, env_vars))
let env_file = get_env_file(detailed);
Some(Script::Call(entry, env_vars, env_file))
} else if let Some(cmds) = detailed.get("chain").and_then(|x| x.as_array()) {
Some(Script::Chain(
cmds.iter().flat_map(toml_value_as_command_args).collect(),
))
} else if let Some(cmd) = detailed.get("cmd") {
let cmd = toml_value_as_command_args(cmd.as_value()?)?;
let env_vars = get_env_vars(detailed);
Some(Script::Cmd(cmd, env_vars))
let env_file = get_env_file(detailed);
Some(Script::Cmd(cmd, env_vars, env_file))
} else {
None
}
} else {
toml_value_as_command_args(item.as_value()?)
.map(|cmd| Script::Cmd(cmd, EnvVars::default()))
.map(|cmd| Script::Cmd(cmd, EnvVars::default(), None))
}
}
}
Expand All @@ -288,7 +298,7 @@ fn shlex_quote_unsafe(s: &str) -> Cow<'_, str> {
impl fmt::Display for Script {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Script::Call(entry, env) => {
Script::Call(entry, env, env_file) => {
write!(f, "{}", shlex_quote_unsafe(entry))?;
if !env.is_empty() {
write!(f, " (env: ")?;
Expand All @@ -305,9 +315,12 @@ impl fmt::Display for Script {
}
write!(f, ")")?;
}
if let Some(ref env_file) = env_file {
write!(f, " (env-file: {})", env_file.display())?;
}
Ok(())
}
Script::Cmd(args, env) => {
Script::Cmd(args, env, env_file) => {
let mut need_space = false;
for (key, value) in env.iter() {
if need_space {
Expand All @@ -328,6 +341,9 @@ impl fmt::Display for Script {
write!(f, "{}", shlex_quote_unsafe(arg))?;
need_space = true;
}
if let Some(ref env_file) = env_file {
write!(f, " (env-file: {})", env_file.display())?;
}
Ok(())
}
Script::Chain(cmds) => {
Expand Down
51 changes: 51 additions & 0 deletions rye/tests/test_cli.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
use std::fs;

use toml_edit::value;

use crate::common::{rye_cmd_snapshot, Space};

mod common;

#[test]
fn test_dotenv() {
let space = Space::new();
space.init("my-project");
space.edit_toml("pyproject.toml", |doc| {
doc["tool"]["rye"]["scripts"]["hello"]["cmd"] =
value("python -c \"import os; print(os.environ['MY_COOL_VAR'], os.environ['MY_COOL_OTHER_VAR'])\"");
doc["tool"]["rye"]["scripts"]["hello"]["env-file"] = value(".other.env");
});
fs::write(space.project_path().join(".env"), "MY_COOL_VAR=42").unwrap();
fs::write(
space.project_path().join(".other.env"),
"MY_COOL_OTHER_VAR=23",
)
.unwrap();
rye_cmd_snapshot!(space.rye_cmd().arg("sync"), @r###"
success: true
exit_code: 0
----- stdout -----
Initializing new virtualenv in [TEMP_PATH]/project/.venv
Python version: [email protected]
Generating production lockfile: [TEMP_PATH]/project/requirements.lock
Generating dev lockfile: [TEMP_PATH]/project/requirements-dev.lock
Installing dependencies
Done!
----- stderr -----
Built 1 editable in [EXECUTION_TIME]
Installed 1 package in [EXECUTION_TIME]
+ my-project==0.1.0 (from file:[TEMP_PATH]/project)
"###);
rye_cmd_snapshot!(space.rye_cmd()
.arg("--env-file=.env")
.arg("run")
.arg("hello"), @r###"
success: true
exit_code: 0
----- stdout -----
42 23
----- stderr -----
"###);
}

0 comments on commit b290cae

Please sign in to comment.