Allow overriding module name for uv build backend (#11884)

Thank you for uv, it has game-changer capabilities in the field of
Python package and environment maangement!

## Summary

This is a small PR adding the option `module-name`
(`tool.uv.build-backend.module-name`) to the uv build backend (
https://github.com/astral-sh/uv/issues/8779 ).

Currently, the uv build backend will assume that the module name matches
the (dash to underdash-transformed) package name. In some packaging
scenarios this is not the case, and currently there exists no
possibility to override it, which this PR addresses.

From the main issue ( https://github.com/astral-sh/uv/issues/8779 ) I
could not tell if there is any extensive roadmap or plans how to
implement more complex scenarios, hence this PR as a suggestion for a
small feature with a big impact for certain scenarios.

I am new to Rust, I hope the borrow/reference usage is correct.

## Test Plan

So far I tested this at an example, if desired I can look into extending
the tests.

Fixes #11428

---------

Co-authored-by: konstin <konstin@mailbox.org>
This commit is contained in:
Christian Sachs 2025-03-07 15:20:00 +01:00 committed by GitHub
parent 1ab1945dd9
commit c57dd1a4a8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 303 additions and 13 deletions

View file

@ -15,6 +15,7 @@ use thiserror::Error;
use tracing::debug;
use uv_fs::Simplified;
use uv_globfilter::PortableGlobError;
use uv_pypi_types::IdentifierParseError;
#[derive(Debug, Error)]
pub enum Error {
@ -24,6 +25,8 @@ pub enum Error {
Toml(#[from] toml::de::Error),
#[error("Invalid pyproject.toml")]
Validation(#[from] ValidationError),
#[error(transparent)]
Identifier(#[from] IdentifierParseError),
#[error("Unsupported glob expression in: `{field}`")]
PortableGlob {
field: String,

View file

@ -17,7 +17,7 @@ use uv_pep440::{Version, VersionSpecifiers};
use uv_pep508::{
ExtraOperator, MarkerExpression, MarkerTree, MarkerValueExtra, Requirement, VersionOrUrl,
};
use uv_pypi_types::{Metadata23, VerbatimParsedUrl};
use uv_pypi_types::{Identifier, Metadata23, VerbatimParsedUrl};
use crate::serde_verbatim::SerdeVerbatim;
use crate::Error;
@ -803,7 +803,7 @@ pub(crate) struct ToolUv {
/// When building the source distribution, the following files and directories are included:
/// * `pyproject.toml`
/// * The module under `tool.uv.build-backend.module-root`, by default
/// `src/<project_name_with_underscores>/**`.
/// `src/<module-name or project_name_with_underscores>/**`.
/// * `project.license-files` and `project.readme`.
/// * All directories under `tool.uv.build-backend.data`.
/// * All patterns from `tool.uv.build-backend.source-include`.
@ -812,7 +812,7 @@ pub(crate) struct ToolUv {
///
/// When building the wheel, the following files and directories are included:
/// * The module under `tool.uv.build-backend.module-root`, by default
/// `src/<project_name_with_underscores>/**`.
/// `src/<module-name or project_name_with_underscores>/**`.
/// * `project.license-files` and `project.readme`, as part of the project metadata.
/// * Each directory under `tool.uv.build-backend.data`, as data directories.
///
@ -846,6 +846,15 @@ pub(crate) struct BuildBackendSettings {
/// using the flat layout over the src layout.
pub(crate) module_root: PathBuf,
/// The name of the module directory inside `module-root`.
///
/// The default module name is the package name with dots and dashes replaced by underscores.
///
/// Note that using this option runs the risk of creating two packages with different names but
/// the same module names. Installing such packages together leads to unspecified behavior,
/// often with corrupted files or directory trees.
pub(crate) module_name: Option<Identifier>,
/// Glob expressions which files and directories to additionally include in the source
/// distribution.
///
@ -877,6 +886,7 @@ impl Default for BuildBackendSettings {
fn default() -> Self {
Self {
module_root: PathBuf::from("src"),
module_name: None,
source_include: Vec::new(),
default_excludes: true,
source_exclude: Vec::new(),

View file

@ -8,11 +8,13 @@ use globset::{Glob, GlobSet};
use std::io;
use std::io::{BufReader, Cursor};
use std::path::{Path, PathBuf};
use std::str::FromStr;
use tar::{EntryType, Header};
use tracing::{debug, trace};
use uv_distribution_filename::{SourceDistExtension, SourceDistFilename};
use uv_fs::Simplified;
use uv_globfilter::{parse_portable_glob, GlobDirFilter};
use uv_pypi_types::Identifier;
use uv_warnings::warn_user_once;
use walkdir::WalkDir;
@ -65,11 +67,21 @@ fn source_dist_matcher(
let mut includes: Vec<String> = settings.source_include;
// pyproject.toml is always included.
includes.push(globset::escape("pyproject.toml"));
let module_name = if let Some(module_name) = settings.module_name {
module_name
} else {
// Should never error, the rules for package names (in dist-info formatting) are stricter
// than those for identifiers
Identifier::from_str(pyproject_toml.name().as_dist_info_name().as_ref())?
};
debug!("Module name: `{:?}`", module_name);
// The wheel must not include any files included by the source distribution (at least until we
// have files generated in the source dist -> wheel build step).
let import_path = &settings
.module_root
.join(pyproject_toml.name().as_dist_info_name().as_ref())
.join(module_name.as_ref())
.portable_display()
.to_string();
includes.push(format!("{}/**", globset::escape(import_path)));

View file

@ -1,11 +1,11 @@
use std::io::{BufReader, Read, Write};
use std::path::{Path, PathBuf};
use std::{io, mem};
use fs_err::File;
use globset::{GlobSet, GlobSetBuilder};
use itertools::Itertools;
use sha2::{Digest, Sha256};
use std::io::{BufReader, Read, Write};
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::{io, mem};
use tracing::{debug, trace};
use walkdir::WalkDir;
use zip::{CompressionMethod, ZipWriter};
@ -14,6 +14,7 @@ use uv_distribution_filename::WheelFilename;
use uv_fs::Simplified;
use uv_globfilter::{parse_portable_glob, GlobDirFilter};
use uv_platform_tags::{AbiTag, LanguageTag, PlatformTag};
use uv_pypi_types::Identifier;
use uv_warnings::warn_user_once;
use crate::metadata::{BuildBackendSettings, DEFAULT_EXCLUDES};
@ -128,7 +129,17 @@ fn write_wheel(
return Err(Error::AbsoluteModuleRoot(settings.module_root.clone()));
}
let strip_root = source_tree.join(settings.module_root);
let module_root = strip_root.join(pyproject_toml.name().as_dist_info_name().as_ref());
let module_name = if let Some(module_name) = settings.module_name {
module_name
} else {
// Should never error, the rules for package names (in dist-info formatting) are stricter
// than those for identifiers
Identifier::from_str(pyproject_toml.name().as_dist_info_name().as_ref())?
};
debug!("Module name: `{:?}`", module_name);
let module_root = strip_root.join(module_name.as_ref());
if !module_root.join("__init__.py").is_file() {
return Err(Error::MissingModule(module_root));
}