Skip to content

Commit

Permalink
Avoid dynamic
Browse files Browse the repository at this point in the history
  • Loading branch information
charliermarsh committed Mar 25, 2024
1 parent 71428f7 commit 702a950
Show file tree
Hide file tree
Showing 5 changed files with 97 additions and 11 deletions.
56 changes: 45 additions & 11 deletions crates/uv-build/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,9 @@ pub struct Project {
pub version: Option<Version>,
/// The Python version requirements of the project
pub requires_python: Option<VersionSpecifiers>,
/// Specifies which fields listed by PEP 621 were intentionally unspecified so another tool
/// can/will provide such metadata dynamically.
pub dynamic: Option<Vec<String>>,
}

/// The `[build-system]` section of a pyproject.toml as specified in PEP 517.
Expand Down Expand Up @@ -347,6 +350,8 @@ pub struct SourceBuild {
config_settings: ConfigSettings,
/// If performing a PEP 517 build, the backend to use.
pep517_backend: Option<Pep517Backend>,
/// The PEP 621 project metadata, if any.
project: Option<Project>,
/// The virtual environment in which to build the source distribution.
venv: PythonEnvironment,
/// Populated if `prepare_metadata_for_build_wheel` was called.
Expand Down Expand Up @@ -399,8 +404,9 @@ impl SourceBuild {
let default_backend: Pep517Backend = DEFAULT_BACKEND.clone();

// Check if we have a PEP 517 build backend.
let pep517_backend = Self::get_pep517_backend(setup_py, &source_tree, &default_backend)
.map_err(|err| *err)?;
let (pep517_backend, project) =
Self::get_pep517_backend(&source_tree, setup_py, &default_backend)
.map_err(|err| *err)?;

// Create a virtual environment, or install into the shared environment if requested.
let venv = match build_isolation {
Expand Down Expand Up @@ -487,6 +493,7 @@ impl SourceBuild {
temp_dir,
source_tree,
pep517_backend,
project,
venv,
build_kind,
config_settings,
Expand Down Expand Up @@ -542,17 +549,18 @@ impl SourceBuild {
})
}

/// Extract the PEP 517 backend from the `pyproject.toml` or `setup.py` file.
fn get_pep517_backend(
setup_py: SetupPyStrategy,
source_tree: &Path,
setup_py: SetupPyStrategy,
default_backend: &Pep517Backend,
) -> Result<Option<Pep517Backend>, Box<Error>> {
) -> Result<(Option<Pep517Backend>, Option<Project>), Box<Error>> {
match fs::read_to_string(source_tree.join("pyproject.toml")) {
Ok(toml) => {
let pyproject_toml: PyProjectToml =
toml::from_str(&toml).map_err(Error::InvalidPyprojectToml)?;
if let Some(build_system) = pyproject_toml.build_system {
Ok(Some(Pep517Backend {
let backend = if let Some(build_system) = pyproject_toml.build_system {
Pep517Backend {
// If `build-backend` is missing, inject the legacy setuptools backend, but
// retain the `requires`, to match `pip` and `build`. Note that while PEP 517
// says that in this case we "should revert to the legacy behaviour of running
Expand All @@ -565,12 +573,13 @@ impl SourceBuild {
.unwrap_or_else(|| "setuptools.build_meta:__legacy__".to_string()),
backend_path: build_system.backend_path,
requirements: build_system.requires,
}))
}
} else {
// If a `pyproject.toml` is present, but `[build-system]` is missing, proceed with
// a PEP 517 build using the default backend, to match `pip` and `build`.
Ok(Some(default_backend.clone()))
}
default_backend.clone()
};
Ok((Some(backend), pyproject_toml.project))
}
Err(err) if err.kind() == io::ErrorKind::NotFound => {
// We require either a `pyproject.toml` or a `setup.py` file at the top level.
Expand All @@ -587,8 +596,8 @@ impl SourceBuild {
// 517 builds the default in the future.
// See: https://github.com/pypa/pip/issues/9175.
match setup_py {
SetupPyStrategy::Pep517 => Ok(Some(default_backend.clone())),
SetupPyStrategy::Setuptools => Ok(None),
SetupPyStrategy::Pep517 => Ok((Some(default_backend.clone()), None)),
SetupPyStrategy::Setuptools => Ok((None, None)),
}
}
Err(err) => Err(Box::new(err.into())),
Expand All @@ -607,6 +616,31 @@ impl SourceBuild {
return Ok(Some(metadata_dir.clone()));
}

// Hatch allows for highly dynamic customization of metadata via hooks. In such cases, Hatch
// can't upload the PEP 517 contract, in that the metadata Hatch would return by
// `prepare_metadata_for_build_wheel` isn't guaranteed to match that of the built wheel.
//
// Hatch disables `prepare_metadata_for_build_wheel` entirely for pip. We'll instead disable
// it on our end when metadata is defined as "dynamic" in the pyproject.toml, which should
// allow us to leverage the hook in _most_ cases while still avoiding incorrect metadata for
// the remaining cases.
//
// See: https://github.com/astral-sh/uv/issues/2130
if pep517_backend.backend == "hatchling.build" {
if self
.project
.as_ref()
.and_then(|project| project.dynamic.as_ref())
.is_some_and(|dynamic| {
dynamic
.iter()
.any(|field| field == "dependencies" || field == "optional-dependencies")
})
{
return Ok(None);
}
}

let metadata_directory = self.temp_dir.path().join("metadata_directory");
fs::create_dir(&metadata_directory)?;

Expand Down
36 changes: 36 additions & 0 deletions crates/uv/tests/pip_compile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6046,3 +6046,39 @@ fn unnamed_https_requirement() -> Result<()> {

Ok(())
}

/// Detect the package name from metadata sources from local directories.
#[test]
fn dynamic_dependencies() -> Result<()> {
let context = TestContext::new("3.12");
let requirements_in = context.temp_dir.child("requirements.in");
requirements_in.write_str("hatch-dynamic @ ../../scripts/packages/hatch_dynamic")?;

let filter_path = regex::escape(&requirements_in.user_display().to_string());
let filters: Vec<_> = [(filter_path.as_str(), "requirements.in")]
.into_iter()
.chain(INSTA_FILTERS.to_vec())
.collect();

uv_snapshot!(filters, context.compile()
.arg(requirements_in.path())
.current_dir(current_dir()?), @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 2024-03-25T00:00:00Z requirements.in
anyio==4.3.0
# via hatch-dynamic
hatch-dynamic @ ../../scripts/packages/hatch_dynamic
idna==3.6
# via anyio
sniffio==1.3.1
# via anyio
----- stderr -----
Resolved 4 packages in [TIME]
"###);

Ok(())
}
6 changes: 6 additions & 0 deletions scripts/packages/hatch_dynamic/hatch_build.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from hatchling.builders.hooks.plugin.interface import BuildHookInterface


class LiteraryBuildHook(BuildHookInterface):
def initialize(self, version, build_data):
build_data['dependencies'].append('anyio')
Empty file.
10 changes: 10 additions & 0 deletions scripts/packages/hatch_dynamic/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "hatch-dynamic"
version = "1.0.0"
dynamic = ["dependencies"]

[tool.hatch.build.targets.wheel.hooks.custom]

0 comments on commit 702a950

Please sign in to comment.