Skip to content

Commit

Permalink
Implement uv run --directory
Browse files Browse the repository at this point in the history
Allow running scripts or binaries in a project without having to change
directory into the project using uv run.
  • Loading branch information
bluss committed Jul 29, 2024
1 parent f932512 commit e295ed6
Show file tree
Hide file tree
Showing 5 changed files with 78 additions and 5 deletions.
4 changes: 4 additions & 0 deletions crates/uv-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down
15 changes: 10 additions & 5 deletions crates/uv/src/commands/project/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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? {
Expand All @@ -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(None).await? {
} else if let Some(request) = request_from_version_file(Some(&directory)).await? {
Some(request)
// (3) `Requires-Python` in `pyproject.toml`
} else {
Expand Down Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions crates/uv/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -916,6 +916,7 @@ async fn run_project(
args.extras,
args.dev,
args.python,
args.directory,
args.settings,
globals.isolated,
globals.preview,
Expand Down
3 changes: 3 additions & 0 deletions crates/uv/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
Expand All @@ -217,6 +218,7 @@ impl RunSettings {
refresh,
package,
python,
directory,
} = args;

Self {
Expand All @@ -235,6 +237,7 @@ impl RunSettings {
.collect(),
package,
python,
directory,
refresh: Refresh::from(refresh),
settings: ResolverInstallerSettings::combine(
resolver_installer_options(installer, build),
Expand Down
60 changes: 60 additions & 0 deletions crates/uv/tests/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -823,3 +825,61 @@ 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())
"#
})?;

// Our tests change files in <1s, so we must disable CPython bytecode caching with `-B` or we'll
// get stale files, see https://github.com/python/cpython/issues/75953.
let mut command = context.run();
let command_with_args = command
.arg("--preview")
.arg("--directory")
.arg("project")
.arg("main");
uv_snapshot!(context.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: project/.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(())
}

0 comments on commit e295ed6

Please sign in to comment.