Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 3 commits into from
Nov 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -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" }
Expand Down
11 changes: 11 additions & 0 deletions crates/uv-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
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 @@ -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 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
40 changes: 40 additions & 0 deletions crates/uv/src/commands/project/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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);

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 @@ -1309,6 +1310,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 @@ -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 {
Expand Down Expand Up @@ -277,6 +279,8 @@ impl RunSettings {
no_project,
python,
show_resolution,
env_file,
no_env_file,
} = args;

Self {
Expand Down Expand Up @@ -308,6 +312,8 @@ impl RunSettings {
resolver_installer_options(installer, build),
filesystem,
),
env_file,
no_env_file,
}
}
}
Expand Down
219 changes: 216 additions & 3 deletions crates/uv/tests/it/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 -----
Expand All @@ -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 changes: 2 additions & 0 deletions docs/configuration/environment.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Loading
Loading