mirror of
https://github.com/astral-sh/uv.git
synced 2025-08-03 10:33:49 +00:00
Prefer running executables in the environment with <name>
over <name>/__main__.py
(#11431)
Closes https://github.com/astral-sh/uv/issues/11423
Closes https://github.com/astral-sh/uv/issues/9167
Closes https://github.com/astral-sh/uv/pull/9722
c23fc4024e
demonstrates the behavior.
This commit is contained in:
parent
070120e1c2
commit
3f6a7f9879
2 changed files with 122 additions and 9 deletions
|
@ -1107,7 +1107,8 @@ pub(crate) enum RunCommand {
|
|||
/// Execute a `pythonw` GUI script.
|
||||
PythonGuiScript(PathBuf, Vec<OsString>),
|
||||
/// Execute a Python package containing a `__main__.py` file.
|
||||
PythonPackage(PathBuf, Vec<OsString>),
|
||||
/// If an entrypoint with the target name is installed in the environment, it is preferred.
|
||||
PythonPackage(OsString, PathBuf, Vec<OsString>),
|
||||
/// Execute a Python [zipapp].
|
||||
/// [zipapp]: <https://docs.python.org/3/library/zipapp.html>
|
||||
PythonZipapp(PathBuf, Vec<OsString>),
|
||||
|
@ -1129,10 +1130,12 @@ impl RunCommand {
|
|||
match self {
|
||||
Self::Python(_)
|
||||
| Self::PythonScript(..)
|
||||
| Self::PythonPackage(..)
|
||||
| Self::PythonZipapp(..)
|
||||
| Self::PythonRemote(..)
|
||||
| Self::Empty => Cow::Borrowed("python"),
|
||||
// N.B. We can't know if we'll invoke `<target>` or `python <target>` without checking
|
||||
// the available scripts in the interpreter — we could improve this message
|
||||
Self::PythonPackage(target, ..) => target.to_string_lossy(),
|
||||
Self::PythonModule(..) => Cow::Borrowed("python -m"),
|
||||
Self::PythonGuiScript(..) => {
|
||||
if cfg!(windows) {
|
||||
|
@ -1161,9 +1164,24 @@ impl RunCommand {
|
|||
process.args(args);
|
||||
process
|
||||
}
|
||||
Self::PythonScript(target, args)
|
||||
| Self::PythonPackage(target, args)
|
||||
| Self::PythonZipapp(target, args) => {
|
||||
Self::PythonPackage(target, path, args) => {
|
||||
let name = PathBuf::from(target).with_extension(std::env::consts::EXE_EXTENSION);
|
||||
let entrypoint = interpreter.scripts().join(name);
|
||||
|
||||
// If the target is an installed, executable script — prefer that
|
||||
if uv_fs::which::is_executable(&entrypoint) {
|
||||
let mut process = Command::new(entrypoint);
|
||||
process.args(args);
|
||||
process
|
||||
// Otherwise, invoke `python <module>`
|
||||
} else {
|
||||
let mut process = Command::new(interpreter.sys_executable());
|
||||
process.arg(path);
|
||||
process.args(args);
|
||||
process
|
||||
}
|
||||
}
|
||||
Self::PythonScript(target, args) | Self::PythonZipapp(target, args) => {
|
||||
let mut process = Command::new(interpreter.sys_executable());
|
||||
process.arg(target);
|
||||
process.args(args);
|
||||
|
@ -1272,9 +1290,14 @@ impl std::fmt::Display for RunCommand {
|
|||
}
|
||||
Ok(())
|
||||
}
|
||||
Self::PythonScript(target, args)
|
||||
| Self::PythonPackage(target, args)
|
||||
| Self::PythonZipapp(target, args) => {
|
||||
Self::PythonPackage(target, _path, args) => {
|
||||
write!(f, "{}", target.to_string_lossy())?;
|
||||
for arg in args {
|
||||
write!(f, " {}", arg.to_string_lossy())?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Self::PythonScript(target, args) | Self::PythonZipapp(target, args) => {
|
||||
write!(f, "python {}", target.display())?;
|
||||
for arg in args {
|
||||
write!(f, " {}", arg.to_string_lossy())?;
|
||||
|
@ -1416,7 +1439,11 @@ impl RunCommand {
|
|||
{
|
||||
Ok(Self::PythonGuiScript(target_path, args.to_vec()))
|
||||
} else if is_dir && target_path.join("__main__.py").is_file() {
|
||||
Ok(Self::PythonPackage(target_path, args.to_vec()))
|
||||
Ok(Self::PythonPackage(
|
||||
target.clone(),
|
||||
target_path,
|
||||
args.to_vec(),
|
||||
))
|
||||
} else if is_file && is_python_zipapp(&target_path) {
|
||||
Ok(Self::PythonZipapp(target_path, args.to_vec()))
|
||||
} else {
|
||||
|
|
|
@ -3151,6 +3151,92 @@ fn run_script_without_build_system() -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_script_module_conflict() -> Result<()> {
|
||||
let context = TestContext::new("3.12");
|
||||
|
||||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||||
pyproject_toml.write_str(indoc! { r#"
|
||||
[project]
|
||||
name = "foo"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = []
|
||||
|
||||
[project.scripts]
|
||||
foo = "foo:app"
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
"#
|
||||
})?;
|
||||
|
||||
let init = context.temp_dir.child("src/foo/__init__.py");
|
||||
init.write_str(indoc! { r#"
|
||||
def app():
|
||||
print("Hello from `__init__`")
|
||||
"#
|
||||
})?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.run().arg("foo"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
Hello from `__init__`
|
||||
|
||||
----- stderr -----
|
||||
Resolved 1 package in [TIME]
|
||||
Prepared 1 package in [TIME]
|
||||
Installed 1 package in [TIME]
|
||||
+ foo==0.1.0 (from file://[TEMP_DIR]/)
|
||||
"###);
|
||||
|
||||
// Creating `__main__` should not change the behavior, the entrypoint should take precedence
|
||||
let main = context.temp_dir.child("src/foo/__main__.py");
|
||||
main.write_str(indoc! { r#"
|
||||
print("Hello from `__main__`")
|
||||
"#
|
||||
})?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.run().arg("foo"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
Hello from `__init__`
|
||||
|
||||
----- stderr -----
|
||||
Resolved 1 package in [TIME]
|
||||
Audited 1 package in [TIME]
|
||||
"###);
|
||||
|
||||
// Even if the working directory is `src`
|
||||
uv_snapshot!(context.filters(), context.run().arg("--directory").arg("src").arg("foo"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
Hello from `__init__`
|
||||
|
||||
----- stderr -----
|
||||
Resolved 1 package in [TIME]
|
||||
Audited 1 package in [TIME]
|
||||
"###);
|
||||
|
||||
// Unless the user opts-in to module running with `-m`
|
||||
uv_snapshot!(context.filters(), context.run().arg("-m").arg("foo"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
Hello from `__main__`
|
||||
|
||||
----- stderr -----
|
||||
Resolved 1 package in [TIME]
|
||||
Audited 1 package in [TIME]
|
||||
"###);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_script_explicit() -> Result<()> {
|
||||
let context = TestContext::new("3.12");
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue