mirror of
https://github.com/astral-sh/uv.git
synced 2025-08-03 18:38:21 +00:00
Implement uv run --directory
(#5566)
## Summary uv run --directory <path> means that one doesn't have to change to a project's directory to run programs from it. It makes it possible to use projects as if they are tool installations. To support this, first the code reading .python-version was updated so that it can read such markers outside the current directory. Note the minor change this causes (if I'm right), described in the commit. ## Test Plan One test has been added. ## --directory Not sure what the name of the argument should be, but it's following uv sync's directory for now. Other alternatives could be "--project". Uv run and uv tool run should probably find common agreement on this (relevant for project-locked tools). I've implemented this same change in Rye, some time ago, and then we went with --pyproject `<`path to pyproject.toml file`>`. I think using pyproject.toml file path and not directory was probably a mistake, an overgeneralization one doesn't need.
This commit is contained in:
parent
cf94a10054
commit
e46c24d3cf
10 changed files with 119 additions and 22 deletions
|
@ -1932,6 +1932,10 @@ pub struct RunArgs {
|
|||
/// - `/home/ferris/.local/bin/python3.10` uses the exact Python at the given path.
|
||||
#[arg(long, short, env = "UV_PYTHON", verbatim_doc_comment)]
|
||||
pub python: Option<String>,
|
||||
|
||||
/// The path to the project. Defaults to the current working directory.
|
||||
#[arg(long, hide = true)]
|
||||
pub directory: Option<PathBuf>,
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
use std::path::Path;
|
||||
|
||||
use fs_err as fs;
|
||||
use tracing::debug;
|
||||
|
||||
|
@ -13,15 +15,17 @@ pub static PYTHON_VERSIONS_FILENAME: &str = ".python-versions";
|
|||
///
|
||||
/// Prefers `.python-versions` then `.python-version`.
|
||||
/// If only one Python version is desired, use [`request_from_version_files`] which prefers the `.python-version` file.
|
||||
pub async fn requests_from_version_file() -> Result<Option<Vec<PythonRequest>>, std::io::Error> {
|
||||
if let Some(versions) = read_versions_file().await? {
|
||||
pub async fn requests_from_version_file(
|
||||
directory: Option<&Path>,
|
||||
) -> Result<Option<Vec<PythonRequest>>, std::io::Error> {
|
||||
if let Some(versions) = read_versions_file(directory).await? {
|
||||
Ok(Some(
|
||||
versions
|
||||
.into_iter()
|
||||
.map(|version| PythonRequest::parse(&version))
|
||||
.collect(),
|
||||
))
|
||||
} else if let Some(version) = read_version_file().await? {
|
||||
} else if let Some(version) = read_version_file(directory).await? {
|
||||
Ok(Some(vec![PythonRequest::parse(&version)]))
|
||||
} else {
|
||||
Ok(None)
|
||||
|
@ -30,12 +34,17 @@ pub async fn requests_from_version_file() -> Result<Option<Vec<PythonRequest>>,
|
|||
|
||||
/// Read a [`PythonRequest`] from a version file, if present.
|
||||
///
|
||||
/// Find the version file inside directory, or the current directory
|
||||
/// if None.
|
||||
///
|
||||
/// Prefers `.python-version` then the first entry of `.python-versions`.
|
||||
/// If multiple Python versions are desired, use [`requests_from_version_files`] instead.
|
||||
pub async fn request_from_version_file() -> Result<Option<PythonRequest>, std::io::Error> {
|
||||
if let Some(version) = read_version_file().await? {
|
||||
pub async fn request_from_version_file(
|
||||
directory: Option<&Path>,
|
||||
) -> Result<Option<PythonRequest>, std::io::Error> {
|
||||
if let Some(version) = read_version_file(directory).await? {
|
||||
Ok(Some(PythonRequest::parse(&version)))
|
||||
} else if let Some(versions) = read_versions_file().await? {
|
||||
} else if let Some(versions) = read_versions_file(directory).await? {
|
||||
Ok(versions
|
||||
.into_iter()
|
||||
.next()
|
||||
|
@ -52,10 +61,17 @@ pub async fn write_version_file(version: &str) -> Result<(), std::io::Error> {
|
|||
fs::tokio::write(PYTHON_VERSION_FILENAME, format!("{version}\n")).await
|
||||
}
|
||||
|
||||
async fn read_versions_file() -> Result<Option<Vec<String>>, std::io::Error> {
|
||||
match fs::tokio::read_to_string(PYTHON_VERSIONS_FILENAME).await {
|
||||
async fn read_versions_file(
|
||||
directory: Option<&Path>,
|
||||
) -> Result<Option<Vec<String>>, std::io::Error> {
|
||||
let file_path = directory.map(|pth| pth.join(PYTHON_VERSIONS_FILENAME));
|
||||
let path = file_path
|
||||
.as_deref()
|
||||
.unwrap_or(Path::new(PYTHON_VERSIONS_FILENAME));
|
||||
|
||||
match fs::tokio::read_to_string(path).await {
|
||||
Ok(content) => {
|
||||
debug!("Reading requests from `{PYTHON_VERSIONS_FILENAME}`");
|
||||
debug!("Reading requests from `{}`", path.display());
|
||||
Ok(Some(
|
||||
content
|
||||
.lines()
|
||||
|
@ -73,10 +89,15 @@ async fn read_versions_file() -> Result<Option<Vec<String>>, std::io::Error> {
|
|||
}
|
||||
}
|
||||
|
||||
async fn read_version_file() -> Result<Option<String>, std::io::Error> {
|
||||
match fs::tokio::read_to_string(PYTHON_VERSION_FILENAME).await {
|
||||
async fn read_version_file(directory: Option<&Path>) -> Result<Option<String>, std::io::Error> {
|
||||
let file_path = directory.map(|pth| pth.join(PYTHON_VERSION_FILENAME));
|
||||
let path = file_path
|
||||
.as_deref()
|
||||
.unwrap_or(Path::new(PYTHON_VERSION_FILENAME));
|
||||
|
||||
match fs::tokio::read_to_string(path).await {
|
||||
Ok(content) => {
|
||||
debug!("Reading requests from `{PYTHON_VERSION_FILENAME}`");
|
||||
debug!("Reading requests from `{}`", path.display());
|
||||
Ok(content
|
||||
.lines()
|
||||
.find(|line| {
|
||||
|
|
|
@ -161,7 +161,9 @@ impl FoundInterpreter {
|
|||
let python_request = if let Some(request) = python_request {
|
||||
Some(request)
|
||||
// (2) Request from `.python-version`
|
||||
} else if let Some(request) = request_from_version_file().await? {
|
||||
} else if let Some(request) =
|
||||
request_from_version_file(Some(workspace.install_path())).await?
|
||||
{
|
||||
Some(request)
|
||||
// (3) `Requires-Python` in `pyproject.toml`
|
||||
} else {
|
||||
|
|
|
@ -44,6 +44,7 @@ pub(crate) async fn run(
|
|||
extras: ExtrasSpecification,
|
||||
dev: bool,
|
||||
python: Option<String>,
|
||||
directory: Option<PathBuf>,
|
||||
settings: ResolverInstallerSettings,
|
||||
isolated: bool,
|
||||
preview: PreviewMode,
|
||||
|
@ -89,6 +90,12 @@ pub(crate) async fn run(
|
|||
|
||||
let reporter = PythonDownloadReporter::single(printer);
|
||||
|
||||
let directory = if let Some(directory) = directory {
|
||||
directory.simple_canonicalize()?
|
||||
} else {
|
||||
std::env::current_dir()?
|
||||
};
|
||||
|
||||
// Determine whether the command to execute is a PEP 723 script.
|
||||
let script_interpreter = if let RunCommand::Python(target, _) = &command {
|
||||
if let Some(metadata) = uv_scripts::read_pep723_metadata(&target).await? {
|
||||
|
@ -102,7 +109,7 @@ pub(crate) async fn run(
|
|||
let python_request = if let Some(request) = python.as_deref() {
|
||||
Some(PythonRequest::parse(request))
|
||||
// (2) Request from `.python-version`
|
||||
} else if let Some(request) = request_from_version_file().await? {
|
||||
} else if let Some(request) = request_from_version_file(Some(&directory)).await? {
|
||||
Some(request)
|
||||
// (3) `Requires-Python` in `pyproject.toml`
|
||||
} else {
|
||||
|
@ -167,15 +174,13 @@ pub(crate) async fn run(
|
|||
// We need a workspace, but we don't need to have a current package, we can be e.g. in
|
||||
// the root of a virtual workspace and then switch into the selected package.
|
||||
Some(VirtualProject::Project(
|
||||
Workspace::discover(&std::env::current_dir()?, &DiscoveryOptions::default())
|
||||
Workspace::discover(&directory, &DiscoveryOptions::default())
|
||||
.await?
|
||||
.with_current_project(package.clone())
|
||||
.with_context(|| format!("Package `{package}` not found in workspace"))?,
|
||||
))
|
||||
} else {
|
||||
match VirtualProject::discover(&std::env::current_dir()?, &DiscoveryOptions::default())
|
||||
.await
|
||||
{
|
||||
match VirtualProject::discover(&directory, &DiscoveryOptions::default()).await {
|
||||
Ok(project) => Some(project),
|
||||
Err(WorkspaceError::MissingPyprojectToml) => None,
|
||||
Err(WorkspaceError::NonWorkspace(_)) => None,
|
||||
|
|
|
@ -55,7 +55,7 @@ pub(crate) async fn install(
|
|||
}
|
||||
None
|
||||
} else {
|
||||
requests_from_version_file().await?
|
||||
requests_from_version_file(None).await?
|
||||
};
|
||||
version_file_requests.unwrap_or_else(|| vec![PythonRequest::Any])
|
||||
} else {
|
||||
|
|
|
@ -49,7 +49,7 @@ pub(crate) async fn pin(
|
|||
|
||||
let Some(request) = request else {
|
||||
// Display the current pinned Python version
|
||||
if let Some(pins) = requests_from_version_file().await? {
|
||||
if let Some(pins) = requests_from_version_file(None).await? {
|
||||
for pin in pins {
|
||||
writeln!(printer.stdout(), "{}", pin.to_canonical_string())?;
|
||||
if let Some(virtual_project) = &virtual_project {
|
||||
|
@ -126,7 +126,7 @@ pub(crate) async fn pin(
|
|||
request.to_canonical_string()
|
||||
};
|
||||
|
||||
let existing = request_from_version_file().await.ok().flatten();
|
||||
let existing = request_from_version_file(None).await.ok().flatten();
|
||||
write_version_file(&output).await?;
|
||||
|
||||
if let Some(existing) = existing
|
||||
|
|
|
@ -140,7 +140,7 @@ async fn venv_impl(
|
|||
|
||||
let mut interpreter_request = python_request.map(PythonRequest::parse);
|
||||
if preview.is_enabled() && interpreter_request.is_none() {
|
||||
interpreter_request = request_from_version_file().await.into_diagnostic()?;
|
||||
interpreter_request = request_from_version_file(None).await.into_diagnostic()?;
|
||||
}
|
||||
if preview.is_disabled() && relocatable {
|
||||
warn_user_once!("`--relocatable` is experimental and may change without warning");
|
||||
|
|
|
@ -916,6 +916,7 @@ async fn run_project(
|
|||
args.extras,
|
||||
args.dev,
|
||||
args.python,
|
||||
args.directory,
|
||||
args.settings,
|
||||
globals.isolated,
|
||||
globals.preview,
|
||||
|
|
|
@ -193,6 +193,7 @@ pub(crate) struct RunSettings {
|
|||
pub(crate) with_requirements: Vec<PathBuf>,
|
||||
pub(crate) package: Option<PackageName>,
|
||||
pub(crate) python: Option<String>,
|
||||
pub(crate) directory: Option<PathBuf>,
|
||||
pub(crate) refresh: Refresh,
|
||||
pub(crate) settings: ResolverInstallerSettings,
|
||||
}
|
||||
|
@ -217,6 +218,7 @@ impl RunSettings {
|
|||
refresh,
|
||||
package,
|
||||
python,
|
||||
directory,
|
||||
} = args;
|
||||
|
||||
Self {
|
||||
|
@ -235,6 +237,7 @@ impl RunSettings {
|
|||
.collect(),
|
||||
package,
|
||||
python,
|
||||
directory,
|
||||
refresh: Refresh::from(refresh),
|
||||
settings: ResolverInstallerSettings::combine(
|
||||
resolver_installer_options(installer, build),
|
||||
|
|
|
@ -5,6 +5,8 @@ use assert_cmd::assert::OutputAssertExt;
|
|||
use assert_fs::{fixture::ChildPath, prelude::*};
|
||||
use indoc::indoc;
|
||||
|
||||
use uv_python::PYTHON_VERSION_FILENAME;
|
||||
|
||||
use common::{uv_snapshot, TestContext};
|
||||
|
||||
mod common;
|
||||
|
@ -823,3 +825,62 @@ fn run_editable() -> Result<()> {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_from_directory() -> Result<()> {
|
||||
// default 3.11 so that the .python-version is meaningful
|
||||
let context = TestContext::new_with_versions(&["3.11", "3.12"]);
|
||||
|
||||
let project_dir = context.temp_dir.child("project");
|
||||
project_dir.create_dir_all()?;
|
||||
project_dir
|
||||
.child(PYTHON_VERSION_FILENAME)
|
||||
.write_str("3.12")?;
|
||||
|
||||
let pyproject_toml = project_dir.child("pyproject.toml");
|
||||
pyproject_toml.write_str(indoc! { r#"
|
||||
[project]
|
||||
name = "foo"
|
||||
version = "1.0.0"
|
||||
requires-python = ">=3.11, <4"
|
||||
dependencies = []
|
||||
|
||||
[project.scripts]
|
||||
main = "main:main"
|
||||
"#
|
||||
})?;
|
||||
let main_script = project_dir.child("main.py");
|
||||
main_script.write_str(indoc! { r"
|
||||
import platform
|
||||
|
||||
def main():
|
||||
print(platform.python_version())
|
||||
"
|
||||
})?;
|
||||
|
||||
let mut command = context.run();
|
||||
let command_with_args = command
|
||||
.arg("--preview")
|
||||
.arg("--directory")
|
||||
.arg("project")
|
||||
.arg("main");
|
||||
|
||||
let mut filters = context.filters();
|
||||
filters.push((r"project(\\|/).venv", "[VENV]"));
|
||||
uv_snapshot!(filters, command_with_args, @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
3.12.[X]
|
||||
|
||||
----- stderr -----
|
||||
Using Python 3.12.[X] interpreter at: [PYTHON-3.12]
|
||||
Creating virtualenv at: [VENV]
|
||||
Resolved 1 package in [TIME]
|
||||
Prepared 1 package in [TIME]
|
||||
Installed 1 package in [TIME]
|
||||
+ foo==1.0.0 (from file://[TEMP_DIR]/project)
|
||||
"###);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue