1
1
use crate :: Error ;
2
2
use itertools:: Itertools ;
3
3
use serde:: Deserialize ;
4
- use std:: collections:: { BTreeMap , Bound } ;
4
+ use std:: collections:: { BTreeMap , BTreeSet , Bound } ;
5
5
use std:: ffi:: OsStr ;
6
6
use std:: fmt:: Display ;
7
7
use std:: path:: { Path , PathBuf } ;
@@ -11,7 +11,9 @@ use uv_fs::Simplified;
11
11
use uv_globfilter:: { parse_portable_glob, GlobDirFilter } ;
12
12
use uv_normalize:: { ExtraName , PackageName } ;
13
13
use uv_pep440:: { Version , VersionSpecifiers } ;
14
- use uv_pep508:: { Requirement , VersionOrUrl } ;
14
+ use uv_pep508:: {
15
+ ExtraOperator , MarkerExpression , MarkerTree , MarkerValueExtra , Requirement , VersionOrUrl ,
16
+ } ;
15
17
use uv_pypi_types:: { Metadata23 , VerbatimParsedUrl } ;
16
18
use version_ranges:: Ranges ;
17
19
use walkdir:: WalkDir ;
@@ -471,8 +473,21 @@ impl PyProjectToml {
471
473
. optional_dependencies
472
474
. iter ( )
473
475
. 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 < _ > > ( ) ;
476
491
477
492
Ok ( Metadata23 {
478
493
metadata_version : metadata_version. to_string ( ) ,
@@ -500,13 +515,8 @@ impl PyProjectToml {
500
515
license_expression,
501
516
license_files,
502
517
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 ( ) ,
510
520
// Not commonly set.
511
521
provides_dist : vec ! [ ] ,
512
522
// Not supported.
@@ -519,11 +529,80 @@ impl PyProjectToml {
519
529
// Not used by other tools, not supported.
520
530
requires_external : vec ! [ ] ,
521
531
project_urls,
522
- provides_extras : extras,
523
532
dynamic : vec ! [ ] ,
524
533
} )
525
534
}
526
535
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
+
527
606
/// Validate and convert the entrypoints in `pyproject.toml`, including console and GUI scripts,
528
607
/// to an `entry_points.txt`.
529
608
///
@@ -1009,37 +1088,172 @@ mod tests {
1009
1088
let metadata = pyproject_toml. to_metadata ( temp_dir. path ( ) ) . unwrap ( ) ;
1010
1089
1011
1090
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
+ "### ) ;
1043
1257
1044
1258
assert_snapshot ! ( pyproject_toml. to_entry_points( ) . unwrap( ) . unwrap( ) , @r###"
1045
1259
[console_scripts]
0 commit comments