mirror of
https://github.com/astral-sh/uv.git
synced 2025-10-17 22:07:47 +00:00
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:
parent
1ab1945dd9
commit
c57dd1a4a8
10 changed files with 303 additions and 13 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -5387,6 +5387,7 @@ dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"hashbrown 0.15.2",
|
"hashbrown 0.15.2",
|
||||||
"indexmap",
|
"indexmap",
|
||||||
|
"insta",
|
||||||
"itertools 0.14.0",
|
"itertools 0.14.0",
|
||||||
"jiff",
|
"jiff",
|
||||||
"mailparse",
|
"mailparse",
|
||||||
|
|
|
@ -15,6 +15,7 @@ use thiserror::Error;
|
||||||
use tracing::debug;
|
use tracing::debug;
|
||||||
use uv_fs::Simplified;
|
use uv_fs::Simplified;
|
||||||
use uv_globfilter::PortableGlobError;
|
use uv_globfilter::PortableGlobError;
|
||||||
|
use uv_pypi_types::IdentifierParseError;
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
|
@ -24,6 +25,8 @@ pub enum Error {
|
||||||
Toml(#[from] toml::de::Error),
|
Toml(#[from] toml::de::Error),
|
||||||
#[error("Invalid pyproject.toml")]
|
#[error("Invalid pyproject.toml")]
|
||||||
Validation(#[from] ValidationError),
|
Validation(#[from] ValidationError),
|
||||||
|
#[error(transparent)]
|
||||||
|
Identifier(#[from] IdentifierParseError),
|
||||||
#[error("Unsupported glob expression in: `{field}`")]
|
#[error("Unsupported glob expression in: `{field}`")]
|
||||||
PortableGlob {
|
PortableGlob {
|
||||||
field: String,
|
field: String,
|
||||||
|
|
|
@ -17,7 +17,7 @@ use uv_pep440::{Version, VersionSpecifiers};
|
||||||
use uv_pep508::{
|
use uv_pep508::{
|
||||||
ExtraOperator, MarkerExpression, MarkerTree, MarkerValueExtra, Requirement, VersionOrUrl,
|
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::serde_verbatim::SerdeVerbatim;
|
||||||
use crate::Error;
|
use crate::Error;
|
||||||
|
@ -803,7 +803,7 @@ pub(crate) struct ToolUv {
|
||||||
/// When building the source distribution, the following files and directories are included:
|
/// When building the source distribution, the following files and directories are included:
|
||||||
/// * `pyproject.toml`
|
/// * `pyproject.toml`
|
||||||
/// * The module under `tool.uv.build-backend.module-root`, by default
|
/// * 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`.
|
/// * `project.license-files` and `project.readme`.
|
||||||
/// * All directories under `tool.uv.build-backend.data`.
|
/// * All directories under `tool.uv.build-backend.data`.
|
||||||
/// * All patterns from `tool.uv.build-backend.source-include`.
|
/// * 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:
|
/// When building the wheel, the following files and directories are included:
|
||||||
/// * The module under `tool.uv.build-backend.module-root`, by default
|
/// * 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.
|
/// * `project.license-files` and `project.readme`, as part of the project metadata.
|
||||||
/// * Each directory under `tool.uv.build-backend.data`, as data directories.
|
/// * 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.
|
/// using the flat layout over the src layout.
|
||||||
pub(crate) module_root: PathBuf,
|
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
|
/// Glob expressions which files and directories to additionally include in the source
|
||||||
/// distribution.
|
/// distribution.
|
||||||
///
|
///
|
||||||
|
@ -877,6 +886,7 @@ impl Default for BuildBackendSettings {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
module_root: PathBuf::from("src"),
|
module_root: PathBuf::from("src"),
|
||||||
|
module_name: None,
|
||||||
source_include: Vec::new(),
|
source_include: Vec::new(),
|
||||||
default_excludes: true,
|
default_excludes: true,
|
||||||
source_exclude: Vec::new(),
|
source_exclude: Vec::new(),
|
||||||
|
|
|
@ -8,11 +8,13 @@ use globset::{Glob, GlobSet};
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::io::{BufReader, Cursor};
|
use std::io::{BufReader, Cursor};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::str::FromStr;
|
||||||
use tar::{EntryType, Header};
|
use tar::{EntryType, Header};
|
||||||
use tracing::{debug, trace};
|
use tracing::{debug, trace};
|
||||||
use uv_distribution_filename::{SourceDistExtension, SourceDistFilename};
|
use uv_distribution_filename::{SourceDistExtension, SourceDistFilename};
|
||||||
use uv_fs::Simplified;
|
use uv_fs::Simplified;
|
||||||
use uv_globfilter::{parse_portable_glob, GlobDirFilter};
|
use uv_globfilter::{parse_portable_glob, GlobDirFilter};
|
||||||
|
use uv_pypi_types::Identifier;
|
||||||
use uv_warnings::warn_user_once;
|
use uv_warnings::warn_user_once;
|
||||||
use walkdir::WalkDir;
|
use walkdir::WalkDir;
|
||||||
|
|
||||||
|
@ -65,11 +67,21 @@ fn source_dist_matcher(
|
||||||
let mut includes: Vec<String> = settings.source_include;
|
let mut includes: Vec<String> = settings.source_include;
|
||||||
// pyproject.toml is always included.
|
// pyproject.toml is always included.
|
||||||
includes.push(globset::escape("pyproject.toml"));
|
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
|
// 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).
|
// have files generated in the source dist -> wheel build step).
|
||||||
let import_path = &settings
|
let import_path = &settings
|
||||||
.module_root
|
.module_root
|
||||||
.join(pyproject_toml.name().as_dist_info_name().as_ref())
|
.join(module_name.as_ref())
|
||||||
.portable_display()
|
.portable_display()
|
||||||
.to_string();
|
.to_string();
|
||||||
includes.push(format!("{}/**", globset::escape(import_path)));
|
includes.push(format!("{}/**", globset::escape(import_path)));
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
use std::io::{BufReader, Read, Write};
|
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
use std::{io, mem};
|
|
||||||
|
|
||||||
use fs_err::File;
|
use fs_err::File;
|
||||||
use globset::{GlobSet, GlobSetBuilder};
|
use globset::{GlobSet, GlobSetBuilder};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use sha2::{Digest, Sha256};
|
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 tracing::{debug, trace};
|
||||||
use walkdir::WalkDir;
|
use walkdir::WalkDir;
|
||||||
use zip::{CompressionMethod, ZipWriter};
|
use zip::{CompressionMethod, ZipWriter};
|
||||||
|
@ -14,6 +14,7 @@ use uv_distribution_filename::WheelFilename;
|
||||||
use uv_fs::Simplified;
|
use uv_fs::Simplified;
|
||||||
use uv_globfilter::{parse_portable_glob, GlobDirFilter};
|
use uv_globfilter::{parse_portable_glob, GlobDirFilter};
|
||||||
use uv_platform_tags::{AbiTag, LanguageTag, PlatformTag};
|
use uv_platform_tags::{AbiTag, LanguageTag, PlatformTag};
|
||||||
|
use uv_pypi_types::Identifier;
|
||||||
use uv_warnings::warn_user_once;
|
use uv_warnings::warn_user_once;
|
||||||
|
|
||||||
use crate::metadata::{BuildBackendSettings, DEFAULT_EXCLUDES};
|
use crate::metadata::{BuildBackendSettings, DEFAULT_EXCLUDES};
|
||||||
|
@ -128,7 +129,17 @@ fn write_wheel(
|
||||||
return Err(Error::AbsoluteModuleRoot(settings.module_root.clone()));
|
return Err(Error::AbsoluteModuleRoot(settings.module_root.clone()));
|
||||||
}
|
}
|
||||||
let strip_root = source_tree.join(settings.module_root);
|
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() {
|
if !module_root.join("__init__.py").is_file() {
|
||||||
return Err(Error::MissingModule(module_root));
|
return Err(Error::MissingModule(module_root));
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,7 +32,7 @@ mailparse = { workspace = true }
|
||||||
regex = { workspace = true }
|
regex = { workspace = true }
|
||||||
rkyv = { workspace = true }
|
rkyv = { workspace = true }
|
||||||
schemars = { workspace = true, optional = true }
|
schemars = { workspace = true, optional = true }
|
||||||
serde = { workspace = true, optional = true }
|
serde = { workspace = true }
|
||||||
serde-untagged = { workspace = true }
|
serde-untagged = { workspace = true }
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
toml = { workspace = true }
|
toml = { workspace = true }
|
||||||
|
@ -42,6 +42,7 @@ url = { workspace = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
|
insta = { version = "1.40.0" }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
schemars = ["dep:schemars", "uv-normalize/schemars"]
|
schemars = ["dep:schemars", "uv-normalize/schemars"]
|
||||||
|
|
160
crates/uv-pypi-types/src/identifier.rs
Normal file
160
crates/uv-pypi-types/src/identifier.rs
Normal file
|
@ -0,0 +1,160 @@
|
||||||
|
use std::fmt::Display;
|
||||||
|
use std::str::FromStr;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
/// Simplified Python identifier.
|
||||||
|
///
|
||||||
|
/// We don't match Python's identifier rules
|
||||||
|
/// (<https://docs.python.org/3.13/reference/lexical_analysis.html#identifiers>) exactly
|
||||||
|
/// (we just use Rust's `is_alphabetic`) and we don't convert to NFKC, but it's good enough
|
||||||
|
/// for our validation purposes.
|
||||||
|
#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
pub struct Identifier(Box<str>);
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Error)]
|
||||||
|
pub enum IdentifierParseError {
|
||||||
|
#[error("An identifier must not be empty")]
|
||||||
|
Empty,
|
||||||
|
#[error(
|
||||||
|
"Invalid first character `{first}` for identifier `{identifier}`, expected an underscore or an alphabetic character"
|
||||||
|
)]
|
||||||
|
InvalidFirstChar { first: char, identifier: Box<str> },
|
||||||
|
#[error(
|
||||||
|
"Invalid character `{invalid_char}` at position {pos} for identifier `{identifier}`, \
|
||||||
|
expected an underscore or an alphanumeric character"
|
||||||
|
)]
|
||||||
|
InvalidChar {
|
||||||
|
pos: usize,
|
||||||
|
invalid_char: char,
|
||||||
|
identifier: Box<str>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Identifier {
|
||||||
|
pub fn new(identifier: impl Into<Box<str>>) -> Result<Self, IdentifierParseError> {
|
||||||
|
let identifier = identifier.into();
|
||||||
|
let mut chars = identifier.chars().enumerate();
|
||||||
|
let (_, first_char) = chars.next().ok_or(IdentifierParseError::Empty)?;
|
||||||
|
if first_char != '_' && !first_char.is_alphabetic() {
|
||||||
|
return Err(IdentifierParseError::InvalidFirstChar {
|
||||||
|
first: first_char,
|
||||||
|
identifier,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (pos, current_char) in chars {
|
||||||
|
if current_char != '_' && !current_char.is_alphanumeric() {
|
||||||
|
return Err(IdentifierParseError::InvalidChar {
|
||||||
|
// Make the position 1-indexed
|
||||||
|
pos: pos + 1,
|
||||||
|
invalid_char: current_char,
|
||||||
|
identifier,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self(identifier))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for Identifier {
|
||||||
|
type Err = IdentifierParseError;
|
||||||
|
|
||||||
|
fn from_str(identifier: &str) -> Result<Self, Self::Err> {
|
||||||
|
Self::new(identifier.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Identifier {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{}", self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRef<str> for Identifier {
|
||||||
|
fn as_ref(&self) -> &str {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> serde::de::Deserialize<'de> for Identifier {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::de::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let s = String::deserialize(deserializer)?;
|
||||||
|
Identifier::from_str(&s).map_err(serde::de::Error::custom)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use insta::assert_snapshot;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn valid() {
|
||||||
|
let valid_ids = vec![
|
||||||
|
"abc",
|
||||||
|
"_abc",
|
||||||
|
"a_bc",
|
||||||
|
"a123",
|
||||||
|
"snake_case",
|
||||||
|
"camelCase",
|
||||||
|
"PascalCase",
|
||||||
|
// A single character is valid
|
||||||
|
"_",
|
||||||
|
"a",
|
||||||
|
// Unicode
|
||||||
|
"α",
|
||||||
|
"férrîs",
|
||||||
|
"안녕하세요",
|
||||||
|
];
|
||||||
|
|
||||||
|
for valid_id in valid_ids {
|
||||||
|
assert!(Identifier::from_str(valid_id).is_ok(), "{}", valid_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn empty() {
|
||||||
|
assert_snapshot!(Identifier::from_str("").unwrap_err(), @"An identifier must not be empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn invalid_first_char() {
|
||||||
|
assert_snapshot!(
|
||||||
|
Identifier::from_str("1foo").unwrap_err(),
|
||||||
|
@"Invalid first character `1` for identifier `1foo`, expected an underscore or an alphabetic character"
|
||||||
|
);
|
||||||
|
assert_snapshot!(
|
||||||
|
Identifier::from_str("$foo").unwrap_err(),
|
||||||
|
@"Invalid first character `$` for identifier `$foo`, expected an underscore or an alphabetic character"
|
||||||
|
);
|
||||||
|
assert_snapshot!(
|
||||||
|
Identifier::from_str(".foo").unwrap_err(),
|
||||||
|
@"Invalid first character `.` for identifier `.foo`, expected an underscore or an alphabetic character"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn invalid_char() {
|
||||||
|
// A dot in module names equals a path separator, which is a separate problem.
|
||||||
|
assert_snapshot!(
|
||||||
|
Identifier::from_str("foo.bar").unwrap_err(),
|
||||||
|
@"Invalid character `.` at position 4 for identifier `foo.bar`, expected an underscore or an alphanumeric character"
|
||||||
|
);
|
||||||
|
assert_snapshot!(
|
||||||
|
Identifier::from_str("foo-bar").unwrap_err(),
|
||||||
|
@"Invalid character `-` at position 4 for identifier `foo-bar`, expected an underscore or an alphanumeric character"
|
||||||
|
);
|
||||||
|
assert_snapshot!(
|
||||||
|
Identifier::from_str("foo_bar$").unwrap_err(),
|
||||||
|
@"Invalid character `$` at position 8 for identifier `foo_bar$`, expected an underscore or an alphanumeric character"
|
||||||
|
);
|
||||||
|
assert_snapshot!(
|
||||||
|
Identifier::from_str("foo🦀bar").unwrap_err(),
|
||||||
|
@"Invalid character `🦀` at position 4 for identifier `foo🦀bar`, expected an underscore or an alphanumeric character"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,6 +2,7 @@ pub use base_url::*;
|
||||||
pub use conflicts::*;
|
pub use conflicts::*;
|
||||||
pub use dependency_groups::*;
|
pub use dependency_groups::*;
|
||||||
pub use direct_url::*;
|
pub use direct_url::*;
|
||||||
|
pub use identifier::*;
|
||||||
pub use lenient_requirement::*;
|
pub use lenient_requirement::*;
|
||||||
pub use marker_environment::*;
|
pub use marker_environment::*;
|
||||||
pub use metadata::*;
|
pub use metadata::*;
|
||||||
|
@ -15,6 +16,7 @@ mod base_url;
|
||||||
mod conflicts;
|
mod conflicts;
|
||||||
mod dependency_groups;
|
mod dependency_groups;
|
||||||
mod direct_url;
|
mod direct_url;
|
||||||
|
mod identifier;
|
||||||
mod lenient_requirement;
|
mod lenient_requirement;
|
||||||
mod marker_environment;
|
mod marker_environment;
|
||||||
mod metadata;
|
mod metadata;
|
||||||
|
|
|
@ -25,7 +25,7 @@ uv-normalize = { workspace = true }
|
||||||
uv-options-metadata = { workspace = true }
|
uv-options-metadata = { workspace = true }
|
||||||
uv-pep440 = { workspace = true }
|
uv-pep440 = { workspace = true }
|
||||||
uv-pep508 = { workspace = true }
|
uv-pep508 = { workspace = true }
|
||||||
uv-pypi-types = { workspace = true, features = ["serde"] }
|
uv-pypi-types = { workspace = true }
|
||||||
uv-static = { workspace = true }
|
uv-static = { workspace = true }
|
||||||
uv-warnings = { workspace = true }
|
uv-warnings = { workspace = true }
|
||||||
|
|
||||||
|
@ -38,8 +38,8 @@ schemars = { workspace = true, optional = true }
|
||||||
serde = { workspace = true, features = ["derive"] }
|
serde = { workspace = true, features = ["derive"] }
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
tokio = { workspace = true }
|
tokio = { workspace = true }
|
||||||
toml = { workspace = true }
|
toml = { workspace = true }
|
||||||
toml_edit = { workspace = true }
|
toml_edit = { workspace = true }
|
||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
url = { workspace = true }
|
url = { workspace = true }
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use crate::common::{uv_snapshot, venv_bin_path, TestContext};
|
use crate::common::{uv_snapshot, venv_bin_path, TestContext};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use assert_cmd::assert::OutputAssertExt;
|
use assert_cmd::assert::OutputAssertExt;
|
||||||
|
use assert_fs::fixture::{FileWriteStr, PathChild};
|
||||||
use flate2::bufread::GzDecoder;
|
use flate2::bufread::GzDecoder;
|
||||||
use fs_err::File;
|
use fs_err::File;
|
||||||
use indoc::indoc;
|
use indoc::indoc;
|
||||||
|
@ -278,3 +279,92 @@ fn preserve_executable_bit() -> Result<()> {
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Test `tool.uv.build-backend.module-name`.
|
||||||
|
///
|
||||||
|
/// We include only the module specified by `module-name`, ignoring the project name and all other
|
||||||
|
/// potential modules.
|
||||||
|
#[test]
|
||||||
|
fn rename_module() -> Result<()> {
|
||||||
|
let context = TestContext::new("3.12");
|
||||||
|
let temp_dir = TempDir::new()?;
|
||||||
|
|
||||||
|
context
|
||||||
|
.temp_dir
|
||||||
|
.child("pyproject.toml")
|
||||||
|
.write_str(indoc! {r#"
|
||||||
|
[project]
|
||||||
|
name = "foo"
|
||||||
|
version = "1.0.0"
|
||||||
|
|
||||||
|
[tool.uv.build-backend]
|
||||||
|
module-name = "bar"
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["uv_build>=0.5,<0.7"]
|
||||||
|
build-backend = "uv_build"
|
||||||
|
"#})?;
|
||||||
|
|
||||||
|
// This is the module we would usually include, but due to the renaming by `module-name` must
|
||||||
|
// ignore.
|
||||||
|
context
|
||||||
|
.temp_dir
|
||||||
|
.child("src/foo/__init__.py")
|
||||||
|
.write_str(r#"print("Hi from foo")"#)?;
|
||||||
|
// This module would be ignored from just `project.name`, but is selected due to the renaming.
|
||||||
|
context
|
||||||
|
.temp_dir
|
||||||
|
.child("src/bar/__init__.py")
|
||||||
|
.write_str(r#"print("Hi from bar")"#)?;
|
||||||
|
|
||||||
|
uv_snapshot!(context
|
||||||
|
.build_backend()
|
||||||
|
.arg("build-wheel")
|
||||||
|
.arg(temp_dir.path())
|
||||||
|
.env("UV_PREVIEW", "1"), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
foo-1.0.0-py3-none-any.whl
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
"###);
|
||||||
|
|
||||||
|
context
|
||||||
|
.pip_install()
|
||||||
|
.arg(temp_dir.path().join("foo-1.0.0-py3-none-any.whl"))
|
||||||
|
.assert()
|
||||||
|
.success();
|
||||||
|
|
||||||
|
// Importing the module with the `module-name` name succeeds.
|
||||||
|
uv_snapshot!(Command::new(context.interpreter())
|
||||||
|
.arg("-c")
|
||||||
|
.arg("import bar")
|
||||||
|
// Python on windows
|
||||||
|
.env(EnvVars::PYTHONUTF8, "1"), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
Hi from bar
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
"###);
|
||||||
|
|
||||||
|
// Importing the package name fails, it was overridden by `module-name`.
|
||||||
|
uv_snapshot!(Command::new(context.interpreter())
|
||||||
|
.arg("-c")
|
||||||
|
.arg("import foo")
|
||||||
|
// Python on windows
|
||||||
|
.env(EnvVars::PYTHONUTF8, "1"), @r###"
|
||||||
|
success: false
|
||||||
|
exit_code: 1
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "<string>", line 1, in <module>
|
||||||
|
ModuleNotFoundError: No module named 'foo'
|
||||||
|
"###);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue