mirror of
https://github.com/astral-sh/uv.git
synced 2025-10-29 19:17:26 +00:00
Include extras in uv-build Requires-Dist metadata (#10110)
## Summary Closes https://github.com/astral-sh/uv/issues/10091.
This commit is contained in:
parent
c329623770
commit
340e30d91e
1 changed files with 255 additions and 41 deletions
|
|
@ -1,7 +1,7 @@
|
||||||
use crate::Error;
|
use crate::Error;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::collections::{BTreeMap, Bound};
|
use std::collections::{BTreeMap, BTreeSet, Bound};
|
||||||
use std::ffi::OsStr;
|
use std::ffi::OsStr;
|
||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
@ -11,7 +11,9 @@ use uv_fs::Simplified;
|
||||||
use uv_globfilter::{parse_portable_glob, GlobDirFilter};
|
use uv_globfilter::{parse_portable_glob, GlobDirFilter};
|
||||||
use uv_normalize::{ExtraName, PackageName};
|
use uv_normalize::{ExtraName, PackageName};
|
||||||
use uv_pep440::{Version, VersionSpecifiers};
|
use uv_pep440::{Version, VersionSpecifiers};
|
||||||
use uv_pep508::{Requirement, VersionOrUrl};
|
use uv_pep508::{
|
||||||
|
ExtraOperator, MarkerExpression, MarkerTree, MarkerValueExtra, Requirement, VersionOrUrl,
|
||||||
|
};
|
||||||
use uv_pypi_types::{Metadata23, VerbatimParsedUrl};
|
use uv_pypi_types::{Metadata23, VerbatimParsedUrl};
|
||||||
use version_ranges::Ranges;
|
use version_ranges::Ranges;
|
||||||
use walkdir::WalkDir;
|
use walkdir::WalkDir;
|
||||||
|
|
@ -471,8 +473,21 @@ impl PyProjectToml {
|
||||||
.optional_dependencies
|
.optional_dependencies
|
||||||
.iter()
|
.iter()
|
||||||
.flat_map(|optional_dependencies| optional_dependencies.keys())
|
.flat_map(|optional_dependencies| optional_dependencies.keys())
|
||||||
.map(ToString::to_string)
|
.collect::<Vec<_>>();
|
||||||
.collect();
|
|
||||||
|
let requires_dist = self
|
||||||
|
.project
|
||||||
|
.dependencies
|
||||||
|
.iter()
|
||||||
|
.flatten()
|
||||||
|
.cloned()
|
||||||
|
.chain(
|
||||||
|
extras
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.flat_map(|extra| self.flatten_optional_dependencies(extra)),
|
||||||
|
)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
Ok(Metadata23 {
|
Ok(Metadata23 {
|
||||||
metadata_version: metadata_version.to_string(),
|
metadata_version: metadata_version.to_string(),
|
||||||
|
|
@ -500,13 +515,8 @@ impl PyProjectToml {
|
||||||
license_expression,
|
license_expression,
|
||||||
license_files,
|
license_files,
|
||||||
classifiers: self.project.classifiers.clone().unwrap_or_default(),
|
classifiers: self.project.classifiers.clone().unwrap_or_default(),
|
||||||
requires_dist: self
|
requires_dist: requires_dist.iter().map(ToString::to_string).collect(),
|
||||||
.project
|
provides_extras: extras.iter().map(ToString::to_string).collect(),
|
||||||
.dependencies
|
|
||||||
.iter()
|
|
||||||
.flatten()
|
|
||||||
.map(ToString::to_string)
|
|
||||||
.collect(),
|
|
||||||
// Not commonly set.
|
// Not commonly set.
|
||||||
provides_dist: vec![],
|
provides_dist: vec![],
|
||||||
// Not supported.
|
// Not supported.
|
||||||
|
|
@ -519,11 +529,80 @@ impl PyProjectToml {
|
||||||
// Not used by other tools, not supported.
|
// Not used by other tools, not supported.
|
||||||
requires_external: vec![],
|
requires_external: vec![],
|
||||||
project_urls,
|
project_urls,
|
||||||
provides_extras: extras,
|
|
||||||
dynamic: vec![],
|
dynamic: vec![],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return the flattened [`Requirement`] entries for the given [`ExtraName`].
|
||||||
|
fn flatten_optional_dependencies(&self, extra: &ExtraName) -> Vec<Requirement> {
|
||||||
|
fn collect<'project>(
|
||||||
|
extra: &'project ExtraName,
|
||||||
|
marker: MarkerTree,
|
||||||
|
optional_dependencies: &'project BTreeMap<ExtraName, Vec<Requirement>>,
|
||||||
|
project_name: &'project PackageName,
|
||||||
|
dependencies: &mut Vec<Requirement>,
|
||||||
|
seen: &mut BTreeSet<(&'project ExtraName, MarkerTree)>,
|
||||||
|
) {
|
||||||
|
if !seen.insert((extra, marker)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for requirement in optional_dependencies.get(extra).into_iter().flatten() {
|
||||||
|
if requirement.name == *project_name {
|
||||||
|
for extra in &requirement.extras {
|
||||||
|
collect(
|
||||||
|
extra,
|
||||||
|
marker,
|
||||||
|
optional_dependencies,
|
||||||
|
project_name,
|
||||||
|
dependencies,
|
||||||
|
seen,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let mut marker = marker;
|
||||||
|
marker.and(requirement.marker);
|
||||||
|
dependencies.push(Requirement {
|
||||||
|
name: requirement.name.clone(),
|
||||||
|
extras: requirement.extras.clone(),
|
||||||
|
version_or_url: requirement.version_or_url.clone(),
|
||||||
|
origin: requirement.origin.clone(),
|
||||||
|
marker,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve all dependencies for the given extra.
|
||||||
|
let mut dependencies = {
|
||||||
|
let mut dependencies = Vec::new();
|
||||||
|
collect(
|
||||||
|
extra,
|
||||||
|
MarkerTree::default(),
|
||||||
|
self.project
|
||||||
|
.optional_dependencies
|
||||||
|
.as_ref()
|
||||||
|
.unwrap_or(&BTreeMap::new()),
|
||||||
|
&self.project.name,
|
||||||
|
&mut dependencies,
|
||||||
|
&mut BTreeSet::default(),
|
||||||
|
);
|
||||||
|
dependencies
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add the extra to the marker to each dependency.
|
||||||
|
for requirement in &mut dependencies {
|
||||||
|
requirement
|
||||||
|
.marker
|
||||||
|
.and(MarkerTree::expression(MarkerExpression::Extra {
|
||||||
|
operator: ExtraOperator::Equal,
|
||||||
|
name: MarkerValueExtra::Extra(extra.clone()),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies
|
||||||
|
}
|
||||||
|
|
||||||
/// Validate and convert the entrypoints in `pyproject.toml`, including console and GUI scripts,
|
/// Validate and convert the entrypoints in `pyproject.toml`, including console and GUI scripts,
|
||||||
/// to an `entry_points.txt`.
|
/// to an `entry_points.txt`.
|
||||||
///
|
///
|
||||||
|
|
@ -1009,37 +1088,172 @@ mod tests {
|
||||||
let metadata = pyproject_toml.to_metadata(temp_dir.path()).unwrap();
|
let metadata = pyproject_toml.to_metadata(temp_dir.path()).unwrap();
|
||||||
|
|
||||||
assert_snapshot!(metadata.core_metadata_format(), @r###"
|
assert_snapshot!(metadata.core_metadata_format(), @r###"
|
||||||
Metadata-Version: 2.3
|
Metadata-Version: 2.3
|
||||||
Name: hello-world
|
Name: hello-world
|
||||||
Version: 0.1.0
|
Version: 0.1.0
|
||||||
Summary: A Python package
|
Summary: A Python package
|
||||||
Keywords: demo,example,package
|
Keywords: demo,example,package
|
||||||
Author: Ferris the crab
|
Author: Ferris the crab
|
||||||
Author-email: Ferris the crab <ferris@rustacean.net>
|
Author-email: Ferris the crab <ferris@rustacean.net>
|
||||||
License: THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
License: THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||||
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||||
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||||
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
|
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
|
||||||
CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
|
CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
|
||||||
OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
Classifier: Development Status :: 6 - Mature
|
Classifier: Development Status :: 6 - Mature
|
||||||
Classifier: License :: OSI Approved :: MIT License
|
Classifier: License :: OSI Approved :: MIT License
|
||||||
Classifier: License :: OSI Approved :: Apache Software License
|
Classifier: License :: OSI Approved :: Apache Software License
|
||||||
Classifier: Programming Language :: Python
|
Classifier: Programming Language :: Python
|
||||||
Requires-Dist: flask>=3,<4
|
Requires-Dist: flask>=3,<4
|
||||||
Requires-Dist: sqlalchemy[asyncio]>=2.0.35,<3
|
Requires-Dist: sqlalchemy[asyncio]>=2.0.35,<3
|
||||||
Maintainer: Konsti
|
Requires-Dist: pymysql>=1.1.1,<2 ; extra == 'mysql'
|
||||||
Maintainer-email: Konsti <konstin@mailbox.org>
|
Requires-Dist: psycopg>=3.2.2,<4 ; extra == 'postgres'
|
||||||
Project-URL: Homepage, https://github.com/astral-sh/uv
|
Maintainer: Konsti
|
||||||
Project-URL: Repository, https://astral.sh
|
Maintainer-email: Konsti <konstin@mailbox.org>
|
||||||
Provides-Extra: mysql
|
Project-URL: Homepage, https://github.com/astral-sh/uv
|
||||||
Provides-Extra: postgres
|
Project-URL: Repository, https://astral.sh
|
||||||
Description-Content-Type: text/markdown
|
Provides-Extra: mysql
|
||||||
|
Provides-Extra: postgres
|
||||||
|
Description-Content-Type: text/markdown
|
||||||
|
|
||||||
# Foo
|
# Foo
|
||||||
|
|
||||||
This is the foo library.
|
This is the foo library.
|
||||||
"###);
|
"###);
|
||||||
|
|
||||||
|
assert_snapshot!(pyproject_toml.to_entry_points().unwrap().unwrap(), @r###"
|
||||||
|
[console_scripts]
|
||||||
|
foo = foo.cli:__main__
|
||||||
|
|
||||||
|
[gui_scripts]
|
||||||
|
foo-gui = foo.gui
|
||||||
|
|
||||||
|
[bar_group]
|
||||||
|
foo-bar = foo:bar
|
||||||
|
|
||||||
|
"###);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn self_extras() {
|
||||||
|
let temp_dir = TempDir::new().unwrap();
|
||||||
|
|
||||||
|
fs_err::write(
|
||||||
|
temp_dir.path().join("Readme.md"),
|
||||||
|
indoc! {r"
|
||||||
|
# Foo
|
||||||
|
|
||||||
|
This is the foo library.
|
||||||
|
"},
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
fs_err::write(
|
||||||
|
temp_dir.path().join("License.txt"),
|
||||||
|
indoc! {r#"
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||||
|
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||||
|
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
|
||||||
|
CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
|
||||||
|
OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
"#},
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let contents = indoc! {r#"
|
||||||
|
# See https://github.com/pypa/sampleproject/blob/main/pyproject.toml for another example
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "hello-world"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "A Python package"
|
||||||
|
readme = "Readme.md"
|
||||||
|
requires_python = ">=3.12"
|
||||||
|
license = { file = "License.txt" }
|
||||||
|
authors = [{ name = "Ferris the crab", email = "ferris@rustacean.net" }]
|
||||||
|
maintainers = [{ name = "Konsti", email = "konstin@mailbox.org" }]
|
||||||
|
keywords = ["demo", "example", "package"]
|
||||||
|
classifiers = [
|
||||||
|
"Development Status :: 6 - Mature",
|
||||||
|
"License :: OSI Approved :: MIT License",
|
||||||
|
# https://github.com/pypa/trove-classifiers/issues/17
|
||||||
|
"License :: OSI Approved :: Apache Software License",
|
||||||
|
"Programming Language :: Python",
|
||||||
|
]
|
||||||
|
dependencies = ["flask>=3,<4", "sqlalchemy[asyncio]>=2.0.35,<3"]
|
||||||
|
# We don't support dynamic fields, the default empty array is the only allowed value.
|
||||||
|
dynamic = []
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
postgres = ["psycopg>=3.2.2,<4 ; sys_platform == 'linux'"]
|
||||||
|
mysql = ["pymysql>=1.1.1,<2"]
|
||||||
|
databases = ["hello-world[mysql]", "hello-world[postgres]"]
|
||||||
|
all = ["hello-world[databases]", "hello-world[postgres]", "hello-world[mysql]"]
|
||||||
|
|
||||||
|
[project.urls]
|
||||||
|
"Homepage" = "https://github.com/astral-sh/uv"
|
||||||
|
"Repository" = "https://astral.sh"
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
foo = "foo.cli:__main__"
|
||||||
|
|
||||||
|
[project.gui-scripts]
|
||||||
|
foo-gui = "foo.gui"
|
||||||
|
|
||||||
|
[project.entry-points.bar_group]
|
||||||
|
foo-bar = "foo:bar"
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["uv>=0.4.15,<5"]
|
||||||
|
build-backend = "uv"
|
||||||
|
"#
|
||||||
|
};
|
||||||
|
|
||||||
|
let pyproject_toml = PyProjectToml::parse(contents).unwrap();
|
||||||
|
let metadata = pyproject_toml.to_metadata(temp_dir.path()).unwrap();
|
||||||
|
|
||||||
|
assert_snapshot!(metadata.core_metadata_format(), @r###"
|
||||||
|
Metadata-Version: 2.3
|
||||||
|
Name: hello-world
|
||||||
|
Version: 0.1.0
|
||||||
|
Summary: A Python package
|
||||||
|
Keywords: demo,example,package
|
||||||
|
Author: Ferris the crab
|
||||||
|
Author-email: Ferris the crab <ferris@rustacean.net>
|
||||||
|
License: THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||||
|
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||||
|
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
|
||||||
|
CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
|
||||||
|
OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
Classifier: Development Status :: 6 - Mature
|
||||||
|
Classifier: License :: OSI Approved :: MIT License
|
||||||
|
Classifier: License :: OSI Approved :: Apache Software License
|
||||||
|
Classifier: Programming Language :: Python
|
||||||
|
Requires-Dist: flask>=3,<4
|
||||||
|
Requires-Dist: sqlalchemy[asyncio]>=2.0.35,<3
|
||||||
|
Requires-Dist: pymysql>=1.1.1,<2 ; extra == 'all'
|
||||||
|
Requires-Dist: psycopg>=3.2.2,<4 ; sys_platform == 'linux' and extra == 'all'
|
||||||
|
Requires-Dist: pymysql>=1.1.1,<2 ; extra == 'databases'
|
||||||
|
Requires-Dist: psycopg>=3.2.2,<4 ; sys_platform == 'linux' and extra == 'databases'
|
||||||
|
Requires-Dist: pymysql>=1.1.1,<2 ; extra == 'mysql'
|
||||||
|
Requires-Dist: psycopg>=3.2.2,<4 ; sys_platform == 'linux' and extra == 'postgres'
|
||||||
|
Maintainer: Konsti
|
||||||
|
Maintainer-email: Konsti <konstin@mailbox.org>
|
||||||
|
Project-URL: Homepage, https://github.com/astral-sh/uv
|
||||||
|
Project-URL: Repository, https://astral.sh
|
||||||
|
Provides-Extra: all
|
||||||
|
Provides-Extra: databases
|
||||||
|
Provides-Extra: mysql
|
||||||
|
Provides-Extra: postgres
|
||||||
|
Description-Content-Type: text/markdown
|
||||||
|
|
||||||
|
# Foo
|
||||||
|
|
||||||
|
This is the foo library.
|
||||||
|
"###);
|
||||||
|
|
||||||
assert_snapshot!(pyproject_toml.to_entry_points().unwrap().unwrap(), @r###"
|
assert_snapshot!(pyproject_toml.to_entry_points().unwrap().unwrap(), @r###"
|
||||||
[console_scripts]
|
[console_scripts]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue