@@ -160,12 +160,6 @@ fn try_resolve_module_resolution_settings(
160
160
SearchPath :: vendored_stdlib ( )
161
161
} ) ;
162
162
163
- if let Some ( site_packages) = site_packages {
164
- files. try_add_root ( db. upcast ( ) , site_packages, FileRootKind :: LibrarySearchPath ) ;
165
-
166
- static_search_paths. push ( SearchPath :: site_packages ( system, site_packages. clone ( ) ) ?) ;
167
- } ;
168
-
169
163
// TODO vendor typeshed's third-party stubs as well as the stdlib and fallback to them as a final step
170
164
171
165
let target_version = program. target_version ( db. upcast ( ) ) ;
@@ -191,6 +185,7 @@ fn try_resolve_module_resolution_settings(
191
185
Ok ( ModuleResolutionSettings {
192
186
target_version,
193
187
static_search_paths,
188
+ site_packages_paths : site_packages. to_owned ( ) ,
194
189
} )
195
190
}
196
191
@@ -200,67 +195,79 @@ pub(crate) fn module_resolution_settings(db: &dyn Db) -> ModuleResolutionSetting
200
195
try_resolve_module_resolution_settings ( db) . unwrap ( )
201
196
}
202
197
203
- /// Collect all dynamic search paths:
204
- /// search paths listed in `.pth` files in the `site-packages` directory
205
- /// due to editable installations of third-party packages.
198
+ /// Collect all dynamic search paths. For each `site-packages` path:
199
+ /// - Collect that `site-packages` path
200
+ /// - Collect any search paths listed in `.pth` files in that `site-packages` directory
201
+ /// due to editable installations of third-party packages.
202
+ ///
203
+ /// The editable-install search paths for the first `site-packages` directory
204
+ /// should come between the two `site-packages` directories when it comes to
205
+ /// module-resolution priority.
206
206
#[ salsa:: tracked( return_ref) ]
207
- pub ( crate ) fn editable_install_resolution_paths ( db : & dyn Db ) -> Vec < SearchPath > {
208
- let settings = module_resolution_settings ( db) ;
209
- let static_search_paths = & settings. static_search_paths ;
210
-
211
- let site_packages = static_search_paths
212
- . iter ( )
213
- . find ( |path| path. is_site_packages ( ) ) ;
214
-
215
- let Some ( site_packages) = site_packages else {
216
- return Vec :: new ( ) ;
217
- } ;
218
-
219
- let site_packages = site_packages
220
- . as_system_path ( )
221
- . expect ( "Expected site-packages never to be a VendoredPath!" ) ;
222
-
223
- let mut dynamic_paths = Vec :: default ( ) ;
207
+ pub ( crate ) fn dynamic_resolution_paths ( db : & dyn Db ) -> Vec < SearchPath > {
208
+ let ModuleResolutionSettings {
209
+ target_version : _,
210
+ static_search_paths,
211
+ site_packages_paths,
212
+ } = module_resolution_settings ( db) ;
224
213
225
- // This query needs to be re-executed each time a `.pth` file
226
- // is added, modified or removed from the `site-packages` directory.
227
- // However, we don't use Salsa queries to read the source text of `.pth` files;
228
- // we use the APIs on the `System` trait directly. As such, add a dependency on the
229
- // site-package directory's revision.
230
- if let Some ( site_packages_root) = db. files ( ) . root ( db. upcast ( ) , site_packages) {
231
- let _ = site_packages_root. revision ( db. upcast ( ) ) ;
232
- }
214
+ let mut dynamic_paths = Vec :: new ( ) ;
233
215
234
- // As well as modules installed directly into `site-packages`,
235
- // the directory may also contain `.pth` files.
236
- // Each `.pth` file in `site-packages` may contain one or more lines
237
- // containing a (relative or absolute) path.
238
- // Each of these paths may point to an editable install of a package,
239
- // so should be considered an additional search path.
240
- let Ok ( pth_file_iterator) = PthFileIterator :: new ( db, site_packages) else {
216
+ if site_packages_paths. is_empty ( ) {
241
217
return dynamic_paths;
242
- } ;
243
-
244
- // The Python documentation specifies that `.pth` files in `site-packages`
245
- // are processed in alphabetical order, so collecting and then sorting is necessary.
246
- // https://docs.python.org/3/library/site.html#module-site
247
- let mut all_pth_files: Vec < PthFile > = pth_file_iterator. collect ( ) ;
248
- all_pth_files. sort_by ( |a, b| a. path . cmp ( & b. path ) ) ;
218
+ }
249
219
250
220
let mut existing_paths: FxHashSet < _ > = static_search_paths
251
221
. iter ( )
252
222
. filter_map ( |path| path. as_system_path ( ) )
253
223
. map ( Cow :: Borrowed )
254
224
. collect ( ) ;
255
225
256
- dynamic_paths. reserve ( all_pth_files. len ( ) ) ;
226
+ let files = db. files ( ) ;
227
+ let system = db. system ( ) ;
257
228
258
- for pth_file in & all_pth_files {
259
- for installation in pth_file. editable_installations ( ) {
260
- if existing_paths. insert ( Cow :: Owned (
261
- installation. as_system_path ( ) . unwrap ( ) . to_path_buf ( ) ,
262
- ) ) {
263
- dynamic_paths. push ( installation) ;
229
+ for site_packages_dir in site_packages_paths {
230
+ if !existing_paths. insert ( Cow :: Borrowed ( site_packages_dir) ) {
231
+ continue ;
232
+ }
233
+ let site_packages_root = files. try_add_root (
234
+ db. upcast ( ) ,
235
+ site_packages_dir,
236
+ FileRootKind :: LibrarySearchPath ,
237
+ ) ;
238
+ // This query needs to be re-executed each time a `.pth` file
239
+ // is added, modified or removed from the `site-packages` directory.
240
+ // However, we don't use Salsa queries to read the source text of `.pth` files;
241
+ // we use the APIs on the `System` trait directly. As such, add a dependency on the
242
+ // site-package directory's revision.
243
+ site_packages_root. revision ( db. upcast ( ) ) ;
244
+
245
+ dynamic_paths
246
+ . push ( SearchPath :: site_packages ( system, site_packages_dir. to_owned ( ) ) . unwrap ( ) ) ;
247
+
248
+ // As well as modules installed directly into `site-packages`,
249
+ // the directory may also contain `.pth` files.
250
+ // Each `.pth` file in `site-packages` may contain one or more lines
251
+ // containing a (relative or absolute) path.
252
+ // Each of these paths may point to an editable install of a package,
253
+ // so should be considered an additional search path.
254
+ let Ok ( pth_file_iterator) = PthFileIterator :: new ( db, site_packages_dir) else {
255
+ continue ;
256
+ } ;
257
+
258
+ // The Python documentation specifies that `.pth` files in `site-packages`
259
+ // are processed in alphabetical order, so collecting and then sorting is necessary.
260
+ // https://docs.python.org/3/library/site.html#module-site
261
+ let mut all_pth_files: Vec < PthFile > = pth_file_iterator. collect ( ) ;
262
+ all_pth_files. sort_by ( |a, b| a. path . cmp ( & b. path ) ) ;
263
+
264
+ for pth_file in & all_pth_files {
265
+ for installation in pth_file. editable_installations ( ) {
266
+ if existing_paths. insert ( Cow :: Owned (
267
+ installation. as_system_path ( ) . unwrap ( ) . to_path_buf ( ) ,
268
+ ) ) {
269
+ dynamic_paths. push ( installation) ;
270
+ }
264
271
}
265
272
}
266
273
}
@@ -293,7 +300,7 @@ impl<'db> Iterator for SearchPathIterator<'db> {
293
300
294
301
static_paths. next ( ) . or_else ( || {
295
302
dynamic_paths
296
- . get_or_insert_with ( || editable_install_resolution_paths ( * db) . iter ( ) )
303
+ . get_or_insert_with ( || dynamic_resolution_paths ( * db) . iter ( ) )
297
304
. next ( )
298
305
} )
299
306
}
@@ -403,9 +410,18 @@ impl<'db> Iterator for PthFileIterator<'db> {
403
410
#[ derive( Clone , Debug , PartialEq , Eq ) ]
404
411
pub ( crate ) struct ModuleResolutionSettings {
405
412
target_version : TargetVersion ,
413
+
406
414
/// Search paths that have been statically determined purely from reading Ruff's configuration settings.
407
415
/// These shouldn't ever change unless the config settings themselves change.
408
416
static_search_paths : Vec < SearchPath > ,
417
+
418
+ /// site-packages paths are not included in the above field:
419
+ /// if there are multiple site-packages paths, editable installations can appear
420
+ /// *between* the site-packages paths on `sys.path` at runtime.
421
+ /// That means we can't know where a second or third `site-packages` path should sit
422
+ /// in terms of module-resolution priority until we've discovered the editable installs
423
+ /// for the first `site-packages` path
424
+ site_packages_paths : Vec < SystemPathBuf > ,
409
425
}
410
426
411
427
impl ModuleResolutionSettings {
@@ -630,6 +646,7 @@ mod tests {
630
646
} ;
631
647
use ruff_db:: Db ;
632
648
649
+ use crate :: db:: tests:: TestDb ;
633
650
use crate :: module:: ModuleKind ;
634
651
use crate :: module_name:: ModuleName ;
635
652
use crate :: testing:: { FileSpec , MockedTypeshed , TestCase , TestCaseBuilder } ;
@@ -1180,7 +1197,7 @@ mod tests {
1180
1197
extra_paths : vec ! [ ] ,
1181
1198
workspace_root : src. clone ( ) ,
1182
1199
custom_typeshed : Some ( custom_typeshed. clone ( ) ) ,
1183
- site_packages : Some ( site_packages. clone ( ) ) ,
1200
+ site_packages : vec ! [ site_packages] ,
1184
1201
} ;
1185
1202
1186
1203
Program :: new ( & db, TargetVersion :: Py38 , search_paths) ;
@@ -1578,7 +1595,7 @@ not_a_directory
1578
1595
& FilePath :: system( "/y/src/bar.py" )
1579
1596
) ;
1580
1597
let events = db. take_salsa_events ( ) ;
1581
- assert_const_function_query_was_not_run ( & db, editable_install_resolution_paths , & events) ;
1598
+ assert_const_function_query_was_not_run ( & db, dynamic_resolution_paths , & events) ;
1582
1599
}
1583
1600
1584
1601
#[ test]
@@ -1656,4 +1673,53 @@ not_a_directory
1656
1673
assert ! ( !search_paths
1657
1674
. contains( &&SearchPath :: editable( db. system( ) , SystemPathBuf :: from( "/src" ) ) . unwrap( ) ) ) ;
1658
1675
}
1676
+
1677
+ #[ test]
1678
+ fn multiple_site_packages_with_editables ( ) {
1679
+ let mut db = TestDb :: new ( ) ;
1680
+
1681
+ let venv_site_packages = SystemPathBuf :: from ( "/venv-site-packages" ) ;
1682
+ let site_packages_pth = venv_site_packages. join ( "foo.pth" ) ;
1683
+ let system_site_packages = SystemPathBuf :: from ( "/system-site-packages" ) ;
1684
+ let editable_install_location = SystemPathBuf :: from ( "/x/y/a.py" ) ;
1685
+ let system_site_packages_location = system_site_packages. join ( "a.py" ) ;
1686
+
1687
+ db. memory_file_system ( )
1688
+ . create_directory_all ( "/src" )
1689
+ . unwrap ( ) ;
1690
+ db. write_files ( [
1691
+ ( & site_packages_pth, "/x/y" ) ,
1692
+ ( & editable_install_location, "" ) ,
1693
+ ( & system_site_packages_location, "" ) ,
1694
+ ] )
1695
+ . unwrap ( ) ;
1696
+
1697
+ Program :: new (
1698
+ & db,
1699
+ TargetVersion :: default ( ) ,
1700
+ SearchPathSettings {
1701
+ extra_paths : vec ! [ ] ,
1702
+ workspace_root : SystemPathBuf :: from ( "/src" ) ,
1703
+ custom_typeshed : None ,
1704
+ site_packages : vec ! [ venv_site_packages, system_site_packages] ,
1705
+ } ,
1706
+ ) ;
1707
+
1708
+ // The editable installs discovered from the `.pth` file in the first `site-packages` directory
1709
+ // take precedence over the second `site-packages` directory...
1710
+ let a_module_name = ModuleName :: new_static ( "a" ) . unwrap ( ) ;
1711
+ let a_module = resolve_module ( & db, a_module_name. clone ( ) ) . unwrap ( ) ;
1712
+ assert_eq ! ( a_module. file( ) . path( & db) , & editable_install_location) ;
1713
+
1714
+ db. memory_file_system ( )
1715
+ . remove_file ( & site_packages_pth)
1716
+ . unwrap ( ) ;
1717
+ File :: sync_path ( & mut db, & site_packages_pth) ;
1718
+
1719
+ // ...But now that the `.pth` file in the first `site-packages` directory has been deleted,
1720
+ // the editable install no longer exists, so the module now resolves to the file in the
1721
+ // second `site-packages` directory
1722
+ let a_module = resolve_module ( & db, a_module_name) . unwrap ( ) ;
1723
+ assert_eq ! ( a_module. file( ) . path( & db) , & system_site_packages_location) ;
1724
+ }
1659
1725
}
0 commit comments