Skip to content

Commit 2b3d6fd

Browse files
Support .env files in uv tool run (#12386)
## Summary Closes #12371.
1 parent 42a87da commit 2b3d6fd

File tree

6 files changed

+161
-0
lines changed

6 files changed

+161
-0
lines changed

crates/uv-cli/src/lib.rs

+11
Original file line numberDiff line numberDiff line change
@@ -4093,6 +4093,17 @@ pub struct ToolRunArgs {
40934093
#[arg(long)]
40944094
pub isolated: bool,
40954095

4096+
/// Load environment variables from a `.env` file.
4097+
///
4098+
/// Can be provided multiple times, with subsequent files overriding values defined in previous
4099+
/// files.
4100+
#[arg(long, value_delimiter = ' ', env = EnvVars::UV_ENV_FILE)]
4101+
pub env_file: Vec<PathBuf>,
4102+
4103+
/// Avoid reading environment variables from a `.env` file.
4104+
#[arg(long, value_parser = clap::builder::BoolishValueParser::new(), env = EnvVars::UV_NO_ENV_FILE)]
4105+
pub no_env_file: bool,
4106+
40964107
#[command(flatten)]
40974108
pub installer: ResolverInstallerArgs,
40984109

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

+40
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,8 @@ pub(crate) async fn run(
9696
concurrency: Concurrency,
9797
cache: Cache,
9898
printer: Printer,
99+
env_file: Vec<PathBuf>,
100+
no_env_file: bool,
99101
preview: PreviewMode,
100102
) -> anyhow::Result<ExitStatus> {
101103
/// Whether or not a path looks like a Python script based on the file extension.
@@ -104,6 +106,44 @@ pub(crate) async fn run(
104106
.is_some_and(|ext| ext.eq_ignore_ascii_case("py") || ext.eq_ignore_ascii_case("pyw"))
105107
}
106108

109+
// Read from the `.env` file, if necessary.
110+
if !no_env_file {
111+
for env_file_path in env_file.iter().rev().map(PathBuf::as_path) {
112+
match dotenvy::from_path(env_file_path) {
113+
Err(dotenvy::Error::Io(err)) if err.kind() == std::io::ErrorKind::NotFound => {
114+
bail!(
115+
"No environment file found at: `{}`",
116+
env_file_path.simplified_display()
117+
);
118+
}
119+
Err(dotenvy::Error::Io(err)) => {
120+
bail!(
121+
"Failed to read environment file `{}`: {err}",
122+
env_file_path.simplified_display()
123+
);
124+
}
125+
Err(dotenvy::Error::LineParse(content, position)) => {
126+
warn_user!(
127+
"Failed to parse environment file `{}` at position {position}: {content}",
128+
env_file_path.simplified_display(),
129+
);
130+
}
131+
Err(err) => {
132+
warn_user!(
133+
"Failed to parse environment file `{}`: {err}",
134+
env_file_path.simplified_display(),
135+
);
136+
}
137+
Ok(()) => {
138+
debug!(
139+
"Read environment file at: `{}`",
140+
env_file_path.simplified_display()
141+
);
142+
}
143+
}
144+
}
145+
}
146+
107147
let Some(command) = command else {
108148
// When a command isn't provided, we'll show a brief help including available tools
109149
show_help(invocation_source, &cache, printer).await?;

crates/uv/src/lib.rs

+2
Original file line numberDiff line numberDiff line change
@@ -1122,6 +1122,8 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
11221122
globals.concurrency,
11231123
cache,
11241124
printer,
1125+
args.env_file,
1126+
args.no_env_file,
11251127
globals.preview,
11261128
))
11271129
.await

crates/uv/src/settings.rs

+6
Original file line numberDiff line numberDiff line change
@@ -466,6 +466,8 @@ pub(crate) struct ToolRunSettings {
466466
pub(crate) refresh: Refresh,
467467
pub(crate) options: ResolverInstallerOptions,
468468
pub(crate) settings: ResolverInstallerSettings,
469+
pub(crate) env_file: Vec<PathBuf>,
470+
pub(crate) no_env_file: bool,
469471
}
470472

471473
impl ToolRunSettings {
@@ -485,6 +487,8 @@ impl ToolRunSettings {
485487
constraints,
486488
overrides,
487489
isolated,
490+
env_file,
491+
no_env_file,
488492
show_resolution,
489493
installer,
490494
build,
@@ -556,6 +560,8 @@ impl ToolRunSettings {
556560
settings,
557561
options,
558562
install_mirrors,
563+
env_file,
564+
no_env_file,
559565
}
560566
}
561567
}

crates/uv/tests/it/tool_run.rs

+94
Original file line numberDiff line numberDiff line change
@@ -2011,6 +2011,100 @@ fn tool_run_python_from() {
20112011
"###);
20122012
}
20132013

2014+
#[test]
2015+
fn run_with_env_file() -> anyhow::Result<()> {
2016+
let context = TestContext::new("3.12").with_filtered_counts();
2017+
let tool_dir = context.temp_dir.child("tools");
2018+
let bin_dir = context.temp_dir.child("bin");
2019+
2020+
// Create a project with a custom script.
2021+
let foo_dir = context.temp_dir.child("foo");
2022+
let foo_pyproject_toml = foo_dir.child("pyproject.toml");
2023+
2024+
foo_pyproject_toml.write_str(indoc! { r#"
2025+
[project]
2026+
name = "foo"
2027+
version = "1.0.0"
2028+
requires-python = ">=3.8"
2029+
dependencies = []
2030+
2031+
[project.scripts]
2032+
script = "foo.main:run"
2033+
2034+
[build-system]
2035+
requires = ["setuptools>=42"]
2036+
build-backend = "setuptools.build_meta"
2037+
"#
2038+
})?;
2039+
2040+
// Create the `foo` module.
2041+
let foo_project_src = foo_dir.child("src");
2042+
let foo_module = foo_project_src.child("foo");
2043+
let foo_main_py = foo_module.child("main.py");
2044+
foo_main_py.write_str(indoc! { r#"
2045+
def run():
2046+
import os
2047+
2048+
print(os.environ.get('THE_EMPIRE_VARIABLE'))
2049+
print(os.environ.get('REBEL_1'))
2050+
print(os.environ.get('REBEL_2'))
2051+
print(os.environ.get('REBEL_3'))
2052+
2053+
__name__ == "__main__" and run()
2054+
"#
2055+
})?;
2056+
2057+
uv_snapshot!(context.filters(), context.tool_run()
2058+
.arg("--from")
2059+
.arg("./foo")
2060+
.arg("script")
2061+
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
2062+
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str()), @r"
2063+
success: true
2064+
exit_code: 0
2065+
----- stdout -----
2066+
None
2067+
None
2068+
None
2069+
None
2070+
2071+
----- stderr -----
2072+
Resolved [N] packages in [TIME]
2073+
Prepared [N] packages in [TIME]
2074+
Installed [N] packages in [TIME]
2075+
+ foo==1.0.0 (from file://[TEMP_DIR]/foo)
2076+
");
2077+
2078+
context.temp_dir.child(".file").write_str(indoc! { "
2079+
THE_EMPIRE_VARIABLE=palpatine
2080+
REBEL_1=leia_organa
2081+
REBEL_2=obi_wan_kenobi
2082+
REBEL_3=C3PO
2083+
"
2084+
})?;
2085+
2086+
uv_snapshot!(context.filters(), context.tool_run()
2087+
.arg("--env-file").arg(".file")
2088+
.arg("--from")
2089+
.arg("./foo")
2090+
.arg("script")
2091+
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
2092+
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str()), @r"
2093+
success: true
2094+
exit_code: 0
2095+
----- stdout -----
2096+
palpatine
2097+
leia_organa
2098+
obi_wan_kenobi
2099+
C3PO
2100+
2101+
----- stderr -----
2102+
Resolved [N] packages in [TIME]
2103+
");
2104+
2105+
Ok(())
2106+
}
2107+
20142108
#[test]
20152109
fn tool_run_from_at() {
20162110
let context = TestContext::new("3.12")

docs/reference/cli.md

+8
Original file line numberDiff line numberDiff line change
@@ -3149,6 +3149,11 @@ uv tool run [OPTIONS] [COMMAND]
31493149

31503150
<p>See <code>--project</code> to only change the project root directory.</p>
31513151

3152+
</dd><dt id="uv-tool-run--env-file"><a href="#uv-tool-run--env-file"><code>--env-file</code></a> <i>env-file</i></dt><dd><p>Load environment variables from a <code>.env</code> file.</p>
3153+
3154+
<p>Can be provided multiple times, with subsequent files overriding values defined in previous files.</p>
3155+
3156+
<p>May also be set with the <code>UV_ENV_FILE</code> environment variable.</p>
31523157
</dd><dt id="uv-tool-run--exclude-newer"><a href="#uv-tool-run--exclude-newer"><code>--exclude-newer</code></a> <i>exclude-newer</i></dt><dd><p>Limit candidate packages to those that were uploaded prior to the given date.</p>
31533158

31543159
<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>
@@ -3293,6 +3298,9 @@ uv tool run [OPTIONS] [COMMAND]
32933298
<p>Normally, configuration files are discovered in the current directory, parent directories, or user configuration directories.</p>
32943299

32953300
<p>May also be set with the <code>UV_NO_CONFIG</code> environment variable.</p>
3301+
</dd><dt id="uv-tool-run--no-env-file"><a href="#uv-tool-run--no-env-file"><code>--no-env-file</code></a></dt><dd><p>Avoid reading environment variables from a <code>.env</code> file</p>
3302+
3303+
<p>May also be set with the <code>UV_NO_ENV_FILE</code> environment variable.</p>
32963304
</dd><dt id="uv-tool-run--no-index"><a href="#uv-tool-run--no-index"><code>--no-index</code></a></dt><dd><p>Ignore the registry index (e.g., PyPI), instead relying on direct URL dependencies and those provided via <code>--find-links</code></p>
32973305

32983306
</dd><dt id="uv-tool-run--no-managed-python"><a href="#uv-tool-run--no-managed-python"><code>--no-managed-python</code></a></dt><dd><p>Disable use of uv-managed Python versions.</p>

0 commit comments

Comments
 (0)