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

Support python find --script #11891

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
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
10 changes: 10 additions & 0 deletions crates/uv-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4637,6 +4637,16 @@ pub struct PythonFindArgs {

#[arg(long, overrides_with("system"), hide = true)]
pub no_system: bool,

/// Find the environment for a Python script, rather than the current project.
#[arg(
long,
conflicts_with = "request",
conflicts_with = "no_project",
conflicts_with = "system",
conflicts_with = "no_system"
)]
pub script: Option<PathBuf>,
}

#[derive(Args)]
Expand Down
1 change: 1 addition & 0 deletions crates/uv/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ pub(crate) use project::tree::tree;
pub(crate) use publish::publish;
pub(crate) use python::dir::dir as python_dir;
pub(crate) use python::find::find as python_find;
pub(crate) use python::find::find_script as python_find_script;
pub(crate) use python::install::install as python_install;
pub(crate) use python::list::list as python_list;
pub(crate) use python::pin::pin as python_pin;
Expand Down
56 changes: 54 additions & 2 deletions crates/uv/src/commands/python/find.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
use anstream::println;
use anyhow::Result;
use std::fmt::Write;
use std::path::Path;

use uv_cache::Cache;
use uv_fs::Simplified;
use uv_python::{EnvironmentPreference, PythonInstallation, PythonPreference, PythonRequest};
use uv_python::{
EnvironmentPreference, PythonDownloads, PythonInstallation, PythonPreference, PythonRequest,
};
use uv_scripts::Pep723ItemRef;
use uv_settings::PythonInstallMirrors;
use uv_warnings::{warn_user, warn_user_once};
use uv_workspace::{DiscoveryOptions, VirtualProject, WorkspaceError};

use crate::commands::{
project::{validate_project_requires_python, WorkspacePython},
project::{validate_project_requires_python, ScriptInterpreter, WorkspacePython},
ExitStatus,
};
use crate::printer::Printer;
use crate::settings::NetworkSettings;

/// Find a Python interpreter.
pub(crate) async fn find(
Expand Down Expand Up @@ -85,3 +92,48 @@ pub(crate) async fn find(

Ok(ExitStatus::Success)
}

pub(crate) async fn find_script(
script: Pep723ItemRef<'_>,
network_settings: &NetworkSettings,
python_preference: PythonPreference,
python_downloads: PythonDownloads,
no_config: bool,
cache: &Cache,
printer: Printer,
) -> Result<ExitStatus> {
match ScriptInterpreter::discover(
script,
None,
network_settings,
python_preference,
python_downloads,
&PythonInstallMirrors::default(),
no_config,
Some(false),
cache,
printer,
)
.await
{
Err(error) => {
writeln!(printer.stderr(), "{error}")?;

Ok(ExitStatus::Failure)
}

Ok(ScriptInterpreter::Interpreter(interpreter)) => {
let path = interpreter.sys_executable();
println!("{}", std::path::absolute(path)?.simplified_display());

Ok(ExitStatus::Success)
}

Ok(ScriptInterpreter::Environment(environment)) => {
let path = environment.interpreter().sys_executable();
println!("{}", std::path::absolute(path)?.simplified_display());

Ok(ExitStatus::Success)
}
}
}
61 changes: 50 additions & 11 deletions crates/uv/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ use uv_cli::{PythonCommand, PythonNamespace, ToolCommand, ToolNamespace, TopLeve
use uv_cli::{SelfCommand, SelfNamespace, SelfUpdateArgs};
use uv_fs::{Simplified, CWD};
use uv_requirements::RequirementsSource;
use uv_scripts::{Pep723Error, Pep723Item, Pep723Metadata, Pep723Script};
use uv_scripts::{Pep723Error, Pep723Item, Pep723ItemRef, Pep723Metadata, Pep723Script};
use uv_settings::{Combine, FilesystemOptions, Options};
use uv_static::EnvVars;
use uv_warnings::{warn_user, warn_user_once};
Expand Down Expand Up @@ -241,6 +241,32 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
},
_ => None,
}
} else if let Commands::Python(uv_cli::PythonNamespace {
command:
PythonCommand::Find(uv_cli::PythonFindArgs {
script: Some(script),
..
}),
}) = &*cli.command
{
match Pep723Script::read(&script).await {
Ok(Some(script)) => Some(Pep723Item::Script(script)),
Ok(None) => {
bail!(
"`{}` does not contain a PEP 723 metadata tag; run `{}` to initialize the script",
script.user_display().cyan(),
format!("uv init --script {}", script.user_display()).green()
)
}
Err(Pep723Error::Io(err)) if err.kind() == std::io::ErrorKind::NotFound => {
bail!(
"Failed to read `{}` (not found); run `{}` to create a PEP 723 script",
script.user_display().cyan(),
format!("uv init --script {}", script.user_display()).green()
)
}
Err(err) => return Err(err.into()),
}
} else {
None
};
Expand Down Expand Up @@ -1230,16 +1256,29 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
// Initialize the cache.
let cache = cache.init()?;

