diff --git a/crates/uv/src/commands/project/discovery.rs b/crates/uv/src/commands/project/discovery.rs index edc683443..ea1d6bf3a 100644 --- a/crates/uv/src/commands/project/discovery.rs +++ b/crates/uv/src/commands/project/discovery.rs @@ -1,19 +1,38 @@ +use serde::Deserialize; use std::path::{Path, PathBuf}; use tracing::debug; use uv_fs::Simplified; +use uv_normalize::PackageName; use uv_requirements::RequirementsSource; +#[derive(thiserror::Error, Debug)] +pub(crate) enum ProjectError { + #[error(transparent)] + Io(#[from] std::io::Error), + + #[error(transparent)] + Toml(#[from] toml::de::Error), + + #[error("No `project` section found in: {}", _0.user_display())] + MissingProject(PathBuf), + + #[error("No `name` found in `project` section in: {}", _0.user_display())] + MissingName(PathBuf), +} + #[derive(Debug, Clone)] pub(crate) struct Project { + /// The name of the package. + name: PackageName, /// The path to the `pyproject.toml` file. path: PathBuf, } impl Project { /// Find the current project. - pub(crate) fn find(path: impl AsRef) -> Option { + pub(crate) fn find(path: impl AsRef) -> Result, ProjectError> { for ancestor in path.as_ref().ancestors() { let pyproject_path = ancestor.join("pyproject.toml"); if pyproject_path.exists() { @@ -21,13 +40,27 @@ impl Project { "Loading requirements from: {}", pyproject_path.user_display() ); - return Some(Self { + + // Read the `pyproject.toml`. + let contents = fs_err::read_to_string(&pyproject_path)?; + let pyproject_toml: PyProjectToml = toml::from_str(&contents)?; + + // Extract the package name. + let Some(project) = pyproject_toml.project else { + return Err(ProjectError::MissingProject(pyproject_path)); + }; + let Some(name) = project.name else { + return Err(ProjectError::MissingName(pyproject_path)); + }; + + return Ok(Some(Self { + name, path: pyproject_path, - }); + })); } } - None + Ok(None) } /// Return the requirements for the project. @@ -37,4 +70,22 @@ impl Project { RequirementsSource::from_source_tree(self.path.parent().unwrap().to_path_buf()), ] } + + /// Return the [`PackageName`] for the project. + pub(crate) fn name(&self) -> &PackageName { + &self.name + } +} + +/// A pyproject.toml as specified in PEP 517. +#[derive(Deserialize, Debug)] +#[serde(rename_all = "kebab-case")] +struct PyProjectToml { + project: Option, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "kebab-case")] +struct PyProjectProject { + name: Option, } diff --git a/crates/uv/src/commands/project/lock.rs b/crates/uv/src/commands/project/lock.rs index dc6e6511b..a89c155d3 100644 --- a/crates/uv/src/commands/project/lock.rs +++ b/crates/uv/src/commands/project/lock.rs @@ -33,7 +33,7 @@ pub(crate) async fn lock( let venv = PythonEnvironment::from_virtualenv(cache)?; // Find the project requirements. - let Some(project) = Project::find(std::env::current_dir()?) else { + let Some(project) = Project::find(std::env::current_dir()?)? else { return Err(anyhow::anyhow!( "Unable to find `pyproject.toml` for project." )); diff --git a/crates/uv/src/commands/project/run.rs b/crates/uv/src/commands/project/run.rs index 8fa2d5793..c2039b3b9 100644 --- a/crates/uv/src/commands/project/run.rs +++ b/crates/uv/src/commands/project/run.rs @@ -62,7 +62,7 @@ pub(crate) async fn run( } else { debug!("Syncing project environment."); - let Some(project) = Project::find(std::env::current_dir()?) else { + let Some(project) = Project::find(std::env::current_dir()?)? else { return Err(anyhow::anyhow!( "Unable to find `pyproject.toml` for project." )); diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs index 0f2084371..bc27cd544 100644 --- a/crates/uv/src/commands/project/sync.rs +++ b/crates/uv/src/commands/project/sync.rs @@ -8,11 +8,11 @@ use uv_configuration::{ConfigSettings, NoBinary, NoBuild, PreviewMode, SetupPySt use uv_dispatch::BuildDispatch; use uv_installer::SitePackages; use uv_interpreter::PythonEnvironment; -use uv_normalize::PackageName; use uv_resolver::{FlatIndex, InMemoryIndex, Lock}; 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; @@ -32,6 +32,20 @@ pub(crate) async fn sync( let markers = venv.interpreter().markers(); let tags = venv.interpreter().tags()?; + // Find the project requirements. + let Some(project) = Project::find(std::env::current_dir()?)? else { + return Err(anyhow::anyhow!( + "Unable to find `pyproject.toml` for project." + )); + }; + + // Read the lockfile. + let resolution = { + let encoded = fs_err::tokio::read_to_string("uv.lock").await?; + let lock: Lock = toml::from_str(&encoded)?; + lock.to_resolution(markers, tags, project.name()) + }; + // Initialize the registry client. // TODO(zanieb): Support client options e.g. offline, tls, etc. let client = RegistryClientBuilder::new(cache.clone()) @@ -69,15 +83,6 @@ pub(crate) async fn sync( &no_binary, ); - // Read the lockfile. - let resolution = { - // TODO(charlie): Read the project name from disk. - let root = PackageName::new("project".to_string())?; - let encoded = fs_err::tokio::read_to_string("uv.lock").await?; - let lock: Lock = toml::from_str(&encoded)?; - lock.to_resolution(markers, tags, &root) - }; - // Sync the environment. project::install( &resolution,