mirror of https://github.com/astral-sh/uv
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
|
|
@ -4093,6 +4093,17 @@ pub struct ToolRunArgs {
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
pub isolated: bool,
|
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)]
|
#[command(flatten)]
|
||||||
pub installer: ResolverInstallerArgs,
|
pub installer: ResolverInstallerArgs,
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -96,6 +96,8 @@ pub(crate) async fn run(
|
||||||
concurrency: Concurrency,
|
concurrency: Concurrency,
|
||||||
cache: Cache,
|
cache: Cache,
|
||||||
printer: Printer,
|
printer: Printer,
|
||||||
|
env_file: Vec<PathBuf>,
|
||||||
|
no_env_file: bool,
|
||||||
preview: PreviewMode,
|
preview: PreviewMode,
|
||||||
) -> anyhow::Result<ExitStatus> {
|
) -> anyhow::Result<ExitStatus> {
|
||||||
/// Whether or not a path looks like a Python script based on the file extension.
|
/// 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"))
|
.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 {
|
let Some(command) = command else {
|
||||||
// When a command isn't provided, we'll show a brief help including available tools
|
// When a command isn't provided, we'll show a brief help including available tools
|
||||||
show_help(invocation_source, &cache, printer).await?;
|
show_help(invocation_source, &cache, printer).await?;
|
||||||
|
|
|
||||||
|
|
@ -1122,6 +1122,8 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
|
||||||
globals.concurrency,
|
globals.concurrency,
|
||||||
cache,
|
cache,
|
||||||
printer,
|
printer,
|
||||||
|
args.env_file,
|
||||||
|
args.no_env_file,
|
||||||
globals.preview,
|
globals.preview,
|
||||||
))
|
))
|
||||||
.await
|
.await
|
||||||
|
|
|
||||||
|
|
@ -466,6 +466,8 @@ pub(crate) struct ToolRunSettings {
|
||||||
pub(crate) refresh: Refresh,
|
pub(crate) refresh: Refresh,
|
||||||
pub(crate) options: ResolverInstallerOptions,
|
pub(crate) options: ResolverInstallerOptions,
|
||||||
pub(crate) settings: ResolverInstallerSettings,
|
pub(crate) settings: ResolverInstallerSettings,
|
||||||
|
pub(crate) env_file: Vec<PathBuf>,
|
||||||
|
pub(crate) no_env_file: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ToolRunSettings {
|
impl ToolRunSettings {
|
||||||
|
|
@ -485,6 +487,8 @@ impl ToolRunSettings {
|
||||||
constraints,
|
constraints,
|
||||||
overrides,
|
overrides,
|
||||||
isolated,
|
isolated,
|
||||||
|
env_file,
|
||||||
|
no_env_file,
|
||||||
show_resolution,
|
show_resolution,
|
||||||
installer,
|
installer,
|
||||||
build,
|
build,
|
||||||
|
|
@ -556,6 +560,8 @@ impl ToolRunSettings {
|
||||||
settings,
|
settings,
|
||||||
options,
|
options,
|
||||||
install_mirrors,
|
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]
|
#[test]
|
||||||
fn tool_run_from_at() {
|
fn tool_run_from_at() {
|
||||||
let context = TestContext::new("3.12")
|
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>
|
<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>
|
</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>
|
<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>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>
|
<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-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>
|
</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…
Reference in New Issue