@@ -14,6 +14,7 @@ use std::io;
1414use std:: num:: NonZeroUsize ;
1515use std:: ops:: Deref ;
1616
17+ use indexmap:: IndexSet ;
1718use ruff_db:: system:: { System , SystemPath , SystemPathBuf } ;
1819use ruff_python_ast:: PythonVersion ;
1920
@@ -35,7 +36,7 @@ impl PythonEnvironment {
3536
3637 // Attempt to inspect as a virtual environment first
3738 // TODO(zanieb): Consider avoiding the clone here by checking for `pyvenv.cfg` ahead-of-time
38- match VirtualEnvironment :: new ( path. clone ( ) , origin , system) {
39+ match VirtualEnvironment :: new ( path. clone ( ) , system) {
3940 Ok ( venv) => Ok ( Self :: Virtual ( venv) ) ,
4041 // If there's not a `pyvenv.cfg` marker, attempt to inspect as a system environment
4142 //
@@ -54,7 +55,7 @@ impl PythonEnvironment {
5455 pub ( crate ) fn site_packages_directories (
5556 & self ,
5657 system : & dyn System ,
57- ) -> SitePackagesDiscoveryResult < Vec < SystemPathBuf > > {
58+ ) -> SitePackagesDiscoveryResult < IndexSet < SystemPathBuf > > {
5859 match self {
5960 Self :: Virtual ( env) => env. site_packages_directories ( system) ,
6061 Self :: System ( env) => env. site_packages_directories ( system) ,
@@ -111,12 +112,19 @@ pub(crate) struct VirtualEnvironment {
111112 /// This field will be `None` if so.
112113 version : Option < PythonVersion > ,
113114 implementation : PythonImplementation ,
115+
116+ /// If this virtual environment was created using uv,
117+ /// it may be an "ephemeral" virtual environment that dynamically adds the `site-packages`
118+ /// directories of its parent environment to `sys.path` at runtime.
119+ /// Newer versions of uv record the parent environment in the `pyvenv.cfg` file;
120+ /// we'll want to add the `site-packages` directories of the parent environment
121+ /// as search paths as well as the `site-packages` directories of this virtual environment.
122+ parent_environment : Option < Box < VirtualEnvironment > > ,
114123}
115124
116125impl VirtualEnvironment {
117126 pub ( crate ) fn new (
118127 path : SysPrefixPath ,
119- origin : SysPrefixPathOrigin ,
120128 system : & dyn System ,
121129 ) -> SitePackagesDiscoveryResult < Self > {
122130 fn pyvenv_cfg_line_number ( index : usize ) -> NonZeroUsize {
@@ -128,12 +136,14 @@ impl VirtualEnvironment {
128136
129137 let pyvenv_cfg = system
130138 . read_to_string ( & pyvenv_cfg_path)
131- . map_err ( |io_err| SitePackagesDiscoveryError :: NoPyvenvCfgFile ( origin, io_err) ) ?;
139+ . map_err ( |io_err| SitePackagesDiscoveryError :: NoPyvenvCfgFile ( path . origin , io_err) ) ?;
132140
133141 let mut include_system_site_packages = false ;
134142 let mut base_executable_home_path = None ;
135143 let mut version_info_string = None ;
136144 let mut implementation = PythonImplementation :: Unknown ;
145+ let mut created_with_uv = false ;
146+ let mut parent_environment = None ;
137147
138148 // A `pyvenv.cfg` file *looks* like a `.ini` file, but actually isn't valid `.ini` syntax!
139149 // The Python standard-library's `site` module parses these files by splitting each line on
@@ -178,6 +188,8 @@ impl VirtualEnvironment {
178188 _ => PythonImplementation :: Unknown ,
179189 } ;
180190 }
191+ "uv" => created_with_uv = true ,
192+ "extends-environment" => parent_environment = Some ( value) ,
181193 _ => continue ,
182194 }
183195 }
@@ -196,11 +208,35 @@ impl VirtualEnvironment {
196208 let base_executable_home_path = PythonHomePath :: new ( base_executable_home_path, system)
197209 . map_err ( |io_err| {
198210 SitePackagesDiscoveryError :: PyvenvCfgParseError (
199- pyvenv_cfg_path,
211+ pyvenv_cfg_path. clone ( ) ,
200212 PyvenvCfgParseErrorKind :: InvalidHomeValue ( io_err) ,
201213 )
202214 } ) ?;
203215
216+ let parent_environment = if created_with_uv {
217+ parent_environment
218+ . and_then ( |sys_prefix| {
219+ SysPrefixPath :: new (
220+ sys_prefix,
221+ SysPrefixPathOrigin :: DerivedFromPyvenvCfg ,
222+ system,
223+ )
224+ . and_then ( |sys_prefix|Self :: new ( sys_prefix, system) )
225+ . inspect_err ( |err| {
226+ tracing:: warn!(
227+ "Failed to resolve the parent virtual environment of this ephemeral uv virtual environment \
228+ from the `extends-environment` value specified in the `pyvenv.cfg` file at {pyvenv_cfg_path}. \
229+ Packages installed into the parent environment will not be resolved correctly. \
230+ Underlying error: {err}",
231+ ) ;
232+ } )
233+ . ok ( )
234+ } )
235+ . map ( Box :: new)
236+ } else {
237+ None
238+ } ;
239+
204240 // but the `version`/`version_info` key is not read by the standard library,
205241 // and is provided under different keys depending on which virtual-environment creation tool
206242 // created the `pyvenv.cfg` file. Lenient parsing is appropriate here:
@@ -218,6 +254,7 @@ impl VirtualEnvironment {
218254 include_system_site_packages,
219255 version,
220256 implementation,
257+ parent_environment,
221258 } ;
222259
223260 tracing:: trace!( "Resolved metadata for virtual environment: {metadata:?}" ) ;
@@ -230,21 +267,38 @@ impl VirtualEnvironment {
230267 pub ( crate ) fn site_packages_directories (
231268 & self ,
232269 system : & dyn System ,
233- ) -> SitePackagesDiscoveryResult < Vec < SystemPathBuf > > {
270+ ) -> SitePackagesDiscoveryResult < IndexSet < SystemPathBuf > > {
234271 let VirtualEnvironment {
235272 root_path,
236273 base_executable_home_path,
237274 include_system_site_packages,
238275 implementation,
239276 version,
277+ parent_environment,
240278 } = self ;
241279
242- let mut site_packages_directories = vec ! [ site_packages_directory_from_sys_prefix(
243- root_path,
244- * version,
245- * implementation,
246- system,
247- ) ?] ;
280+ let mut site_packages_directories =
281+ IndexSet :: from ( [ site_packages_directory_from_sys_prefix (
282+ root_path,
283+ * version,
284+ * implementation,
285+ system,
286+ ) ?] ) ;
287+
288+ if let Some ( parent_env_site_packages) = parent_environment. as_deref ( ) {
289+ match parent_env_site_packages. site_packages_directories ( system) {
290+ Ok ( parent_environment_site_packages) => {
291+ site_packages_directories. extend ( parent_environment_site_packages) ;
292+ }
293+ Err ( err) => {
294+ tracing:: warn!(
295+ "Failed to resolve the site-packages directories of this ephemeral uv virtual environment's \
296+ parent environment. Packages installed into the parent environment will not be resolved correctly. \
297+ Underlying error: {err}"
298+ ) ;
299+ }
300+ }
301+ }
248302
249303 if * include_system_site_packages {
250304 let system_sys_prefix =
@@ -261,7 +315,7 @@ impl VirtualEnvironment {
261315 system,
262316 ) {
263317 Ok ( site_packages_directory) => {
264- site_packages_directories. push ( site_packages_directory) ;
318+ site_packages_directories. insert ( site_packages_directory) ;
265319 }
266320 Err ( error) => tracing:: warn!(
267321 "{error}. System site-packages will not be used for module resolution."
@@ -309,15 +363,15 @@ impl SystemEnvironment {
309363 pub ( crate ) fn site_packages_directories (
310364 & self ,
311365 system : & dyn System ,
312- ) -> SitePackagesDiscoveryResult < Vec < SystemPathBuf > > {
366+ ) -> SitePackagesDiscoveryResult < IndexSet < SystemPathBuf > > {
313367 let SystemEnvironment { root_path } = self ;
314368
315- let site_packages_directories = vec ! [ site_packages_directory_from_sys_prefix(
369+ let site_packages_directories = IndexSet :: from ( [ site_packages_directory_from_sys_prefix (
316370 root_path,
317371 None ,
318372 PythonImplementation :: Unknown ,
319373 system,
320- ) ?] ;
374+ ) ?] ) ;
321375
322376 tracing:: debug!(
323377 "Resolved site-packages directories for this environment are: {site_packages_directories:?}"
@@ -550,12 +604,12 @@ impl SysPrefixPath {
550604 if cfg ! ( target_os = "windows" ) {
551605 Some ( Self {
552606 inner : path. to_path_buf ( ) ,
553- origin : SysPrefixPathOrigin :: Derived ,
607+ origin : SysPrefixPathOrigin :: DerivedFromPyvenvCfg ,
554608 } )
555609 } else {
556610 path. parent ( ) . map ( |path| Self {
557611 inner : path. to_path_buf ( ) ,
558- origin : SysPrefixPathOrigin :: Derived ,
612+ origin : SysPrefixPathOrigin :: DerivedFromPyvenvCfg ,
559613 } )
560614 }
561615 }
@@ -575,13 +629,22 @@ impl fmt::Display for SysPrefixPath {
575629 }
576630}
577631
632+ /// Enumeration of sources a `sys.prefix` path can come from.
578633#[ derive( Debug , PartialEq , Eq , Copy , Clone ) ]
579634#[ cfg_attr( feature = "serde" , derive( serde:: Serialize , serde:: Deserialize ) ) ]
580635pub enum SysPrefixPathOrigin {
636+ /// The `sys.prefix` path came from a `--python` CLI flag
581637 PythonCliFlag ,
638+ /// The `sys.prefix` path came from the `VIRTUAL_ENV` environment variable
582639 VirtualEnvVar ,
640+ /// The `sys.prefix` path came from the `CONDA_PREFIX` environment variable
583641 CondaPrefixVar ,
584- Derived ,
642+ /// The `sys.prefix` path was derived from a value in a `pyvenv.cfg` file:
643+ /// either the value associated with the `home` key
644+ /// or the value associated with the `extends-environment` key
645+ DerivedFromPyvenvCfg ,
646+ /// A `.venv` directory was found in the current working directory,
647+ /// and the `sys.prefix` path is the path to that virtual environment.
585648 LocalVenv ,
586649}
587650
@@ -591,7 +654,7 @@ impl SysPrefixPathOrigin {
591654 pub ( crate ) fn must_be_virtual_env ( self ) -> bool {
592655 match self {
593656 Self :: LocalVenv | Self :: VirtualEnvVar => true ,
594- Self :: PythonCliFlag | Self :: Derived | Self :: CondaPrefixVar => false ,
657+ Self :: PythonCliFlag | Self :: DerivedFromPyvenvCfg | Self :: CondaPrefixVar => false ,
595658 }
596659 }
597660}
@@ -602,7 +665,7 @@ impl Display for SysPrefixPathOrigin {
602665 Self :: PythonCliFlag => f. write_str ( "`--python` argument" ) ,
603666 Self :: VirtualEnvVar => f. write_str ( "`VIRTUAL_ENV` environment variable" ) ,
604667 Self :: CondaPrefixVar => f. write_str ( "`CONDA_PREFIX` environment variable" ) ,
605- Self :: Derived => f. write_str ( "derived `sys.prefix` path" ) ,
668+ Self :: DerivedFromPyvenvCfg => f. write_str ( "derived `sys.prefix` path" ) ,
606669 Self :: LocalVenv => f. write_str ( "local virtual environment" ) ,
607670 }
608671 }
@@ -901,11 +964,14 @@ mod tests {
901964
902965 if self_venv. system_site_packages {
903966 assert_eq ! (
904- & site_packages_directories,
967+ site_packages_directories. as_slice ( ) ,
905968 & [ expected_venv_site_packages, expected_system_site_packages]
906969 ) ;
907970 } else {
908- assert_eq ! ( & site_packages_directories, & [ expected_venv_site_packages] ) ;
971+ assert_eq ! (
972+ site_packages_directories. as_slice( ) ,
973+ & [ expected_venv_site_packages]
974+ ) ;
909975 }
910976 }
911977
@@ -946,7 +1012,10 @@ mod tests {
9461012 ) )
9471013 } ;
9481014
949- assert_eq ! ( & site_packages_directories, & [ expected_site_packages] ) ;
1015+ assert_eq ! (
1016+ site_packages_directories. as_slice( ) ,
1017+ & [ expected_site_packages]
1018+ ) ;
9501019 }
9511020 }
9521021
0 commit comments