Skip to content

Commit

Permalink
Add support for .env and custom env files in uv run (#8263)
Browse files Browse the repository at this point in the history
## Summary

I have been reading discussion #1384 about .env and how to include it in
the `uv run` command.

I have always wanted to include this possibility in `uv`, so I was
interested in the latest changes. Following @charliermarsh's [advice
](#1384 (comment)) I
have tried to respect the philosophy that `uv run` uses the default
`.env` and this can be discarded or changed via terminal or environment
variables.

The behaviour is as follows:
- `uv run file.py` executes file.py using the `.env` (if it exists).
- `uv run --env-file .env.development file.py` uses the
`.env.development` file to load the variables and then runs file.py. In
this case the program fails if the file does not exist.
- `uv run --no-env-file file.py` skips reading the `.env` file.

Equivalently, I have included the `UV_ENV_FILE` and `UV_NO_ENV_FILE`
environment variables.

## Test Plan

I haven't got into including automated tests, I would need help with
this.

I have tried the above commands, with a python script that prints
environment variables.

---------

Co-authored-by: Charlie Marsh <charlie.r.marsh@gmail.com>
  • Loading branch information
edugzlez and charliermarsh authored Oct 22, 2024
1 parent 2777324 commit f6d9804
Show file tree
Hide file tree
Showing 12 changed files with 335 additions and 0 deletions.
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.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ csv = { version = "1.3.0" }
ctrlc = { version = "3.4.5" }
dashmap = { version = "6.1.0" }
data-encoding = { version = "2.6.0" }
dotenvy = { version = "0.15.7" }
dunce = { version = "1.0.5" }
either = { version = "1.13.0" }
encoding_rs_io = { version = "0.1.7" }
Expand Down
13 changes: 13 additions & 0 deletions crates/uv-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -618,6 +618,9 @@ pub enum ProjectCommand {
/// arguments to uv. All options to uv must be provided before the command,
/// e.g., `uv run --verbose foo`. A `--` can be used to separate the command
/// from uv options for clarity, e.g., `uv run --python 3.12 -- python`.
///
/// Respects `.env` files in the current directory unless `--no-env-file` is
/// provided.
#[command(
after_help = "Use `uv help run` for more details.",
after_long_help = ""
Expand Down Expand Up @@ -2628,6 +2631,16 @@ pub struct RunArgs {
#[arg(long)]
pub no_editable: bool,

/// Load environment variables from a `.env` file.
///
/// Defaults to reading `.env` in the current working directory.
#[arg(long, value_parser = parse_file_path, env = EnvVars::UV_ENV_FILE)]
pub env_file: Option<PathBuf>,

/// Avoid reading environment variables from a `.env` file.
#[arg(long, conflicts_with = "env_file", value_parser = clap::builder::BoolishValueParser::new(), env = EnvVars::UV_NO_ENV_FILE)]
pub no_env_file: bool,

/// The command to run.
///
/// If the path to a Python script (i.e., ending in `.py`), it will be
Expand Down
6 changes: 6 additions & 0 deletions crates/uv-static/src/env_vars.rs
Original file line number Diff line number Diff line change
Expand Up @@ -386,4 +386,10 @@ impl EnvVars {

/// Used to set test credentials for keyring tests.
pub const KEYRING_TEST_CREDENTIALS: &'static str = "KEYRING_TEST_CREDENTIALS";

/// Used to overwrite path for loading `.env` files when executing `uv run` commands.
pub const UV_ENV_FILE: &'static str = "UV_ENV_FILE";

/// Used to ignore `.env` files when executing `uv run` commands.
pub const UV_NO_ENV_FILE: &'static str = "UV_NO_ENV_FILE";
}
1 change: 1 addition & 0 deletions crates/uv/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ axoupdater = { workspace = true, features = [
clap = { workspace = true, features = ["derive", "string", "wrap_help"] }
console = { workspace = true }
ctrlc = { workspace = true }
dotenvy = { workspace = true }
flate2 = { workspace = true, default-features = false }
fs-err = { workspace = true, features = ["tokio"] }
futures = { workspace = true }
Expand Down
53 changes: 53 additions & 0 deletions crates/uv/src/commands/project/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ pub(crate) async fn run(
native_tls: bool,
cache: &Cache,
printer: Printer,
env_file: Option<PathBuf>,
no_env_file: bool,
) -> anyhow::Result<ExitStatus> {
// These cases seem quite complex because (in theory) they should change the "current package".
// Let's ban them entirely for now.
Expand All @@ -105,6 +107,57 @@ pub(crate) async fn run(
// Initialize any shared state.
let state = SharedState::default();

// Read from the `.env` file, if necessary.
if !no_env_file {
let env_file_path = env_file.as_deref().unwrap_or_else(|| Path::new(".env"));
match dotenvy::from_path(env_file_path) {
Err(dotenvy::Error::Io(err)) if err.kind() == std::io::ErrorKind::NotFound => {
if env_file.is_none() {
debug!(
"No environment file found at: `{}`",
env_file_path.simplified_display()
);
} else {
bail!(
"No environment file found at: `{}`",
env_file_path.simplified_display()
);
}
}
Err(dotenvy::Error::Io(err)) => {
if env_file.is_none() {
debug!(
"Failed to read environment file `{}`: {err}",
env_file_path.simplified_display()
);
} else {
bail!(
"Failed to read environment file `{}`: {err}",
env_file_path.simplified_display()
);
}
}
Err(dotenvy::Error::LineParse(content, position)) => {
warn_user!(
"Failed to parse environment file `{}` at position {position}: {content}",
env_file_path.simplified_display(),
);
}
Err(err) => {
warn_user!(
"Failed to parse environment file `{}`: {err}",
env_file_path.simplified_display(),
);
}
Ok(()) => {
debug!(
"Read environment file at: `{}`",
env_file_path.simplified_display()
);
}
}
}

// Initialize any output reporters.
let download_reporter = PythonDownloadReporter::single(printer);

Expand Down
3 changes: 3 additions & 0 deletions crates/uv/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use std::borrow::Cow;
use std::env;
use std::ffi::OsString;
use std::fmt::Write;
use std::io::stdout;
Expand Down Expand Up @@ -1295,6 +1296,8 @@ async fn run_project(
globals.native_tls,
&cache,
printer,
args.env_file,
args.no_env_file,
))
.await
}
Expand Down
6 changes: 6 additions & 0 deletions crates/uv/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,8 @@ pub(crate) struct RunSettings {
pub(crate) python: Option<String>,
pub(crate) refresh: Refresh,
pub(crate) settings: ResolverInstallerSettings,
pub(crate) env_file: Option<PathBuf>,
pub(crate) no_env_file: bool,
}

impl RunSettings {
Expand Down Expand Up @@ -275,6 +277,8 @@ impl RunSettings {
no_project,
python,
show_resolution,
env_file,
no_env_file,
} = args;

Self {
Expand Down Expand Up @@ -303,6 +307,8 @@ impl RunSettings {
resolver_installer_options(installer, build),
filesystem,
),
env_file,
no_env_file,
}
}
}
Expand Down
Loading

0 comments on commit f6d9804

Please sign in to comment.