Skip to content

Commit 7bbffd7

Browse files
zaniebcharliermarsh
andcommitted
Add support for .env and custom env files in uv run (#8263)
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. 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 <[email protected]>
1 parent 9042a80 commit 7bbffd7

File tree

12 files changed

+334
-0
lines changed

12 files changed

+334
-0
lines changed

Cargo.lock

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ csv = { version = "1.3.0" }
9191
ctrlc = { version = "3.4.5" }
9292
dashmap = { version = "6.1.0" }
9393
data-encoding = { version = "2.6.0" }
94+
dotenvy = { version = "0.15.7" }
9495
dunce = { version = "1.0.5" }
9596
either = { version = "1.13.0" }
9697
encoding_rs_io = { version = "0.1.7" }

crates/uv-cli/src/lib.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -618,6 +618,9 @@ pub enum ProjectCommand {
618618
/// arguments to uv. All options to uv must be provided before the command,
619619
/// e.g., `uv run --verbose foo`. A `--` can be used to separate the command
620620
/// from uv options for clarity, e.g., `uv run --python 3.12 -- python`.
621+
///
622+
/// Respects `.env` files in the current directory unless `--no-env-file` is
623+
/// provided.
621624
#[command(
622625
after_help = "Use `uv help run` for more details.",
623626
after_long_help = ""
@@ -2656,6 +2659,16 @@ pub struct RunArgs {
26562659
#[arg(long)]
26572660
pub no_editable: bool,
26582661

2662+
/// Load environment variables from a `.env` file.
2663+
///
2664+
/// Defaults to reading `.env` in the current working directory.
2665+
#[arg(long, value_parser = parse_file_path, env = EnvVars::UV_ENV_FILE)]
2666+
pub env_file: Option<PathBuf>,
2667+
2668+
/// Avoid reading environment variables from a `.env` file.
2669+
#[arg(long, conflicts_with = "env_file", value_parser = clap::builder::BoolishValueParser::new(), env = EnvVars::UV_NO_ENV_FILE)]
2670+
pub no_env_file: bool,
2671+
26592672
/// The command to run.
26602673
///
26612674
/// If the path to a Python script (i.e., ending in `.py`), it will be

crates/uv-static/src/env_vars.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -522,4 +522,10 @@ impl EnvVars {
522522
/// Used to set test credentials for keyring tests.
523523
#[attr_hidden]
524524
pub const KEYRING_TEST_CREDENTIALS: &'static str = "KEYRING_TEST_CREDENTIALS";
525+
526+
/// Used to overwrite path for loading `.env` files when executing `uv run` commands.
527+
pub const UV_ENV_FILE: &'static str = "UV_ENV_FILE";
528+
529+
/// Used to ignore `.env` files when executing `uv run` commands.
530+
pub const UV_NO_ENV_FILE: &'static str = "UV_NO_ENV_FILE";
525531
}

crates/uv/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ axoupdater = { workspace = true, features = [
6363
clap = { workspace = true, features = ["derive", "string", "wrap_help"] }
6464
console = { workspace = true }
6565
ctrlc = { workspace = true }
66+
dotenvy = { workspace = true }
6667
flate2 = { workspace = true, default-features = false }
6768
fs-err = { workspace = true, features = ["tokio"] }
6869
futures = { workspace = true }

crates/uv/src/commands/project/run.rs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ pub(crate) async fn run(
8181
native_tls: bool,
8282
cache: &Cache,
8383
printer: Printer,
84+
env_file: Option<PathBuf>,
85+
no_env_file: bool,
8486
) -> anyhow::Result<ExitStatus> {
8587
// These cases seem quite complex because (in theory) they should change the "current package".
8688
// Let's ban them entirely for now.
@@ -107,6 +109,57 @@ pub(crate) async fn run(
107109
// Initialize any shared state.
108110
let state = SharedState::default();
109111

112+
// Read from the `.env` file, if necessary.
113+
if !no_env_file {
114+
let env_file_path = env_file.as_deref().unwrap_or_else(|| Path::new(".env"));
115+
match dotenvy::from_path(env_file_path) {
116+
Err(dotenvy::Error::Io(err)) if err.kind() == std::io::ErrorKind::NotFound => {
117+
if env_file.is_none() {
118+
debug!(
119+
"No environment file found at: `{}`",
120+
env_file_path.simplified_display()
121+
);
122+
} else {
123+
bail!(
124+
"No environment file found at: `{}`",
125+
env_file_path.simplified_display()
126+
);
127+
}
128+
}
129+
Err(dotenvy::Error::Io(err)) => {
130+
if env_file.is_none() {
131+
debug!(
132+
"Failed to read environment file `{}`: {err}",
133+
env_file_path.simplified_display()
134+
);
135+
} else {
136+
bail!(
137+
"Failed to read environment file `{}`: {err}",
138+
env_file_path.simplified_display()
139+
);
140+
}
141+
}
142+
Err(dotenvy::Error::LineParse(content, position)) => {
143+
warn_user!(
144+
"Failed to parse environment file `{}` at position {position}: {content}",
145+
env_file_path.simplified_display(),
146+
);
147+
}
148+
Err(err) => {
149+
warn_user!(
150+
"Failed to parse environment file `{}`: {err}",
151+
env_file_path.simplified_display(),
152+
);
153+
}
154+
Ok(()) => {
155+
debug!(
156+
"Read environment file at: `{}`",
157+
env_file_path.simplified_display()
158+
);
159+
}
160+
}
161+
}
162+
110163
// Initialize any output reporters.
111164
let download_reporter = PythonDownloadReporter::single(printer);
112165

crates/uv/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use std::borrow::Cow;
2+
use std::env;
23
use std::ffi::OsString;
34
use std::fmt::Write;
45
use std::io::stdout;
@@ -1309,6 +1310,8 @@ async fn run_project(
13091310
globals.native_tls,
13101311
&cache,
13111312
printer,
1313+
args.env_file,
1314+
args.no_env_file,
13121315
))
13131316
.await
13141317
}

crates/uv/src/settings.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,8 @@ pub(crate) struct RunSettings {
246246
pub(crate) python: Option<String>,
247247
pub(crate) refresh: Refresh,
248248
pub(crate) settings: ResolverInstallerSettings,
249+
pub(crate) env_file: Option<PathBuf>,
250+
pub(crate) no_env_file: bool,
249251
}
250252

251253
impl RunSettings {
@@ -281,6 +283,8 @@ impl RunSettings {
281283
no_project,
282284
python,
283285
show_resolution,
286+
env_file,
287+
no_env_file,
284288
} = args;
285289

286290
Self {
@@ -312,6 +316,8 @@ impl RunSettings {
312316
resolver_installer_options(installer, build),
313317
filesystem,
314318
),
319+
env_file,
320+
no_env_file,
315321
}
316322
}
317323
}

0 commit comments

Comments
 (0)