From db5898bd67307fd505d40da0b10a501acf98bba5 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sat, 16 Mar 2024 12:06:42 -0700 Subject: [PATCH] Add support for Hatch's `{root:uri}` paths in editable installs (#2492) ## Summary If a package uses Hatch's `root.uri` feature, we currently error: ```toml dependencies = [ "black @ {root:uri}/../black_editable" ] ``` Even though we're using PEP 517 hooks to get the metadata, which _should_ support this. The problem is that we load the full `PyProjectToml`, which means we parse the requirements, which means we reject what looks like a relative URL in dependencies. Instead, we should only enforce a limited subset of `pyproject.toml` (arguably none). Closes https://github.com/astral-sh/uv/issues/2475. --- Cargo.lock | 1 + crates/uv-build/Cargo.toml | 1 + crates/uv-build/src/lib.rs | 16 +++++++-- crates/uv/tests/pip_compile.rs | 36 +++++++++++++++++++ .../editable-installs/root_editable/README.md | 0 .../root_editable/pyproject.toml | 22 ++++++++++++ .../root_editable/root_editable/__init__.py | 2 ++ 7 files changed, 76 insertions(+), 2 deletions(-) create mode 100644 scripts/editable-installs/root_editable/README.md create mode 100644 scripts/editable-installs/root_editable/pyproject.toml create mode 100644 scripts/editable-installs/root_editable/root_editable/__init__.py diff --git a/Cargo.lock b/Cargo.lock index 6c4862001..d164edd22 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4301,6 +4301,7 @@ dependencies = [ "insta", "itertools 0.12.1", "once_cell", + "pep440_rs", "pep508_rs", "pypi-types", "pyproject-toml", diff --git a/crates/uv-build/Cargo.toml b/crates/uv-build/Cargo.toml index 9ee5b7e27..796f97c55 100644 --- a/crates/uv-build/Cargo.toml +++ b/crates/uv-build/Cargo.toml @@ -15,6 +15,7 @@ workspace = true [dependencies] distribution-types = { path = "../distribution-types" } +pep440_rs = { path = "../pep440-rs" } pep508_rs = { path = "../pep508-rs" } pypi-types = { path = "../pypi-types" } uv-fs = { path = "../uv-fs" } diff --git a/crates/uv-build/src/lib.rs b/crates/uv-build/src/lib.rs index e1696a9db..969938714 100644 --- a/crates/uv-build/src/lib.rs +++ b/crates/uv-build/src/lib.rs @@ -15,7 +15,6 @@ use fs_err as fs; use indoc::formatdoc; use itertools::Itertools; use once_cell::sync::Lazy; -use pyproject_toml::Project; use regex::Regex; use rustc_hash::FxHashMap; use serde::de::{value, SeqAccess, Visitor}; @@ -27,6 +26,7 @@ use tokio::sync::Mutex; use tracing::{debug, info_span, instrument, Instrument}; use distribution_types::Resolution; +use pep440_rs::{Version, VersionSpecifiers}; use pep508_rs::Requirement; use uv_fs::Simplified; use uv_interpreter::{Interpreter, PythonEnvironment}; @@ -193,7 +193,7 @@ impl Error { } } -/// A pyproject.toml as specified in PEP 517 +/// A pyproject.toml as specified in PEP 517. #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] #[serde(rename_all = "kebab-case")] pub struct PyProjectToml { @@ -203,6 +203,18 @@ pub struct PyProjectToml { pub project: Option, } +/// The `[project]` section of a pyproject.toml as specified in PEP 621. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +#[serde(rename_all = "kebab-case")] +pub struct Project { + /// The name of the project + pub name: String, + /// The version of the project as supported by PEP 440 + pub version: Option, + /// The Python version requirements of the project + pub requires_python: Option, +} + /// The `[build-system]` section of a pyproject.toml as specified in PEP 517. #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] #[serde(rename_all = "kebab-case")] diff --git a/crates/uv/tests/pip_compile.rs b/crates/uv/tests/pip_compile.rs index 403a3e0ee..28b40f219 100644 --- a/crates/uv/tests/pip_compile.rs +++ b/crates/uv/tests/pip_compile.rs @@ -5150,3 +5150,39 @@ requires-python = "<=3.8" Ok(()) } + +/// Build an editable package with Hatchling's {root:uri} feature. +#[test] +fn compile_root_uri() -> Result<()> { + let context = TestContext::new("3.12"); + + let requirements_in = context.temp_dir.child("requirements.in"); + requirements_in.write_str("-e ${ROOT_PATH}")?; + + // In addition to the standard filters, remove the temporary directory from the snapshot. + let filters: Vec<_> = [(r"file://.*/", "file://[TEMP_DIR]/")] + .into_iter() + .chain(INSTA_FILTERS.to_vec()) + .collect(); + + let root_path = current_dir()?.join("../../scripts/editable-installs/root_editable"); + uv_snapshot!(filters, context.compile() + .arg("requirements.in") + .env("ROOT_PATH", root_path.as_os_str()), @r###" + success: true + exit_code: 0 + ----- stdout ----- + # This file was autogenerated by uv via the following command: + # uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2023-11-18T12:00:00Z requirements.in + -e ${ROOT_PATH} + black @ file://[TEMP_DIR]/black_editable + # via root-editable + + ----- stderr ----- + Built 1 editable in [TIME] + Resolved 2 packages in [TIME] + "### + ); + + Ok(()) +} diff --git a/scripts/editable-installs/root_editable/README.md b/scripts/editable-installs/root_editable/README.md new file mode 100644 index 000000000..e69de29bb diff --git a/scripts/editable-installs/root_editable/pyproject.toml b/scripts/editable-installs/root_editable/pyproject.toml new file mode 100644 index 000000000..ae9a26171 --- /dev/null +++ b/scripts/editable-installs/root_editable/pyproject.toml @@ -0,0 +1,22 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "root-editable" +description = 'A simple editable package with a {root:uri} dependency.' +readme = "README.md" +requires-python = ">=3.7" +license = "MIT" +keywords = [] +authors = [ + { name = "Astral Software Inc.", email = "hey@astral.sh" }, +] +classifiers = [] +dependencies = [ + "black @ {root:uri}/../black_editable" +] +version = "0.1.0" + +[tool.hatch.metadata] +allow-direct-references = true diff --git a/scripts/editable-installs/root_editable/root_editable/__init__.py b/scripts/editable-installs/root_editable/root_editable/__init__.py new file mode 100644 index 000000000..b9bfa6f12 --- /dev/null +++ b/scripts/editable-installs/root_editable/root_editable/__init__.py @@ -0,0 +1,2 @@ +def func(): + pass