commands::python_find(
&project_dir,
args.request,
args.no_project,
cli.top_level.no_config,
args.system,
globals.python_preference,
&cache,
)
.await
if let Some(Pep723Item::Script(script)) = script {
commands::python_find_script(
Pep723ItemRef::Script(&script),
&globals.network_settings,
globals.python_preference,
globals.python_downloads,
cli.top_level.no_config,
&cache,
printer,
)
.await
} else {
commands::python_find(
&project_dir,
args.request,
args.no_project,
cli.top_level.no_config,
args.system,
globals.python_preference,
&cache,
)
.await
}
}
Commands::Python(PythonNamespace {
command: PythonCommand::Pin(args),
Expand Down
1 change: 1 addition & 0 deletions crates/uv/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -962,6 +962,7 @@ impl PythonFindSettings {
no_project,
system,
no_system,
script: _,
} = args;

Self {
Expand Down
183 changes: 183 additions & 0 deletions crates/uv/tests/it/python_find.rs
Original file line number Diff line number Diff line change
Expand Up @@ -707,3 +707,186 @@ fn python_required_python_major_minor() {
error: No interpreter found for Python >3.11.[X], <3.12 in virtual environments, managed installations, or search path
"###);
}

#[test]
fn python_find_script() {
let context = TestContext::new("3.13");
let filters = context
.filters()
.into_iter()
.chain(vec![(
r"environments-v2/[\w-]+",
"environments-v2/[HASHEDNAME]",
)])
.collect::<Vec<_>>();

uv_snapshot!(filters, context.init().arg("--script").arg("foo.py"), @r###"
success: true
exit_code: 0
----- stdout -----

----- stderr -----
Initialized script at `foo.py`
"###);

uv_snapshot!(filters, context.sync().arg("--script").arg("foo.py"), @r###"
success: true
exit_code: 0
----- stdout -----

----- stderr -----
Creating script environment at: [CACHE_DIR]/environments-v2/[HASHEDNAME]
"###);

if cfg!(windows) {
uv_snapshot!(filters, context.python_find().arg("--script").arg("foo.py"), @r###"
success: true
exit_code: 0
----- stdout -----
[CACHE_DIR]/environments-v2/[HASHEDNAME]/Scripts/python.exe

----- stderr -----
"###);
} else {
uv_snapshot!(filters, context.python_find().arg("--script").arg("foo.py"), @r###"
success: true
exit_code: 0
----- stdout -----
[CACHE_DIR]/environments-v2/[HASHEDNAME]/bin/python3

----- stderr -----
"###);
}
}

#[test]
fn python_find_script_no_environment() {
let context = TestContext::new("3.13");

let script = context.temp_dir.child("foo.py");

script
.write_str(indoc! {r"
# /// script
# dependencies = []
# ///
"})
.unwrap();

if cfg!(windows) {
uv_snapshot!(context.filters(), context.python_find().arg("--script").arg("foo.py"), @r###"
success: true
exit_code: 0
----- stdout -----
[VENV]/Scripts/python.exe

----- stderr -----
"###);
} else {
uv_snapshot!(context.filters(), context.python_find().arg("--script").arg("foo.py"), @r###"
success: true
exit_code: 0
----- stdout -----
[VENV]/bin/python3

----- stderr -----
"###);
}
}

#[test]
fn python_find_script_python_not_found() {
let context = TestContext::new_with_versions(&[]);

let script = context.temp_dir.child("foo.py");

script
.write_str(indoc! {r"
# /// script
# dependencies = []
# ///
"})
.unwrap();

if cfg!(windows) {
uv_snapshot!(context.filters(), context.python_find().arg("--script").arg("foo.py"), @r"
success: false
exit_code: 1
----- stdout -----

----- stderr -----
No interpreter found in virtual environments, managed installations, search path, or registry
");
} else {
uv_snapshot!(context.filters(), context.python_find().arg("--script").arg("foo.py"), @r"
success: false
exit_code: 1
----- stdout -----

----- stderr -----
No interpreter found in virtual environments, managed installations, or search path
");
}
}

#[test]
fn python_find_script_no_such_version() {
let context = TestContext::new("3.13");
let filters = context
.filters()
.into_iter()
.chain(vec![(
r"environments-v2/[\w-]+",
"environments-v2/[HASHEDNAME]",
)])
.collect::<Vec<_>>();

let script = context.temp_dir.child("foo.py");

script
.write_str(indoc! {r#"
# /// script
# requires-python = ">=3.13"
# dependencies = []
# ///
"#})
.unwrap();

uv_snapshot!(filters, context.sync().arg("--script").arg("foo.py"), @r###"
success: true
exit_code: 0
----- stdout -----

----- stderr -----
Creating script environment at: [CACHE_DIR]/environments-v2/[HASHEDNAME]
"###);

script
.write_str(indoc! {r#"
# /// script
# requires-python = ">=3.14"
# dependencies = []
# ///
"#})
.unwrap();

if cfg!(windows) {
uv_snapshot!(filters, context.python_find().arg("--script").arg("foo.py"), @r"
success: false
exit_code: 1
----- stdout -----

----- stderr -----
No interpreter found for Python >=3.14 in virtual environments, managed installations, search path, or registry
");
} else {
uv_snapshot!(filters, context.python_find().arg("--script").arg("foo.py"), @r"
success: false
exit_code: 1
----- stdout -----

----- stderr -----
No interpreter found for Python >=3.14 in virtual environments, managed installations, or search path
");
}
}
2 changes: 2 additions & 0 deletions docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -5099,6 +5099,8 @@ uv python find [OPTIONS] [REQUEST]
</ul>
</dd><dt id="uv-python-find--quiet"><a href="#uv-python-find--quiet"><code>--quiet</code></a>, <code>-q</code></dt><dd><p>Do not print any output</p>

</dd><dt id="uv-python-find--script"><a href="#uv-python-find--script"><code>--script</code></a> <i>script</i></dt><dd><p>Find the environment for a Python script, rather than the current project</p>

</dd><dt id="uv-python-find--system"><a href="#uv-python-find--system"><code>--system</code></a></dt><dd><p>Only find system Python interpreters.</p>

<p>By default, uv will report the first Python interpreter it would use, including those in an active virtual environment or a virtual environment in the current working directory or any parent directory.</p>
Expand Down
Loading