mirror of
https://github.com/astral-sh/uv.git
synced 2025-10-17 13:58:29 +00:00
Support .env
files in uv tool run
(#12386)
## Summary Closes https://github.com/astral-sh/uv/issues/12371.
This commit is contained in:
parent
42a87da857
commit
2b3d6fd7b6
6 changed files with 161 additions and 0 deletions
|
@ -4093,6 +4093,17 @@ pub struct ToolRunArgs {
|
|||
#[arg(long)]
|
||||
pub isolated: bool,
|
||||
|
||||
/// Load environment variables from a `.env` file.
|
||||
///
|
||||
/// Can be provided multiple times, with subsequent files overriding values defined in previous
|
||||
/// files.
|
||||
#[arg(long, value_delimiter = ' ', 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,
|
||||
|
||||
#[command(flatten)]
|
||||
pub installer: ResolverInstallerArgs,
|
||||
|
||||
|
|
|
@ -96,6 +96,8 @@ pub(crate) async fn run(
|
|||
concurrency: Concurrency,
|
||||
cache: Cache,
|
||||
printer: Printer,
|
||||
env_file: Vec<PathBuf>,
|
||||
no_env_file: bool,
|
||||
preview: PreviewMode,
|
||||
) -> anyhow::Result<ExitStatus> {
|
||||
/// Whether or not a path looks like a Python script based on the file extension.
|
||||
|
@ -104,6 +106,44 @@ pub(crate) async fn run(
|
|||
.is_some_and(|ext| ext.eq_ignore_ascii_case("py") || ext.eq_ignore_ascii_case("pyw"))
|
||||
}
|
||||
|
||||
// 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()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let Some(command) = command else {
|
||||
// When a command isn't provided, we'll show a brief help including available tools
|
||||
show_help(invocation_source, &cache, printer).await?;
|
||||
|
|
|
@ -1122,6 +1122,8 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
|
|||
globals.concurrency,
|
||||
cache,
|
||||
printer,
|
||||
args.env_file,
|
||||
args.no_env_file,
|
||||
globals.preview,
|
||||
))
|
||||
.await
|
||||
|
|
|
@ -466,6 +466,8 @@ pub(crate) struct ToolRunSettings {
|
|||
pub(crate) refresh: Refresh,
|
||||
pub(crate) options: ResolverInstallerOptions,
|
||||
pub(crate) settings: ResolverInstallerSettings,
|
||||
pub(crate) env_file: Vec<PathBuf>,
|
||||
pub(crate) no_env_file: bool,
|
||||
}
|
||||
|
||||
impl ToolRunSettings {
|
||||
|
@ -485,6 +487,8 @@ impl ToolRunSettings {
|
|||
constraints,
|
||||
overrides,
|
||||
isolated,
|
||||
env_file,
|
||||
no_env_file,
|
||||
show_resolution,
|
||||
installer,
|
||||
build,
|
||||
|
@ -556,6 +560,8 @@ impl ToolRunSettings {
|
|||
settings,
|
||||
options,
|
||||
install_mirrors,
|
||||
env_file,
|
||||
no_env_file,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2011,6 +2011,100 @@ fn tool_run_python_from() {
|
|||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_with_env_file() -> anyhow::Result<()> {
|
||||
let context = TestContext::new("3.12").with_filtered_counts();
|
||||
let tool_dir = context.temp_dir.child("tools");
|
||||
let bin_dir = context.temp_dir.child("bin");
|
||||
|
||||
// Create a project with a custom script.
|
||||
let foo_dir = context.temp_dir.child("foo");
|
||||
let foo_pyproject_toml = foo_dir.child("pyproject.toml");
|
||||
|
||||
foo_pyproject_toml.write_str(indoc! { r#"
|
||||
[project]
|
||||
name = "foo"
|
||||
version = "1.0.0"
|
||||
requires-python = ">=3.8"
|
||||
dependencies = []
|
||||
|
||||
[project.scripts]
|
||||
script = "foo.main:run"
|
||||
|
||||
[build-system]
|
||||
requires = ["setuptools>=42"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
"#
|
||||
})?;
|
||||
|
||||
// Create the `foo` module.
|
||||
let foo_project_src = foo_dir.child("src");
|
||||
let foo_module = foo_project_src.child("foo");
|
||||
let foo_main_py = foo_module.child("main.py");
|
||||
foo_main_py.write_str(indoc! { r#"
|
||||
def run():
|
||||
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'))
|
||||
|
||||
__name__ == "__main__" and run()
|
||||
"#
|
||||
})?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.tool_run()
|
||||
.arg("--from")
|
||||
.arg("./foo")
|
||||
.arg("script")
|
||||
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
|
||||
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str()), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
None
|
||||
None
|
||||
None
|
||||
None
|
||||
|
||||
----- stderr -----
|
||||
Resolved [N] packages in [TIME]
|
||||
Prepared [N] packages in [TIME]
|
||||
Installed [N] packages in [TIME]
|
||||
+ foo==1.0.0 (from file://[TEMP_DIR]/foo)
|
||||
");
|
||||
|
||||
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.tool_run()
|
||||
.arg("--env-file").arg(".file")
|
||||
.arg("--from")
|
||||
.arg("./foo")
|
||||
.arg("script")
|
||||
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
|
||||
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str()), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
palpatine
|
||||
leia_organa
|
||||
obi_wan_kenobi
|
||||
C3PO
|
||||
|
||||
----- stderr -----
|
||||
Resolved [N] packages in [TIME]
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_run_from_at() {
|
||||
let context = TestContext::new("3.12")
|
||||
|
|
|
@ -3149,6 +3149,11 @@ uv tool run [OPTIONS] [COMMAND]
|
|||
|
||||
<p>See <code>--project</code> to only change the project root directory.</p>
|
||||
|
||||
</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>
|
||||
|
||||
<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 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>
|
||||
|
||||
<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>
|
||||
|
@ -3293,6 +3298,9 @@ uv tool run [OPTIONS] [COMMAND]
|
|||
<p>Normally, configuration files are discovered in the current directory, parent directories, or user configuration directories.</p>
|
||||
|
||||
<p>May also be set with the <code>UV_NO_CONFIG</code> environment variable.</p>
|
||||
</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>
|
||||
|
||||
<p>May also be set with the <code>UV_NO_ENV_FILE</code> environment variable.</p>
|
||||
</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>
|
||||
|
||||
</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>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue