@@ -1067,32 +1067,17 @@ impl Lock {
1067
1067
}
1068
1068
}
1069
1069
1070
- // Validate that the member sources have not changed.
1071
- {
1072
- // E.g., that they've switched from virtual to non-virtual or vice versa.
1073
- for ( name, member) in packages {
1074
- let expected = !member. pyproject_toml ( ) . is_package ( ) ;
1075
- let actual = self
1076
- . find_by_name ( name)
1077
- . ok ( )
1078
- . flatten ( )
1079
- . map ( |package| matches ! ( package. id. source, Source :: Virtual ( _) ) ) ;
1080
- if actual != Some ( expected) {
1081
- return Ok ( SatisfiesResult :: MismatchedVirtual ( name. clone ( ) , expected) ) ;
1082
- }
1083
- }
1084
-
1085
- // E.g., that they've switched from dynamic to non-dynamic or vice versa.
1086
- for ( name, member) in packages {
1087
- let expected = member. pyproject_toml ( ) . is_dynamic ( ) ;
1088
- let actual = self
1089
- . find_by_name ( name)
1090
- . ok ( )
1091
- . flatten ( )
1092
- . map ( Package :: is_dynamic) ;
1093
- if actual != Some ( expected) {
1094
- return Ok ( SatisfiesResult :: MismatchedDynamic ( name. clone ( ) , expected) ) ;
1095
- }
1070
+ // Validate that the member sources have not changed (e.g., that they've switched from
1071
+ // virtual to non-virtual or vice versa).
1072
+ for ( name, member) in packages {
1073
+ let expected = !member. pyproject_toml ( ) . is_package ( ) ;
1074
+ let actual = self
1075
+ . find_by_name ( name)
1076
+ . ok ( )
1077
+ . flatten ( )
1078
+ . map ( |package| matches ! ( package. id. source, Source :: Virtual ( _) ) ) ;
1079
+ if actual != Some ( expected) {
1080
+ return Ok ( SatisfiesResult :: MismatchedVirtual ( name. clone ( ) , expected) ) ;
1096
1081
}
1097
1082
}
1098
1083
@@ -1287,60 +1272,10 @@ impl Lock {
1287
1272
continue ;
1288
1273
}
1289
1274
1290
- // Fetch the metadata for the distribution.
1291
- //
1292
- // If the distribution is a source tree, attempt to extract the requirements from the
1293
- // `pyproject.toml` directly. The distribution database will do this too, but we can be
1294
- // even more aggressive here since we _only_ need the requirements. So, for example,
1295
- // even if the version is dynamic, we can still extract the requirements without
1296
- // performing a build, unlike in the database where we typically construct a "complete"
1297
- // metadata object.
1298
- let metadata = if let Some ( source_tree) = package. id . source . as_source_tree ( ) {
1299
- database
1300
- . requires_dist ( root. join ( source_tree) )
1301
- . await
1302
- . map_err ( |err| LockErrorKind :: Resolution {
1303
- id : package. id . clone ( ) ,
1304
- err,
1305
- } ) ?
1306
- } else {
1307
- None
1308
- } ;
1309
-
1310
- let satisfied = metadata. is_some_and ( |metadata| {
1311
- match satisfies_requires_dist ( metadata, package, root) {
1312
- Ok ( SatisfiesResult :: Satisfied ) => {
1313
- debug ! ( "Static `requires-dist` for `{}` is up-to-date" , package. id) ;
1314
- true
1315
- } ,
1316
- Ok ( ..) => {
1317
- debug ! ( "Static `requires-dist` for `{}` is out-of-date; falling back to distribution database" , package. id) ;
1318
- false
1319
- } ,
1320
- Err ( ..) => {
1321
- debug ! ( "Static `requires-dist` for `{}` is invalid; falling back to distribution database" , package. id) ;
1322
- false
1323
- } ,
1324
- }
1325
- } ) ;
1326
-
1327
- // If the `requires-dist` metadata matches the requirements, we're done; otherwise,
1328
- // fetch the "full" metadata, which may involve invoking the build system. In some
1329
- // cases, build backends return metadata that does _not_ match the `pyproject.toml`
1330
- // exactly. For example, `hatchling` will flatten any recursive (or self-referential)
1331
- // extras, while `setuptools` will not.
1332
- if !satisfied {
1333
- // Get the metadata for the distribution.
1334
- let dist = package. to_dist (
1335
- root,
1336
- // When validating, it's okay to use wheels that don't match the current platform.
1337
- TagPolicy :: Preferred ( tags) ,
1338
- // When validating, it's okay to use (e.g.) a source distribution with `--no-build`.
1339
- // We're just trying to determine whether the lockfile is up-to-date. If we end
1340
- // up needing to build a source distribution in order to do so, below, we'll error
1341
- // there.
1342
- & BuildOptions :: default ( ) ,
1343
- ) ?;
1275
+ if let Some ( version) = package. id . version . as_ref ( ) {
1276
+ // For a non-dynamic package, fetch the metadata from the distribution database.
1277
+ let dist =
1278
+ package. to_dist ( root, TagPolicy :: Preferred ( tags) , & BuildOptions :: default ( ) ) ?;
1344
1279
1345
1280
let metadata = {
1346
1281
let id = dist. version_id ( ) ;
@@ -1380,10 +1315,139 @@ impl Lock {
1380
1315
}
1381
1316
} ;
1382
1317
1318
+ // If this is a local package, validate that it hasn't become dynamic (in which
1319
+ // case, we'd expect the version to be omitted).
1320
+ if package. id . source . is_source_tree ( ) {
1321
+ if metadata. dynamic {
1322
+ return Ok ( SatisfiesResult :: MismatchedDynamic (
1323
+ package. id . name . clone ( ) ,
1324
+ false ,
1325
+ ) ) ;
1326
+ }
1327
+ }
1328
+
1329
+ // Validate the `version` metadata.
1330
+ if metadata. version != * version {
1331
+ return Ok ( SatisfiesResult :: MismatchedVersion (
1332
+ package. id . name . clone ( ) ,
1333
+ version. clone ( ) ,
1334
+ Some ( metadata. version . clone ( ) ) ,
1335
+ ) ) ;
1336
+ }
1337
+
1338
+ // Validate that the requirements are unchanged.
1383
1339
match satisfies_requires_dist ( RequiresDist :: from ( metadata) , package, root) ? {
1384
1340
SatisfiesResult :: Satisfied => { }
1385
1341
result => return Ok ( result) ,
1386
1342
}
1343
+ } else if let Some ( source_tree) = package. id . source . as_source_tree ( ) {
1344
+ // For dynamic packages, we don't need the version. We only need to know that the
1345
+ // package is still dynamic, and that the requirements are unchanged.
1346
+ //
1347
+ // If the distribution is a source tree, attempt to extract the requirements from the
1348
+ // `pyproject.toml` directly. The distribution database will do this too, but we can be
1349
+ // even more aggressive here since we _only_ need the requirements. So, for example,
1350
+ // even if the version is dynamic, we can still extract the requirements without
1351
+ // performing a build, unlike in the database where we typically construct a "complete"
1352
+ // metadata object.
1353
+ let metadata = database
1354
+ . requires_dist ( root. join ( source_tree) )
1355
+ . await
1356
+ . map_err ( |err| LockErrorKind :: Resolution {
1357
+ id : package. id . clone ( ) ,
1358
+ err,
1359
+ } ) ?;
1360
+
1361
+ let satisfied = metadata. is_some_and ( |metadata| {
1362
+ // Validate that the package is still dynamic.
1363
+ if !metadata. dynamic {
1364
+ debug ! ( "Static `requires-dist` for `{}` is out-of-date; falling back to distribution database" , package. id) ;
1365
+ return false ;
1366
+ }
1367
+
1368
+ // Validate that the requirements are unchanged.
1369
+ match satisfies_requires_dist ( metadata, package, root) {
1370
+ Ok ( SatisfiesResult :: Satisfied ) => {
1371
+ debug ! ( "Static `requires-dist` for `{}` is up-to-date" , package. id) ;
1372
+ true
1373
+ } ,
1374
+ Ok ( ..) => {
1375
+ debug ! ( "Static `requires-dist` for `{}` is out-of-date; falling back to distribution database" , package. id) ;
1376
+ false
1377
+ } ,
1378
+ Err ( ..) => {
1379
+ debug ! ( "Static `requires-dist` for `{}` is invalid; falling back to distribution database" , package. id) ;
1380
+ false
1381
+ } ,
1382
+ }
1383
+ } ) ;
1384
+
1385
+ // If the `requires-dist` metadata matches the requirements, we're done; otherwise,
1386
+ // fetch the "full" metadata, which may involve invoking the build system. In some
1387
+ // cases, build backends return metadata that does _not_ match the `pyproject.toml`
1388
+ // exactly. For example, `hatchling` will flatten any recursive (or self-referential)
1389
+ // extras, while `setuptools` will not.
1390
+ if !satisfied {
1391
+ let dist = package. to_dist (
1392
+ root,
1393
+ TagPolicy :: Preferred ( tags) ,
1394
+ & BuildOptions :: default ( ) ,
1395
+ ) ?;
1396
+
1397
+ let metadata = {
1398
+ let id = dist. version_id ( ) ;
1399
+ if let Some ( archive) =
1400
+ index
1401
+ . distributions ( )
1402
+ . get ( & id)
1403
+ . as_deref ( )
1404
+ . and_then ( |response| {
1405
+ if let MetadataResponse :: Found ( archive, ..) = response {
1406
+ Some ( archive)
1407
+ } else {
1408
+ None
1409
+ }
1410
+ } )
1411
+ {
1412
+ // If the metadata is already in the index, return it.
1413
+ archive. metadata . clone ( )
1414
+ } else {
1415
+ // Run the PEP 517 build process to extract metadata from the source distribution.
1416
+ let archive = database
1417
+ . get_or_build_wheel_metadata ( & dist, hasher. get ( & dist) )
1418
+ . await
1419
+ . map_err ( |err| LockErrorKind :: Resolution {
1420
+ id : package. id . clone ( ) ,
1421
+ err,
1422
+ } ) ?;
1423
+
1424
+ let metadata = archive. metadata . clone ( ) ;
1425
+
1426
+ // Insert the metadata into the index.
1427
+ index
1428
+ . distributions ( )
1429
+ . done ( id, Arc :: new ( MetadataResponse :: Found ( archive) ) ) ;
1430
+
1431
+ metadata
1432
+ }
1433
+ } ;
1434
+
1435
+ // Validate that the package is still dynamic.
1436
+ if !metadata. dynamic {
1437
+ return Ok ( SatisfiesResult :: MismatchedDynamic (
1438
+ package. id . name . clone ( ) ,
1439
+ true ,
1440
+ ) ) ;
1441
+ }
1442
+
1443
+ // Validate that the requirements are unchanged.
1444
+ match satisfies_requires_dist ( RequiresDist :: from ( metadata) , package, root) ? {
1445
+ SatisfiesResult :: Satisfied => { }
1446
+ result => return Ok ( result) ,
1447
+ }
1448
+ }
1449
+ } else {
1450
+ return Ok ( SatisfiesResult :: MissingVersion ( package. id . name . clone ( ) ) ) ;
1387
1451
}
1388
1452
1389
1453
// Recurse.
@@ -1446,7 +1510,7 @@ pub enum SatisfiesResult<'lock> {
1446
1510
MismatchedMembers ( BTreeSet < PackageName > , & ' lock BTreeSet < PackageName > ) ,
1447
1511
/// A workspace member switched from virtual to non-virtual or vice versa.
1448
1512
MismatchedVirtual ( PackageName , bool ) ,
1449
- /// A workspace member switched from dynamic to non-dynamic or vice versa.
1513
+ /// A source tree switched from dynamic to non-dynamic or vice versa.
1450
1514
MismatchedDynamic ( PackageName , bool ) ,
1451
1515
/// The lockfile uses a different set of version for its workspace members.
1452
1516
MismatchedVersion ( PackageName , Version , Option < Version > ) ,
@@ -1483,6 +1547,8 @@ pub enum SatisfiesResult<'lock> {
1483
1547
BTreeMap < GroupName , BTreeSet < Requirement > > ,
1484
1548
BTreeMap < GroupName , BTreeSet < Requirement > > ,
1485
1549
) ,
1550
+ /// The lockfile is missing a version.
1551
+ MissingVersion ( PackageName ) ,
1486
1552
}
1487
1553
1488
1554
/// We discard the lockfile if these options match.
@@ -4131,15 +4197,7 @@ fn normalize_url(mut url: Url) -> UrlString {
4131
4197
/// 2. Ensures that the lock and install paths are appropriately framed with respect to the
4132
4198
/// current [`Workspace`].
4133
4199
/// 3. Removes the `origin` field, which is only used in `requirements.txt`.
4134
- fn normalize_requirement (
4135
- mut requirement : Requirement ,
4136
- root : & Path ,
4137
- ) -> Result < Requirement , LockError > {
4138
- // Sort the extras and groups for consistency.
4139
- requirement. extras . sort ( ) ;
4140
- requirement. groups . sort ( ) ;
4141
-
4142
- // Normalize the requirement source.
4200
+ fn normalize_requirement ( requirement : Requirement , root : & Path ) -> Result < Requirement , LockError > {
4143
4201
match requirement. source {
4144
4202
RequirementSource :: Git {
4145
4203
mut repository,
0 commit comments