diff --git a/crates/uv-requirements/src/sources.rs b/crates/uv-requirements/src/sources.rs index 94ce2075d191..0ddc3ccbed0c 100644 --- a/crates/uv-requirements/src/sources.rs +++ b/crates/uv-requirements/src/sources.rs @@ -22,6 +22,8 @@ pub enum RequirementsSource { SetupPy(PathBuf), /// Dependencies were provided via a `setup.cfg` file (e.g., `pip-compile setup.cfg`). SetupCfg(PathBuf), + /// Dependencies were provided via a path to a source tree (e.g., `pip install .`). + SourceTree(PathBuf), } impl RequirementsSource { @@ -122,6 +124,12 @@ impl RequirementsSource { Self::Package(name) } + /// Parse a [`RequirementsSource`] from a user-provided string, assumed to be a path to a source + /// tree. + pub fn from_source_tree(path: PathBuf) -> Self { + Self::SourceTree(path) + } + /// Returns `true` if the source allows extras to be specified. pub fn allows_extras(&self) -> bool { matches!( @@ -139,7 +147,8 @@ impl std::fmt::Display for RequirementsSource { Self::RequirementsTxt(path) | Self::PyprojectToml(path) | Self::SetupPy(path) - | Self::SetupCfg(path) => { + | Self::SetupCfg(path) + | Self::SourceTree(path) => { write!(f, "{}", path.simplified_display()) } } diff --git a/crates/uv-requirements/src/specification.rs b/crates/uv-requirements/src/specification.rs index 712fcf968e5f..30290a049f00 100644 --- a/crates/uv-requirements/src/specification.rs +++ b/crates/uv-requirements/src/specification.rs @@ -11,6 +11,7 @@ use distribution_types::{ FlatIndexLocation, IndexUrl, Requirement, RequirementSource, UnresolvedRequirement, UnresolvedRequirementSpecification, }; +use pep508_rs::{UnnamedRequirement, VerbatimUrl}; use requirements_txt::{ EditableRequirement, FindLink, RequirementEntry, RequirementsTxt, RequirementsTxtRequirement, }; @@ -163,6 +164,29 @@ impl RequirementsSpecification { no_binary: NoBinary::default(), no_build: NoBuild::default(), }, + RequirementsSource::SourceTree(path) => Self { + project: None, + requirements: vec![UnresolvedRequirementSpecification { + requirement: UnresolvedRequirement::Unnamed(UnnamedRequirement { + url: VerbatimUrl::from_path(path), + extras: vec![], + marker: None, + origin: None, + }), + hashes: vec![], + }], + constraints: vec![], + overrides: vec![], + editables: vec![], + source_trees: vec![], + extras: FxHashSet::default(), + index_url: None, + extra_index_urls: vec![], + no_index: false, + find_links: vec![], + no_binary: NoBinary::default(), + no_build: NoBuild::default(), + }, }) } diff --git a/crates/uv/src/commands/project/discovery.rs b/crates/uv/src/commands/project/discovery.rs new file mode 100644 index 000000000000..edc683443a00 --- /dev/null +++ b/crates/uv/src/commands/project/discovery.rs @@ -0,0 +1,40 @@ +use std::path::{Path, PathBuf}; + +use tracing::debug; +use uv_fs::Simplified; + +use uv_requirements::RequirementsSource; + +#[derive(Debug, Clone)] +pub(crate) struct Project { + /// The path to the `pyproject.toml` file. + path: PathBuf, +} + +impl Project { + /// Find the current project. + pub(crate) fn find(path: impl AsRef) -> Option { + for ancestor in path.as_ref().ancestors() { + let pyproject_path = ancestor.join("pyproject.toml"); + if pyproject_path.exists() { + debug!( + "Loading requirements from: {}", + pyproject_path.user_display() + ); + return Some(Self { + path: pyproject_path, + }); + } + } + + None + } + + /// Return the requirements for the project. + pub(crate) fn requirements(&self) -> Vec { + vec![ + RequirementsSource::from_requirements_file(self.path.clone()), + RequirementsSource::from_source_tree(self.path.parent().unwrap().to_path_buf()), + ] + } +} diff --git a/crates/uv/src/commands/project/lock.rs b/crates/uv/src/commands/project/lock.rs index 9ecf44c19f65..dc6e6511b5ce 100644 --- a/crates/uv/src/commands/project/lock.rs +++ b/crates/uv/src/commands/project/lock.rs @@ -13,6 +13,7 @@ use uv_resolver::{FlatIndex, InMemoryIndex, OptionsBuilder}; use uv_types::{BuildIsolation, HashStrategy, InFlight}; use uv_warnings::warn_user; +use crate::commands::project::discovery::Project; use crate::commands::project::Error; use crate::commands::{project, ExitStatus}; use crate::printer::Printer; @@ -32,9 +33,9 @@ pub(crate) async fn lock( let venv = PythonEnvironment::from_virtualenv(cache)?; // Find the project requirements. - let Some(requirements) = project::find_project()? else { + let Some(project) = Project::find(std::env::current_dir()?) else { return Err(anyhow::anyhow!( - "Unable to find `pyproject.toml` for project project." + "Unable to find `pyproject.toml` for project." )); }; @@ -45,7 +46,7 @@ pub(crate) async fn lock( // TODO(zanieb): Consider allowing constraints and extras // TODO(zanieb): Allow specifying extras somehow let spec = RequirementsSpecification::from_sources( - &requirements, + &project.requirements(), &[], &[], &ExtrasSpecification::None, diff --git a/crates/uv/src/commands/project/mod.rs b/crates/uv/src/commands/project/mod.rs index 3e0d3995bcb3..6936a98c5612 100644 --- a/crates/uv/src/commands/project/mod.rs +++ b/crates/uv/src/commands/project/mod.rs @@ -3,7 +3,6 @@ use std::fmt::Write; use anyhow::{Context, Result}; use itertools::Itertools; use owo_colors::OwoColorize; -use tracing::debug; use distribution_types::{IndexLocations, InstalledMetadata, LocalDist, Name, Resolution}; use install_wheel_rs::linker::LinkMode; @@ -14,12 +13,11 @@ use uv_cache::Cache; use uv_client::RegistryClient; use uv_configuration::{Constraints, NoBinary, Overrides, Reinstall}; use uv_dispatch::BuildDispatch; -use uv_fs::Simplified; use uv_installer::{Downloader, Plan, Planner, SitePackages}; use uv_interpreter::{Interpreter, PythonEnvironment}; use uv_requirements::{ - ExtrasSpecification, LookaheadResolver, NamedRequirementsResolver, RequirementsSource, - RequirementsSpecification, SourceTreeResolver, + ExtrasSpecification, LookaheadResolver, NamedRequirementsResolver, RequirementsSpecification, + SourceTreeResolver, }; use uv_resolver::{ Exclusions, FlatIndex, InMemoryIndex, Manifest, Options, PythonRequirement, ResolutionGraph, @@ -31,6 +29,7 @@ use crate::commands::reporters::{DownloadReporter, InstallReporter, ResolverRepo use crate::commands::{elapsed, ChangeEvent, ChangeEventKind}; use crate::printer::Printer; +mod discovery; pub(crate) mod lock; pub(crate) mod run; pub(crate) mod sync; @@ -62,25 +61,6 @@ pub(crate) enum Error { Anyhow(#[from] anyhow::Error), } -/// Find the requirements for the current workspace. -pub(crate) fn find_project() -> Result>> { - // TODO(zanieb): Add/use workspace logic to load requirements for a workspace - // We cannot use `Workspace::find` yet because it depends on a `[tool.uv]` section - let pyproject_path = std::env::current_dir()?.join("pyproject.toml"); - if pyproject_path.exists() { - debug!( - "Loading requirements from {}", - pyproject_path.user_display() - ); - return Ok(Some(vec![ - RequirementsSource::from_requirements_file(pyproject_path), - RequirementsSource::from_package(".".to_string()), - ])); - } - - Ok(None) -} - /// Resolve a set of requirements, similar to running `pip compile`. #[allow(clippy::too_many_arguments)] pub(crate) async fn resolve( diff --git a/crates/uv/src/commands/project/run.rs b/crates/uv/src/commands/project/run.rs index 0d10d13b03f3..8fa2d57939a4 100644 --- a/crates/uv/src/commands/project/run.rs +++ b/crates/uv/src/commands/project/run.rs @@ -1,4 +1,3 @@ -use std::env; use std::ffi::OsString; use std::path::PathBuf; @@ -21,6 +20,7 @@ use uv_resolver::{FlatIndex, InMemoryIndex, OptionsBuilder}; use uv_types::{BuildIsolation, HashStrategy, InFlight}; use uv_warnings::warn_user; +use crate::commands::project::discovery::Project; use crate::commands::{project, ExitStatus}; use crate::printer::Printer; @@ -62,16 +62,16 @@ pub(crate) async fn run( } else { debug!("Syncing project environment."); - let Some(project_requirements) = project::find_project()? else { + let Some(project) = Project::find(std::env::current_dir()?) else { return Err(anyhow::anyhow!( - "Unable to find `pyproject.toml` for project project." + "Unable to find `pyproject.toml` for project." )); }; let venv = PythonEnvironment::from_virtualenv(cache)?; // Install the project requirements. - Some(update_environment(venv, &project_requirements, preview, cache, printer).await?) + Some(update_environment(venv, &project.requirements(), preview, cache, printer).await?) }; // If necessary, create an environment for the ephemeral requirements. @@ -97,7 +97,7 @@ pub(crate) async fn run( // Create a virtual environment // TODO(zanieb): Move this path derivation elsewhere - let uv_state_path = env::current_dir()?.join(".uv"); + let uv_state_path = std::env::current_dir()?.join(".uv"); fs_err::create_dir_all(&uv_state_path)?; tmpdir = tempdir_in(uv_state_path)?; let venv = uv_virtualenv::create_venv( @@ -117,7 +117,7 @@ pub(crate) async fn run( process.args(&args); // Construct the `PATH` environment variable. - let new_path = env::join_paths( + let new_path = std::env::join_paths( ephemeral_env .as_ref() .map(PythonEnvironment::scripts) @@ -130,16 +130,16 @@ pub(crate) async fn run( ) .map(PathBuf::from) .chain( - env::var_os("PATH") + std::env::var_os("PATH") .as_ref() .iter() - .flat_map(env::split_paths), + .flat_map(std::env::split_paths), ), )?; process.env("PATH", new_path); // Construct the `PYTHONPATH` environment variable. - let new_python_path = env::join_paths( + let new_python_path = std::env::join_paths( ephemeral_env .as_ref() .map(PythonEnvironment::site_packages) @@ -154,10 +154,10 @@ pub(crate) async fn run( ) .map(PathBuf::from) .chain( - env::var_os("PYTHONPATH") + std::env::var_os("PYTHONPATH") .as_ref() .iter() - .flat_map(env::split_paths), + .flat_map(std::env::split_paths), ), )?; process.env("PYTHONPATH", new_python_path);