Add support for .env and custom env files in uv run (#8811)

## Summary

This PR pulls in https://github.com/astral-sh/uv/pull/8263 and
https://github.com/astral-sh/uv/pull/8463, which were originally merged
into the v0.5 tracking branch but can now be committed separately, as
we've made `.env` loading opt-in.

In summary:

- `.env` loading is now opt-in (`--env-file .env`).
- `.env` remains supported on `uv run`, so it's meant for providing
environment variables to the run command, rather than to uv itself.

---------

Co-authored-by: Eduardo González Vaquero <47718648+edugzlez@users.noreply.github.com>
This commit is contained in:
Charlie Marsh
2024-11-04 14:26:05 -05:00
committed by GitHub
parent 224622b089
commit a42c09fdfb
12 changed files with 326 additions and 3 deletions
Generated
+7
View File
@@ -950,6 +950,12 @@ version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10"
[[package]]
name = "dotenvy"
version = "0.15.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
[[package]]
name = "dunce"
version = "1.0.5"
@@ -4178,6 +4184,7 @@ dependencies = [
"clap",
"console",
"ctrlc",
"dotenvy",
"etcetera",
"filetime",
"flate2",
+1
View File
@@ -93,6 +93,7 @@ dashmap = { version = "6.1.0" }
data-encoding = { version = "2.6.0" }
directories = { version = "5.0.1" }
dirs-sys = { version = "0.4.1" }
dotenvy = { version = "0.15.7" }
dunce = { version = "1.0.5" }
either = { version = "1.13.0" }
encoding_rs_io = { version = "0.1.7" }
+11
View File
@@ -2656,6 +2656,17 @@ pub struct RunArgs {
#[arg(long)]
pub no_editable: bool,
/// Load environment variables from a `.env` file.
///
/// Can be provided multiple times, with subsequent files overriding values defined in
/// previous files.
#[arg(long, env = EnvVars::UV_ENV_FILE)]
pub env_file: Vec<PathBuf>,
/// Avoid reading environment variables from a `.env` file.
#[arg(long, 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
+6
View File
@@ -519,4 +519,10 @@ impl EnvVars {
/// Used to set test credentials for keyring tests.
#[attr_hidden]
pub const KEYRING_TEST_CREDENTIALS: &'static str = "KEYRING_TEST_CREDENTIALS";
/// `.env` files from which to load environment variables when executing `uv run` commands.
pub const UV_ENV_FILE: &'static str = "UV_ENV_FILE";
/// Ignore `.env` files when executing `uv run` commands.
pub const UV_NO_ENV_FILE: &'static str = "UV_NO_ENV_FILE";
}
+1
View File
@@ -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 }
+40
View File
@@ -81,6 +81,8 @@ pub(crate) async fn run(
native_tls: bool,
cache: &Cache,
printer: Printer,
env_file: Vec<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.
@@ -107,6 +109,44 @@ pub(crate) async fn run(
// Initialize any shared state.
let state = SharedState::default();
// Read from the `.env` file, if necessary.
if !no_env_file {
for env_file_path in env_file.iter().rev().map(PathBuf::as_path) {
match dotenvy::from_path(env_file_path) {
Err(dotenvy::Error::Io(err)) if err.kind() == std::io::ErrorKind::NotFound => {
bail!(
"No environment file found at: `{}`",
env_file_path.simplified_display()
);
}
Err(dotenvy::Error::Io(err)) => {
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);
+3
View File
@@ -1,4 +1,5 @@
use std::borrow::Cow;
use std::env;
use std::ffi::OsString;
use std::fmt::Write;
use std::io::stdout;
@@ -1309,6 +1310,8 @@ async fn run_project(
globals.native_tls,
&cache,
printer,
args.env_file,
args.no_env_file,
))
.await
}
+6
View File
@@ -242,6 +242,8 @@ pub(crate) struct RunSettings {
pub(crate) python: Option<String>,
pub(crate) refresh: Refresh,
pub(crate) settings: ResolverInstallerSettings,
pub(crate) env_file: Vec<PathBuf>,
pub(crate) no_env_file: bool,
}
impl RunSettings {
@@ -277,6 +279,8 @@ impl RunSettings {
no_project,
python,
show_resolution,
env_file,
no_env_file,
} = args;
Self {
@@ -308,6 +312,8 @@ impl RunSettings {
resolver_installer_options(installer, build),
filesystem,
),
env_file,
no_env_file,
}
}
}
+216 -3
View File
@@ -2813,9 +2813,7 @@ fn run_stdin_with_pep723() -> Result<()> {
"#
})?;
let mut command = context.run();
let command_with_args = command.stdin(std::fs::File::open(test_script)?).arg("-");
uv_snapshot!(context.filters(), command_with_args, @r###"
uv_snapshot!(context.filters(), context.run().stdin(std::fs::File::open(test_script)?).arg("-"), @r###"
success: true
exit_code: 0
----- stdout -----
@@ -2831,3 +2829,218 @@ fn run_stdin_with_pep723() -> Result<()> {
Ok(())
}
#[test]
fn run_with_env() -> Result<()> {
let context = TestContext::new("3.12");
context.temp_dir.child("test.py").write_str(indoc! { "
import os
print(os.environ.get('THE_EMPIRE_VARIABLE'))
print(os.environ.get('REBEL_1'))
print(os.environ.get('REBEL_2'))
print(os.environ.get('REBEL_3'))
"
})?;
context.temp_dir.child(".env").write_str(indoc! { "
THE_EMPIRE_VARIABLE=palpatine
REBEL_1=leia_organa
REBEL_2=obi_wan_kenobi
REBEL_3=C3PO
"
})?;
uv_snapshot!(context.filters(), context.run().arg("test.py"), @r###"
success: true
exit_code: 0
----- stdout -----
None
None
None
None
----- stderr -----
"###);
uv_snapshot!(context.filters(), context.run().arg("--env-file").arg(".env").arg("test.py"), @r###"
success: true
exit_code: 0
----- stdout -----
palpatine
leia_organa
obi_wan_kenobi
C3PO
----- stderr -----
"###);
Ok(())
}
#[test]
fn run_with_env_file() -> Result<()> {
let context = TestContext::new("3.12");
context.temp_dir.child("test.py").write_str(indoc! { "
import os
print(os.environ.get('THE_EMPIRE_VARIABLE'))
print(os.environ.get('REBEL_1'))
print(os.environ.get('REBEL_2'))
print(os.environ.get('REBEL_3'))
"
})?;
context.temp_dir.child(".file").write_str(indoc! { "
THE_EMPIRE_VARIABLE=palpatine
REBEL_1=leia_organa
REBEL_2=obi_wan_kenobi
REBEL_3=C3PO
"
})?;
uv_snapshot!(context.filters(), context.run().arg("--env-file").arg(".file").arg("test.py"), @r###"
success: true
exit_code: 0
----- stdout -----
palpatine
leia_organa
obi_wan_kenobi
C3PO
----- stderr -----
"###);
Ok(())
}
#[test]
fn run_with_multiple_env_files() -> Result<()> {
let context = TestContext::new("3.12");
context.temp_dir.child("test.py").write_str(indoc! { "
import os
print(os.environ.get('THE_EMPIRE_VARIABLE'))
print(os.environ.get('REBEL_1'))
print(os.environ.get('REBEL_2'))
"
})?;
context.temp_dir.child(".env1").write_str(indoc! { "
THE_EMPIRE_VARIABLE=palpatine
REBEL_1=leia_organa
"
})?;
context.temp_dir.child(".env2").write_str(indoc! { "
THE_EMPIRE_VARIABLE=palpatine
REBEL_1=obi_wan_kenobi
REBEL_2=C3PO
"
})?;
uv_snapshot!(context.filters(), context.run().arg("--env-file").arg(".env1").arg("--env-file").arg(".env2").arg("test.py"), @r###"
success: true
exit_code: 0
----- stdout -----
palpatine
obi_wan_kenobi
C3PO
----- stderr -----
"###);
uv_snapshot!(context.filters(), context.run().arg("test.py").env("UV_ENV_FILE", ".env1 .env2"), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: No environment file found at: `.env1 .env2`
"###);
Ok(())
}
#[test]
fn run_with_env_omitted() -> Result<()> {
let context = TestContext::new("3.12");
context.temp_dir.child("test.py").write_str(indoc! { "
import os
print(os.environ.get('THE_EMPIRE_VARIABLE'))
"
})?;
context.temp_dir.child(".env").write_str(indoc! { "
THE_EMPIRE_VARIABLE=palpatine
"
})?;
uv_snapshot!(context.filters(), context.run().arg("--env-file").arg(".env").arg("--no-env-file").arg("test.py"), @r###"
success: true
exit_code: 0
----- stdout -----
None
----- stderr -----
"###);
Ok(())
}
#[test]
fn run_with_malformed_env() -> Result<()> {
let context = TestContext::new("3.12");
context.temp_dir.child("test.py").write_str(indoc! { "
import os
print(os.environ.get('THE_EMPIRE_VARIABLE'))
"
})?;
context.temp_dir.child(".env").write_str(indoc! { "
THE_^EMPIRE_VARIABLE=darth_vader
"
})?;
uv_snapshot!(context.filters(), context.run().arg("--env-file").arg(".env").arg("test.py"), @r###"
success: true
exit_code: 0
----- stdout -----
None
----- stderr -----
warning: Failed to parse environment file `.env` at position 4: THE_^EMPIRE_VARIABLE=darth_vader
"###);
Ok(())
}
#[test]
fn run_with_not_existing_env_file() -> Result<()> {
let context = TestContext::new("3.12");
context.temp_dir.child("test.py").write_str(indoc! { "
import os
print(os.environ.get('THE_EMPIRE_VARIABLE'))
"
})?;
let mut filters = context.filters();
filters.push((
r"(?m)^error: Failed to read environment file `.env.development`: .*$",
"error: Failed to read environment file `.env.development`: [ERR]",
));
uv_snapshot!(filters, context.run().arg("--env-file").arg(".env.development").arg("test.py"), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: No environment file found at: `.env.development`
"###);
Ok(())
}
+2
View File
@@ -174,3 +174,5 @@ uv respects the following environment variables:
For example, `RUST_LOG=trace` will enable trace-level logging.
See the [tracing documentation](https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#example-syntax)
for more.
- <a id="UV_ENV_FILE"></a> [`UV_ENV_FILE`](#UV_ENV_FILE): `.env` files from which to load environment variables when executing `uv run` commands.
- <a id="UV_NO_ENV_FILE"></a> [`UV_NO_ENV_FILE`](#UV_NO_ENV_FILE): Ignore `.env` files when executing `uv run` commands.
+25
View File
@@ -72,6 +72,31 @@ configuration files (e.g., user-level configuration will be ignored).
See the [settings reference](../reference/settings.md) for an enumeration of the available settings.
## `.env`
`uv run` can load environment variables from dotenv files (e.g., `.env`, `.env.local`,
`.env.development`), powered by the [`dotenvy`](https://github.com/allan2/dotenvy) crate.
To load a `.env` file from a dedicated location, set the `UV_ENV_FILE` environment variable, or pass
the `--env-file` flag to `uv run`.
For example, to load environment variables from a `.env` file in the current working directory:
```console
$ uv run --env-file .env -- echo $MY_ENV_VAR
```
The `--env-file` flag can be provided multiple times, with subsequent files overriding values
defined in previous files. To provide multiple files via the `UV_ENV_FILE` environment variable,
separate the paths with a space (e.g., `UV_ENV_FILE="/path/to/file1 /path/to/file2"`).
To disable dotenv loading (e.g., to override `UV_ENV_FILE` or the `--env-file` command-line
argument), set the `UV_NO_ENV_FILE` environment variable to `1`, or pass the`--no-env-file` flag to
`uv run`.
If the same variable is defined in the environment and in a `.env` file, the value from the
environment will take precedence.
## Configuring the pip interface
A dedicated [`[tool.uv.pip]`](../reference/settings.md#pip) section is provided for configuring
+8
View File
@@ -139,6 +139,11 @@ uv run [OPTIONS] [COMMAND]
<p>See <code>--project</code> to only change the project root directory.</p>
</dd><dt><code>--env-file</code> <i>env-file</i></dt><dd><p>Load environment variables from a <code>.env</code> file.</p>
<p>Can be provided multiple times, with subsequent files overriding values defined in previous files.</p>
<p>May also be set with the <code>UV_ENV_FILE</code> environment variable.</p>
</dd><dt><code>--exclude-newer</code> <i>exclude-newer</i></dt><dd><p>Limit candidate packages to those that were uploaded prior to the given date.</p>
<p>Accepts both RFC 3339 timestamps (e.g., <code>2006-12-02T02:07:43Z</code>) and local dates in the same format (e.g., <code>2006-12-02</code>) in your system&#8217;s configured time zone.</p>
@@ -296,6 +301,9 @@ uv run [OPTIONS] [COMMAND]
</dd><dt><code>--no-editable</code></dt><dd><p>Install any editable dependencies, including the project and any workspace members, as non-editable</p>
</dd><dt><code>--no-env-file</code></dt><dd><p>Avoid reading environment variables from a <code>.env</code> file</p>
<p>May also be set with the <code>UV_NO_ENV_FILE</code> environment variable.</p>
</dd><dt><code>--no-group</code> <i>no-group</i></dt><dd><p>Exclude dependencies from the specified dependency group.</p>
<p>May be provided multiple times.</p>