Skip to content

Commit 99e7d98

Browse files
committed
Include extras in uv-build Requires-Dist metadata
1 parent 9e33658 commit 99e7d98

File tree

1 file changed

+257
-43
lines changed

1 file changed

+257
-43
lines changed

crates/uv-build-backend/src/metadata.rs

Lines changed: 257 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use crate::Error;
22
use itertools::Itertools;
33
use serde::Deserialize;
4-
use std::collections::{BTreeMap, Bound};
4+
use std::collections::{BTreeMap, BTreeSet, Bound};
55
use std::ffi::OsStr;
66
use std::fmt::Display;
77
use std::path::{Path, PathBuf};
@@ -11,7 +11,9 @@ use uv_fs::Simplified;
1111
use uv_globfilter::{parse_portable_glob, GlobDirFilter};
1212
use uv_normalize::{ExtraName, PackageName};
1313
use uv_pep440::{Version, VersionSpecifiers};
14-
use uv_pep508::{Requirement, VersionOrUrl};
14+
use uv_pep508::{
15+
ExtraOperator, MarkerExpression, MarkerTree, MarkerValueExtra, Requirement, VersionOrUrl,
16+
};
1517
use uv_pypi_types::{Metadata23, VerbatimParsedUrl};
1618
use version_ranges::Ranges;
1719
use walkdir::WalkDir;
@@ -471,8 +473,21 @@ impl PyProjectToml {
471473
.optional_dependencies
472474
.iter()
473475
.flat_map(|optional_dependencies| optional_dependencies.keys())
474-
.map(ToString::to_string)
475-
.collect();
476+
.collect::<Vec<_>>();
477+
478+
let requires_dist = self
479+
.project
480+
.dependencies
481+
.iter()
482+
.flatten()
483+
.cloned()
484+
.chain(
485+
extras
486+
.iter()
487+
.copied()
488+
.flat_map(|extra| self.flatten_optional_dependencies(extra)),
489+
)
490+
.collect::<Vec<_>>();
476491

477492
Ok(Metadata23 {
478493
metadata_version: metadata_version.to_string(),
@@ -500,13 +515,8 @@ impl PyProjectToml {
500515
license_expression,
501516
license_files,
502517
classifiers: self.project.classifiers.clone().unwrap_or_default(),
503-
requires_dist: self
504-
.project
505-
.dependencies
506-
.iter()
507-
.flatten()
508-
.map(ToString::to_string)
509-
.collect(),
518+
requires_dist: requires_dist.iter().map(ToString::to_string).collect(),
519+
provides_extras: extras.iter().map(ToString::to_string).collect(),
510520
// Not commonly set.
511521
provides_dist: vec![],
512522
// Not supported.
@@ -519,11 +529,80 @@ impl PyProjectToml {
519529
// Not used by other tools, not supported.
520530
requires_external: vec![],
521531
project_urls,
522-
provides_extras: extras,
523532
dynamic: vec![],
524533
})
525534
}
526535

536+
/// Return the flattened [`Requirement`] entries for the given [`ExtraName`].
537+
fn flatten_optional_dependencies(&self, extra: &ExtraName) -> Vec<Requirement> {
538+
fn collect<'project>(
539+
extra: &'project ExtraName,
540+
marker: MarkerTree,
541+
optional_dependencies: &'project BTreeMap<ExtraName, Vec<Requirement>>,
542+
project_name: &'project PackageName,
543+
dependencies: &mut Vec<Requirement>,
544+
seen: &mut BTreeSet<(&'project ExtraName, MarkerTree)>,
545+
) {
546+
if !seen.insert((extra, marker)) {
547+
return;
548+
}
549+
550+
for requirement in optional_dependencies.get(extra).into_iter().flatten() {
551+
if requirement.name == *project_name {
552+
for extra in &requirement.extras {
553+
collect(
554+
extra,
555+
marker,
556+
optional_dependencies,
557+
project_name,
558+
dependencies,
559+
seen,
560+
);
561+
}
562+
} else {
563+
let mut marker = marker;
564+
marker.and(requirement.marker);
565+
dependencies.push(Requirement {
566+
name: requirement.name.clone(),
567+
extras: requirement.extras.clone(),
568+
version_or_url: requirement.version_or_url.clone(),
569+
origin: requirement.origin.clone(),
570+
marker,
571+
});
572+
}
573+
}
574+
}
575+
576+
// Resolve all dependencies for the given extra.
577+
let mut dependencies = {
578+
let mut dependencies = Vec::new();
579+
collect(
580+
extra,
581+
MarkerTree::default(),
582+
self.project
583+
.optional_dependencies
584+
.as_ref()
585+
.unwrap_or(&BTreeMap::new()),
586+
&self.project.name,
587+
&mut dependencies,
588+
&mut BTreeSet::default(),
589+
);
590+
dependencies
591+
};
592+
593+
// Add the extra to the marker to each dependency.
594+
for requirement in &mut dependencies {
595+
requirement
596+
.marker
597+
.and(MarkerTree::expression(MarkerExpression::Extra {
598+
operator: ExtraOperator::Equal,
599+
name: MarkerValueExtra::Extra(extra.clone()),
600+
}));
601+
}
602+
603+
dependencies
604+
}
605+
527606
/// Validate and convert the entrypoints in `pyproject.toml`, including console and GUI scripts,
528607
/// to an `entry_points.txt`.
529608
///
@@ -1009,37 +1088,172 @@ mod tests {
10091088
let metadata = pyproject_toml.to_metadata(temp_dir.path()).unwrap();
10101089

10111090
assert_snapshot!(metadata.core_metadata_format(), @r###"
1012-
Metadata-Version: 2.3
1013-
Name: hello-world
1014-
Version: 0.1.0
1015-
Summary: A Python package
1016-
Keywords: demo,example,package
1017-
Author: Ferris the crab
1018-
Author-email: Ferris the crab <[email protected]>
1019-
License: THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
1020-
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
1021-
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
1022-
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
1023-
CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
1024-
OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
1025-
Classifier: Development Status :: 6 - Mature
1026-
Classifier: License :: OSI Approved :: MIT License
1027-
Classifier: License :: OSI Approved :: Apache Software License
1028-
Classifier: Programming Language :: Python
1029-
Requires-Dist: flask>=3,<4
1030-
Requires-Dist: sqlalchemy[asyncio]>=2.0.35,<3
1031-
Maintainer: Konsti
1032-
Maintainer-email: Konsti <[email protected]>
1033-
Project-URL: Homepage, https://github.com/astral-sh/uv
1034-
Project-URL: Repository, https://astral.sh
1035-
Provides-Extra: mysql
1036-
Provides-Extra: postgres
1037-
Description-Content-Type: text/markdown
1038-
1039-
# Foo
1040-
1041-
This is the foo library.
1042-
"###);
1091+
Metadata-Version: 2.3
1092+
Name: hello-world
1093+
Version: 0.1.0
1094+
Summary: A Python package
1095+
Keywords: demo,example,package
1096+
Author: Ferris the crab
1097+
Author-email: Ferris the crab <[email protected]>
1098+
License: THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
1099+
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
1100+
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
1101+
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
1102+
CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
1103+
OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
1104+
Classifier: Development Status :: 6 - Mature
1105+
Classifier: License :: OSI Approved :: MIT License
1106+
Classifier: License :: OSI Approved :: Apache Software License
1107+
Classifier: Programming Language :: Python
1108+
Requires-Dist: flask>=3,<4
1109+
Requires-Dist: sqlalchemy[asyncio]>=2.0.35,<3
1110+
Requires-Dist: pymysql>=1.1.1,<2 ; extra == 'mysql'
1111+
Requires-Dist: psycopg>=3.2.2,<4 ; extra == 'postgres'
1112+
Maintainer: Konsti
1113+
Maintainer-email: Konsti <[email protected]>
1114+
Project-URL: Homepage, https://github.com/astral-sh/uv
1115+
Project-URL: Repository, https://astral.sh
1116+
Provides-Extra: mysql
1117+
Provides-Extra: postgres
1118+
Description-Content-Type: text/markdown
1119+
1120+
# Foo
1121+
1122+
This is the foo library.
1123+
"###);
1124+
1125+
assert_snapshot!(pyproject_toml.to_entry_points().unwrap().unwrap(), @r###"
1126+
[console_scripts]
1127+
foo = foo.cli:__main__
1128+
1129+
[gui_scripts]
1130+
foo-gui = foo.gui
1131+
1132+
[bar_group]
1133+
foo-bar = foo:bar
1134+
1135+
"###);
1136+
}
1137+
1138+
#[test]
1139+
fn self_extras() {
1140+
let temp_dir = TempDir::new().unwrap();
1141+
1142+
fs_err::write(
1143+
temp_dir.path().join("Readme.md"),
1144+
indoc! {r"
1145+
# Foo
1146+
1147+
This is the foo library.
1148+
"},
1149+
)
1150+
.unwrap();
1151+
1152+
fs_err::write(
1153+
temp_dir.path().join("License.txt"),
1154+
indoc! {r#"
1155+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
1156+
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
1157+
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
1158+
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
1159+
CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
1160+
OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
1161+
"#},
1162+
)
1163+
.unwrap();
1164+
1165+
let contents = indoc! {r#"
1166+
# See https://github.com/pypa/sampleproject/blob/main/pyproject.toml for another example
1167+
1168+
[project]
1169+
name = "hello-world"
1170+
version = "0.1.0"
1171+
description = "A Python package"
1172+
readme = "Readme.md"
1173+
requires_python = ">=3.12"
1174+
license = { file = "License.txt" }
1175+
authors = [{ name = "Ferris the crab", email = "[email protected]" }]
1176+
maintainers = [{ name = "Konsti", email = "[email protected]" }]
1177+
keywords = ["demo", "example", "package"]
1178+
classifiers = [
1179+
"Development Status :: 6 - Mature",
1180+
"License :: OSI Approved :: MIT License",
1181+
# https://github.com/pypa/trove-classifiers/issues/17
1182+
"License :: OSI Approved :: Apache Software License",
1183+
"Programming Language :: Python",
1184+
]
1185+
dependencies = ["flask>=3,<4", "sqlalchemy[asyncio]>=2.0.35,<3"]
1186+
# We don't support dynamic fields, the default empty array is the only allowed value.
1187+
dynamic = []
1188+
1189+
[project.optional-dependencies]
1190+
postgres = ["psycopg>=3.2.2,<4 ; sys_platform == 'linux'"]
1191+
mysql = ["pymysql>=1.1.1,<2"]
1192+
databases = ["hello-world[mysql]", "hello-world[postgres]"]
1193+
all = ["hello-world[databases]", "hello-world[postgres]", "hello-world[mysql]"]
1194+
1195+
[project.urls]
1196+
"Homepage" = "https://github.com/astral-sh/uv"
1197+
"Repository" = "https://astral.sh"
1198+
1199+
[project.scripts]
1200+
foo = "foo.cli:__main__"
1201+
1202+
[project.gui-scripts]
1203+
foo-gui = "foo.gui"
1204+
1205+
[project.entry-points.bar_group]
1206+
foo-bar = "foo:bar"
1207+
1208+
[build-system]
1209+
requires = ["uv>=0.4.15,<5"]
1210+
build-backend = "uv"
1211+
"#
1212+
};
1213+
1214+
let pyproject_toml = PyProjectToml::parse(contents).unwrap();
1215+
let metadata = pyproject_toml.to_metadata(temp_dir.path()).unwrap();
1216+
1217+
assert_snapshot!(metadata.core_metadata_format(), @r###"
1218+
Metadata-Version: 2.3
1219+
Name: hello-world
1220+
Version: 0.1.0
1221+
Summary: A Python package
1222+
Keywords: demo,example,package
1223+
Author: Ferris the crab
1224+
Author-email: Ferris the crab <[email protected]>
1225+
License: THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
1226+
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
1227+
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
1228+
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
1229+
CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
1230+
OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
1231+
Classifier: Development Status :: 6 - Mature
1232+
Classifier: License :: OSI Approved :: MIT License
1233+
Classifier: License :: OSI Approved :: Apache Software License
1234+
Classifier: Programming Language :: Python
1235+
Requires-Dist: flask>=3,<4
1236+
Requires-Dist: sqlalchemy[asyncio]>=2.0.35,<3
1237+
Requires-Dist: pymysql>=1.1.1,<2 ; extra == 'all'
1238+
Requires-Dist: psycopg>=3.2.2,<4 ; sys_platform == 'linux' and extra == 'all'
1239+
Requires-Dist: pymysql>=1.1.1,<2 ; extra == 'databases'
1240+
Requires-Dist: psycopg>=3.2.2,<4 ; sys_platform == 'linux' and extra == 'databases'
1241+
Requires-Dist: pymysql>=1.1.1,<2 ; extra == 'mysql'
1242+
Requires-Dist: psycopg>=3.2.2,<4 ; sys_platform == 'linux' and extra == 'postgres'
1243+
Maintainer: Konsti
1244+
Maintainer-email: Konsti <[email protected]>
1245+
Project-URL: Homepage, https://github.com/astral-sh/uv
1246+
Project-URL: Repository, https://astral.sh
1247+
Provides-Extra: all
1248+
Provides-Extra: databases
1249+
Provides-Extra: mysql
1250+
Provides-Extra: postgres
1251+
Description-Content-Type: text/markdown
1252+
1253+
# Foo
1254+
1255+
This is the foo library.
1256+
"###);
10431257

10441258
assert_snapshot!(pyproject_toml.to_entry_points().unwrap().unwrap(), @r###"
10451259
[console_scripts]

0 commit comments

Comments
 (0)