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:
Generated
+7
@@ -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",
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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’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>
|
||||
|
||||
Reference in New Issue
Block a user