Skip to content

Commit

Permalink
Add a --project argument to run a command from a project
Browse files Browse the repository at this point in the history
  • Loading branch information
charliermarsh committed Sep 20, 2024
1 parent b918557 commit 9e805e6
Show file tree
Hide file tree
Showing 20 changed files with 394 additions and 86 deletions.
8 changes: 8 additions & 0 deletions crates/uv-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,14 @@ pub struct GlobalArgs {
/// Change to the given directory prior to running the command.
#[arg(global = true, long, hide = true)]
pub directory: Option<PathBuf>,

/// Run the command within the given project.
///
/// All `pyproject.toml`, `uv.toml`, and `.python-version` files will be discovered by walking
/// up the directory tree from the project root, while relative paths will be resolved relative
/// to the current working directory.
#[arg(global = true, long)]
pub project: Option<PathBuf>,
}

#[derive(Debug, Copy, Clone, clap::ValueEnum)]
Expand Down
16 changes: 8 additions & 8 deletions crates/uv-settings/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,8 @@ impl FilesystemOptions {
///
/// The search starts at the given path and goes up the directory tree until a `uv.toml` file or
/// `pyproject.toml` file is found.
pub fn find(path: impl AsRef<Path>) -> Result<Option<Self>, Error> {
for ancestor in path.as_ref().ancestors() {
pub fn find(path: &Path) -> Result<Option<Self>, Error> {
for ancestor in path.ancestors() {
match Self::from_directory(ancestor) {
Ok(Some(options)) => {
return Ok(Some(options));
Expand All @@ -91,17 +91,17 @@ impl FilesystemOptions {

/// Load a [`FilesystemOptions`] from a directory, preferring a `uv.toml` file over a
/// `pyproject.toml` file.
pub fn from_directory(dir: impl AsRef<Path>) -> Result<Option<Self>, Error> {
pub fn from_directory(dir: &Path) -> Result<Option<Self>, Error> {
// Read a `uv.toml` file in the current directory.
let path = dir.as_ref().join("uv.toml");
let path = dir.join("uv.toml");
match fs_err::read_to_string(&path) {
Ok(content) => {
let options: Options = toml::from_str(&content)
.map_err(|err| Error::UvToml(path.user_display().to_string(), err))?;

// If the directory also contains a `[tool.uv]` table in a `pyproject.toml` file,
// warn.
let pyproject = dir.as_ref().join("pyproject.toml");
let pyproject = dir.join("pyproject.toml");
if let Some(pyproject) = fs_err::read_to_string(pyproject)
.ok()
.and_then(|content| toml::from_str::<PyProjectToml>(&content).ok())
Expand All @@ -121,7 +121,7 @@ impl FilesystemOptions {
}

// Read a `pyproject.toml` file in the current directory.
let path = dir.as_ref().join("pyproject.toml");
let path = dir.join("pyproject.toml");
match fs_err::read_to_string(&path) {
Ok(content) => {
// Parse, but skip any `pyproject.toml` that doesn't have a `[tool.uv]` section.
Expand All @@ -130,14 +130,14 @@ impl FilesystemOptions {
let Some(tool) = pyproject.tool else {
debug!(
"Skipping `pyproject.toml` in `{}` (no `[tool]` section)",
dir.as_ref().display()
dir.display()
);
return Ok(None);
};
let Some(options) = tool.uv else {
debug!(
"Skipping `pyproject.toml` in `{}` (no `[tool.uv]` section)",
dir.as_ref().display()
dir.display()
);
return Ok(None);
};
Expand Down
7 changes: 5 additions & 2 deletions crates/uv/src/commands/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ use uv_cache::Cache;
use uv_client::{BaseClientBuilder, Connectivity, FlatIndexClient, RegistryClientBuilder};
use uv_configuration::{BuildKind, BuildOutput, Concurrency, Constraints, HashCheckingMode};
use uv_dispatch::BuildDispatch;
use uv_fs::{Simplified, CWD};
use uv_fs::Simplified;
use uv_normalize::PackageName;
use uv_python::{
EnvironmentPreference, PythonDownloads, PythonEnvironment, PythonInstallation,
Expand All @@ -29,6 +29,7 @@ use uv_workspace::{DiscoveryOptions, Workspace};
/// Build source distributions and wheels.
#[allow(clippy::fn_params_excessive_bools)]
pub(crate) async fn build(
project_dir: &Path,
src: Option<PathBuf>,
package: Option<PackageName>,
output_dir: Option<PathBuf>,
Expand All @@ -48,6 +49,7 @@ pub(crate) async fn build(
printer: Printer,
) -> Result<ExitStatus> {
let assets = build_impl(
project_dir,
src.as_deref(),
package.as_ref(),
output_dir.as_deref(),
Expand Down Expand Up @@ -89,6 +91,7 @@ pub(crate) async fn build(

#[allow(clippy::fn_params_excessive_bools)]
async fn build_impl(
project_dir: &Path,
src: Option<&Path>,
package: Option<&PackageName>,
output_dir: Option<&Path>,
Expand Down Expand Up @@ -149,7 +152,7 @@ async fn build_impl(
Source::Directory(Cow::Owned(src))
}
} else {
Source::Directory(Cow::Borrowed(&*CWD))
Source::Directory(Cow::Borrowed(project_dir))
};

// Attempt to discover the workspace; on failure, save the error for later.
Expand Down
11 changes: 6 additions & 5 deletions crates/uv/src/commands/project/add.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ use uv_configuration::{
};
use uv_dispatch::BuildDispatch;
use uv_distribution::DistributionDatabase;
use uv_fs::{Simplified, CWD};
use uv_fs::Simplified;
use uv_git::{GitReference, GIT_STORE};
use uv_normalize::PackageName;
use uv_python::{
Expand Down Expand Up @@ -51,6 +51,7 @@ use crate::settings::{ResolverInstallerSettings, ResolverInstallerSettingsRef};
/// Add one or more packages to the project requirements.
#[allow(clippy::fn_params_excessive_bools)]
pub(crate) async fn add(
project_dir: &Path,
locked: bool,
frozen: bool,
no_sync: bool,
Expand Down Expand Up @@ -131,7 +132,7 @@ pub(crate) async fn add(
let python_request = if let Some(request) = python.as_deref() {
// (1) Explicit request from user
PythonRequest::parse(request)
} else if let Some(request) = PythonVersionFile::discover(&*CWD, false, false)
} else if let Some(request) = PythonVersionFile::discover(project_dir, false, false)
.await?
.and_then(PythonVersionFile::into_version)
{
Expand Down Expand Up @@ -162,7 +163,7 @@ pub(crate) async fn add(
let python_request = if let Some(request) = python.as_deref() {
// (1) Explicit request from user
Some(PythonRequest::parse(request))
} else if let Some(request) = PythonVersionFile::discover(&*CWD, false, false)
} else if let Some(request) = PythonVersionFile::discover(project_dir, false, false)
.await?
.and_then(PythonVersionFile::into_version)
{
Expand Down Expand Up @@ -196,13 +197,13 @@ pub(crate) async fn add(
// Find the project in the workspace.
let project = if let Some(package) = package {
VirtualProject::Project(
Workspace::discover(&CWD, &DiscoveryOptions::default())
Workspace::discover(project_dir, &DiscoveryOptions::default())
.await?
.with_current_project(package.clone())
.with_context(|| format!("Package `{package}` not found in workspace"))?,
)
} else {
VirtualProject::discover(&CWD, &DiscoveryOptions::default()).await?
VirtualProject::discover(project_dir, &DiscoveryOptions::default()).await?
};

// For non-project workspace roots, allow dev dependencies, but nothing else.
Expand Down
10 changes: 5 additions & 5 deletions crates/uv/src/commands/project/export.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,14 @@ use std::env;
use anyhow::{Context, Result};
use itertools::Itertools;
use owo_colors::OwoColorize;
use std::path::PathBuf;
use std::path::{Path, PathBuf};

use uv_cache::Cache;
use uv_client::Connectivity;
use uv_configuration::{
Concurrency, DevMode, DevSpecification, EditableMode, ExportFormat, ExtrasSpecification,
InstallOptions,
};
use uv_fs::CWD;
use uv_normalize::{PackageName, DEV_DEPENDENCIES};
use uv_python::{PythonDownloads, PythonPreference, PythonRequest};
use uv_resolver::RequirementsTxtExport;
Expand All @@ -27,6 +26,7 @@ use crate::settings::ResolverSettings;
/// Export the project's `uv.lock` in an alternate format.
#[allow(clippy::fn_params_excessive_bools)]
pub(crate) async fn export(
project_dir: &Path,
format: ExportFormat,
package: Option<PackageName>,
hashes: bool,
Expand All @@ -51,22 +51,22 @@ pub(crate) async fn export(
// Identify the project.
let project = if let Some(package) = package {
VirtualProject::Project(
Workspace::discover(&CWD, &DiscoveryOptions::default())
Workspace::discover(project_dir, &DiscoveryOptions::default())
.await?
.with_current_project(package.clone())
.with_context(|| format!("Package `{package}` not found in workspace"))?,
)
} else if frozen {
VirtualProject::discover(
&CWD,
project_dir,
&DiscoveryOptions {
members: MemberDiscovery::None,
..DiscoveryOptions::default()
},
)
.await?
} else {
VirtualProject::discover(&CWD, &DiscoveryOptions::default()).await?
VirtualProject::discover(project_dir, &DiscoveryOptions::default()).await?
};

let VirtualProject::Project(project) = project else {
Expand Down
5 changes: 3 additions & 2 deletions crates/uv/src/commands/project/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use pep508_rs::PackageName;
use tracing::{debug, warn};
use uv_cache::Cache;
use uv_client::{BaseClientBuilder, Connectivity};
use uv_fs::{Simplified, CWD};
use uv_fs::Simplified;
use uv_python::{
EnvironmentPreference, PythonDownloads, PythonInstallation, PythonPreference, PythonRequest,
PythonVersionFile, VersionRequest,
Expand All @@ -25,6 +25,7 @@ use crate::printer::Printer;
/// Add one or more packages to the project requirements.
#[allow(clippy::single_match_else, clippy::fn_params_excessive_bools)]
pub(crate) async fn init(
project_dir: &Path,
explicit_path: Option<String>,
name: Option<PackageName>,
package: bool,
Expand All @@ -42,7 +43,7 @@ pub(crate) async fn init(
) -> Result<ExitStatus> {
// Default to the current directory if a path was not provided.
let path = match explicit_path {
None => CWD.to_path_buf(),
None => project_dir.to_path_buf(),
Some(ref path) => std::path::absolute(path)?,
};

Expand Down
10 changes: 5 additions & 5 deletions crates/uv/src/commands/project/lock.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
#![allow(clippy::single_match_else)]

use std::collections::BTreeSet;
use std::fmt::Write;

use anstream::eprint;
use owo_colors::OwoColorize;
use rustc_hash::{FxBuildHasher, FxHashMap};
use std::collections::BTreeSet;
use std::fmt::Write;
use std::path::Path;
use tracing::debug;

use distribution_types::{
Expand All @@ -22,7 +22,6 @@ use uv_configuration::{
};
use uv_dispatch::BuildDispatch;
use uv_distribution::DistributionDatabase;
use uv_fs::CWD;
use uv_git::ResolvedRepositoryReference;
use uv_normalize::{PackageName, DEV_DEPENDENCIES};
use uv_python::{Interpreter, PythonDownloads, PythonEnvironment, PythonPreference, PythonRequest};
Expand Down Expand Up @@ -68,6 +67,7 @@ impl LockResult {

/// Resolve the project requirements into a lockfile.
pub(crate) async fn lock(
project_dir: &Path,
locked: bool,
frozen: bool,
python: Option<String>,
Expand All @@ -81,7 +81,7 @@ pub(crate) async fn lock(
printer: Printer,
) -> anyhow::Result<ExitStatus> {
// Find the project requirements.
let workspace = Workspace::discover(&CWD, &DiscoveryOptions::default()).await?;
let workspace = Workspace::discover(project_dir, &DiscoveryOptions::default()).await?;

// Find an interpreter for the project
let interpreter = FoundInterpreter::discover(
Expand Down
11 changes: 6 additions & 5 deletions crates/uv/src/commands/project/remove.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
use std::fmt::Write;

use anyhow::{Context, Result};
use std::fmt::Write;
use std::path::Path;

use owo_colors::OwoColorize;
use pep508_rs::PackageName;
use uv_cache::Cache;
use uv_client::Connectivity;
use uv_configuration::{Concurrency, DevMode, EditableMode, ExtrasSpecification, InstallOptions};
use uv_fs::{Simplified, CWD};
use uv_fs::Simplified;
use uv_python::{PythonDownloads, PythonPreference, PythonRequest};
use uv_scripts::Pep723Script;
use uv_warnings::{warn_user, warn_user_once};
Expand All @@ -24,6 +24,7 @@ use crate::settings::ResolverInstallerSettings;
/// Remove one or more packages from the project requirements.
#[allow(clippy::fn_params_excessive_bools)]
pub(crate) async fn remove(
project_dir: &Path,
locked: bool,
frozen: bool,
no_sync: bool,
Expand Down Expand Up @@ -68,13 +69,13 @@ pub(crate) async fn remove(
// Find the project in the workspace.
let project = if let Some(package) = package {
VirtualProject::Project(
Workspace::discover(&CWD, &DiscoveryOptions::default())
Workspace::discover(project_dir, &DiscoveryOptions::default())
.await?
.with_current_project(package.clone())
.with_context(|| format!("Package `{package}` not found in workspace"))?,
)
} else {
VirtualProject::discover(&CWD, &DiscoveryOptions::default()).await?
VirtualProject::discover(project_dir, &DiscoveryOptions::default()).await?
};

Target::Project(project)
Expand Down
11 changes: 6 additions & 5 deletions crates/uv/src/commands/project/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ use uv_configuration::{
Concurrency, DevMode, EditableMode, ExtrasSpecification, InstallOptions, SourceStrategy,
};
use uv_distribution::LoweredRequirement;
use uv_fs::{PythonExt, Simplified, CWD};
use uv_fs::{PythonExt, Simplified};
use uv_installer::{SatisfiesResult, SitePackages};
use uv_normalize::PackageName;
use uv_python::{
Expand Down Expand Up @@ -47,6 +47,7 @@ use crate::settings::ResolverInstallerSettings;
/// Run a command.
#[allow(clippy::fn_params_excessive_bools)]
pub(crate) async fn run(
project_dir: &Path,
script: Option<Pep723Script>,
command: RunCommand,
requirements: Vec<RequirementsSource>,
Expand Down Expand Up @@ -113,7 +114,7 @@ pub(crate) async fn run(
let source = PythonRequestSource::UserRequest;
let request = Some(PythonRequest::parse(request));
(source, request)
} else if let Some(file) = PythonVersionFile::discover(&*CWD, false, false).await? {
} else if let Some(file) = PythonVersionFile::discover(&project_dir, false, false).await? {
// (2) Request from `.python-version`
let source = PythonRequestSource::DotPythonVersion(file.file_name().to_string());
let request = file.into_version();
Expand Down Expand Up @@ -309,13 +310,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(&CWD, &DiscoveryOptions::default())
Workspace::discover(project_dir, &DiscoveryOptions::default())
.await?
.with_current_project(package.clone())
.with_context(|| format!("Package `{package}` not found in workspace"))?,
))
} else {
match VirtualProject::discover(&CWD, &DiscoveryOptions::default()).await {
match VirtualProject::discover(project_dir, &DiscoveryOptions::default()).await {
Ok(project) => {
if no_project {
debug!("Ignoring discovered project due to `--no-project`");
Expand Down Expand Up @@ -537,7 +538,7 @@ pub(crate) async fn run(
Some(PythonRequest::parse(request))
// (2) Request from `.python-version`
} else {
PythonVersionFile::discover(&*CWD, no_config, false)
PythonVersionFile::discover(&project_dir, no_config, false)
.await?
.and_then(PythonVersionFile::into_version)
};
Expand Down
Loading

0 comments on commit 9e805e6

Please sign in to comment.