Skip to content

Commit cdafd8e

Browse files
Allow discovery of venv in VIRTUAL_ENV env variable (#16853)
## Summary Fixes #16744 Allows the cli to find a virtual environment from the VIRTUAL_ENV environment variable if no `--python` is set ## Test Plan Manual testing, of: - Virtual environments explicitly activated using `source .venv/bin/activate` - Virtual environments implicilty activated via `uv run` - Broken virtual environments with no `pyvenv.cfg` file
1 parent 1272594 commit cdafd8e

File tree

5 files changed

+111
-34
lines changed

5 files changed

+111
-34
lines changed

crates/red_knot/src/args.rs

+2
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ pub(crate) struct CheckCommand {
5050

5151
/// Path to the Python installation from which Red Knot resolves type information and third-party dependencies.
5252
///
53+
/// If not specified, Red Knot will look at the `VIRTUAL_ENV` environment variable.
54+
///
5355
/// Red Knot will search in the path's `site-packages` directories for type information and
5456
/// third-party imports.
5557
///

crates/red_knot_project/src/metadata/options.rs

+7-2
Original file line numberDiff line numberDiff line change
@@ -106,9 +106,14 @@ impl Options {
106106
custom_typeshed: typeshed.map(|path| path.absolute(project_root, system)),
107107
python_path: python
108108
.map(|python_path| {
109-
PythonPath::SysPrefix(python_path.absolute(project_root, system))
109+
PythonPath::from_cli_flag(python_path.absolute(project_root, system))
110110
})
111-
.unwrap_or(PythonPath::KnownSitePackages(vec![])),
111+
.or_else(|| {
112+
std::env::var("VIRTUAL_ENV")
113+
.ok()
114+
.map(PythonPath::from_virtual_env_var)
115+
})
116+
.unwrap_or_else(|| PythonPath::KnownSitePackages(vec![])),
112117
}
113118
}
114119

crates/red_knot_python_semantic/src/module_resolver/resolver.rs

+3-2
Original file line numberDiff line numberDiff line change
@@ -223,14 +223,15 @@ impl SearchPaths {
223223
static_paths.push(stdlib_path);
224224

225225
let site_packages_paths = match python_path {
226-
PythonPath::SysPrefix(sys_prefix) => {
226+
PythonPath::SysPrefix(sys_prefix, origin) => {
227227
// TODO: We may want to warn here if the venv's python version is older
228228
// than the one resolved in the program settings because it indicates
229229
// that the `target-version` is incorrectly configured or that the
230230
// venv is out of date.
231-
VirtualEnvironment::new(sys_prefix, system)
231+
VirtualEnvironment::new(sys_prefix, *origin, system)
232232
.and_then(|venv| venv.site_packages_directories(system))?
233233
}
234+
234235
PythonPath::KnownSitePackages(paths) => paths
235236
.iter()
236237
.map(|path| canonicalize(path, system))

crates/red_knot_python_semantic/src/program.rs

+12-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use crate::module_resolver::SearchPaths;
22
use crate::python_platform::PythonPlatform;
3+
use crate::site_packages::SysPrefixPathOrigin;
34
use crate::Db;
45

56
use anyhow::Context;
@@ -142,11 +143,21 @@ pub enum PythonPath {
142143
/// `/opt/homebrew/lib/python3.X/site-packages`.
143144
///
144145
/// [`sys.prefix`]: https://docs.python.org/3/library/sys.html#sys.prefix
145-
SysPrefix(SystemPathBuf),
146+
SysPrefix(SystemPathBuf, SysPrefixPathOrigin),
146147

147148
/// Resolved site packages paths.
148149
///
149150
/// This variant is mainly intended for testing where we want to skip resolving `site-packages`
150151
/// because it would unnecessarily complicate the test setup.
151152
KnownSitePackages(Vec<SystemPathBuf>),
152153
}
154+
155+
impl PythonPath {
156+
pub fn from_virtual_env_var(path: impl Into<SystemPathBuf>) -> Self {
157+
Self::SysPrefix(path.into(), SysPrefixPathOrigin::VirtualEnvVar)
158+
}
159+
160+
pub fn from_cli_flag(path: SystemPathBuf) -> Self {
161+
Self::SysPrefix(path, SysPrefixPathOrigin::PythonCliFlag)
162+
}
163+
}

crates/red_knot_python_semantic/src/site_packages.rs

+87-29
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
//! on Linux.)
1010
1111
use std::fmt;
12+
use std::fmt::Display;
1213
use std::io;
1314
use std::num::NonZeroUsize;
1415
use std::ops::Deref;
@@ -43,23 +44,28 @@ pub(crate) struct VirtualEnvironment {
4344
impl VirtualEnvironment {
4445
pub(crate) fn new(
4546
path: impl AsRef<SystemPath>,
47+
origin: SysPrefixPathOrigin,
4648
system: &dyn System,
4749
) -> SitePackagesDiscoveryResult<Self> {
48-
Self::new_impl(path.as_ref(), system)
50+
Self::new_impl(path.as_ref(), origin, system)
4951
}
5052

51-
fn new_impl(path: &SystemPath, system: &dyn System) -> SitePackagesDiscoveryResult<Self> {
53+
fn new_impl(
54+
path: &SystemPath,
55+
origin: SysPrefixPathOrigin,
56+
system: &dyn System,
57+
) -> SitePackagesDiscoveryResult<Self> {
5258
fn pyvenv_cfg_line_number(index: usize) -> NonZeroUsize {
5359
index.checked_add(1).and_then(NonZeroUsize::new).unwrap()
5460
}
5561

56-
let venv_path = SysPrefixPath::new(path, system)?;
62+
let venv_path = SysPrefixPath::new(path, origin, system)?;
5763
let pyvenv_cfg_path = venv_path.join("pyvenv.cfg");
5864
tracing::debug!("Attempting to parse virtual environment metadata at '{pyvenv_cfg_path}'");
5965

6066
let pyvenv_cfg = system
6167
.read_to_string(&pyvenv_cfg_path)
62-
.map_err(SitePackagesDiscoveryError::NoPyvenvCfgFile)?;
68+
.map_err(|io_err| SitePackagesDiscoveryError::NoPyvenvCfgFile(origin, io_err))?;
6369

6470
let mut include_system_site_packages = false;
6571
let mut base_executable_home_path = None;
@@ -205,12 +211,12 @@ System site-packages will not be used for module resolution.",
205211

206212
#[derive(Debug, thiserror::Error)]
207213
pub(crate) enum SitePackagesDiscoveryError {
208-
#[error("Invalid --python argument: `{0}` could not be canonicalized")]
209-
VenvDirCanonicalizationError(SystemPathBuf, #[source] io::Error),
210-
#[error("Invalid --python argument: `{0}` does not point to a directory on disk")]
211-
VenvDirIsNotADirectory(SystemPathBuf),
212-
#[error("--python points to a broken venv with no pyvenv.cfg file")]
213-
NoPyvenvCfgFile(#[source] io::Error),
214+
#[error("Invalid {1}: `{0}` could not be canonicalized")]
215+
VenvDirCanonicalizationError(SystemPathBuf, SysPrefixPathOrigin, #[source] io::Error),
216+
#[error("Invalid {1}: `{0}` does not point to a directory on disk")]
217+
VenvDirIsNotADirectory(SystemPathBuf, SysPrefixPathOrigin),
218+
#[error("{0} points to a broken venv with no pyvenv.cfg file")]
219+
NoPyvenvCfgFile(SysPrefixPathOrigin, #[source] io::Error),
214220
#[error("Failed to parse the pyvenv.cfg file at {0} because {1}")]
215221
PyvenvCfgParseError(SystemPathBuf, PyvenvCfgParseErrorKind),
216222
#[error("Failed to search the `lib` directory of the Python installation at {1} for `site-packages`")]
@@ -370,18 +376,23 @@ fn site_packages_directory_from_sys_prefix(
370376
///
371377
/// [`sys.prefix`]: https://docs.python.org/3/library/sys.html#sys.prefix
372378
#[derive(Debug, PartialEq, Eq, Clone)]
373-
pub(crate) struct SysPrefixPath(SystemPathBuf);
379+
pub(crate) struct SysPrefixPath {
380+
inner: SystemPathBuf,
381+
origin: SysPrefixPathOrigin,
382+
}
374383

375384
impl SysPrefixPath {
376385
fn new(
377386
unvalidated_path: impl AsRef<SystemPath>,
387+
origin: SysPrefixPathOrigin,
378388
system: &dyn System,
379389
) -> SitePackagesDiscoveryResult<Self> {
380-
Self::new_impl(unvalidated_path.as_ref(), system)
390+
Self::new_impl(unvalidated_path.as_ref(), origin, system)
381391
}
382392

383393
fn new_impl(
384394
unvalidated_path: &SystemPath,
395+
origin: SysPrefixPathOrigin,
385396
system: &dyn System,
386397
) -> SitePackagesDiscoveryResult<Self> {
387398
// It's important to resolve symlinks here rather than simply making the path absolute,
@@ -392,14 +403,21 @@ impl SysPrefixPath {
392403
.map_err(|io_err| {
393404
SitePackagesDiscoveryError::VenvDirCanonicalizationError(
394405
unvalidated_path.to_path_buf(),
406+
origin,
395407
io_err,
396408
)
397409
})?;
398410
system
399411
.is_directory(&canonicalized)
400-
.then_some(Self(canonicalized))
412+
.then_some(Self {
413+
inner: canonicalized,
414+
origin,
415+
})
401416
.ok_or_else(|| {
402-
SitePackagesDiscoveryError::VenvDirIsNotADirectory(unvalidated_path.to_path_buf())
417+
SitePackagesDiscoveryError::VenvDirIsNotADirectory(
418+
unvalidated_path.to_path_buf(),
419+
origin,
420+
)
403421
})
404422
}
405423

@@ -408,9 +426,15 @@ impl SysPrefixPath {
408426
// the parent of a canonicalised path that is known to exist
409427
// is guaranteed to be a directory.
410428
if cfg!(target_os = "windows") {
411-
Some(Self(path.to_path_buf()))
429+
Some(Self {
430+
inner: path.to_path_buf(),
431+
origin: SysPrefixPathOrigin::Derived,
432+
})
412433
} else {
413-
path.parent().map(|path| Self(path.to_path_buf()))
434+
path.parent().map(|path| Self {
435+
inner: path.to_path_buf(),
436+
origin: SysPrefixPathOrigin::Derived,
437+
})
414438
}
415439
}
416440
}
@@ -419,13 +443,31 @@ impl Deref for SysPrefixPath {
419443
type Target = SystemPath;
420444

421445
fn deref(&self) -> &Self::Target {
422-
&self.0
446+
&self.inner
423447
}
424448
}
425449

426450
impl fmt::Display for SysPrefixPath {
427451
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
428-
write!(f, "`sys.prefix` path `{}`", self.0)
452+
write!(f, "`sys.prefix` path `{}`", self.inner)
453+
}
454+
}
455+
456+
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
457+
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
458+
pub enum SysPrefixPathOrigin {
459+
PythonCliFlag,
460+
VirtualEnvVar,
461+
Derived,
462+
}
463+
464+
impl Display for SysPrefixPathOrigin {
465+
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
466+
match self {
467+
Self::PythonCliFlag => f.write_str("`--python` argument"),
468+
Self::VirtualEnvVar => f.write_str("`VIRTUAL_ENV` environment variable"),
469+
Self::Derived => f.write_str("derived `sys.prefix` path"),
470+
}
429471
}
430472
}
431473

@@ -584,11 +626,19 @@ mod tests {
584626

585627
fn test(self) {
586628
let venv_path = self.build_mock_venv();
587-
let venv = VirtualEnvironment::new(venv_path.clone(), &self.system).unwrap();
629+
let venv = VirtualEnvironment::new(
630+
venv_path.clone(),
631+
SysPrefixPathOrigin::VirtualEnvVar,
632+
&self.system,
633+
)
634+
.unwrap();
588635

589636
assert_eq!(
590637
venv.venv_path,
591-
SysPrefixPath(self.system.canonicalize_path(&venv_path).unwrap())
638+
SysPrefixPath {
639+
inner: self.system.canonicalize_path(&venv_path).unwrap(),
640+
origin: SysPrefixPathOrigin::VirtualEnvVar,
641+
}
592642
);
593643
assert_eq!(venv.include_system_site_packages, self.system_site_packages);
594644

@@ -730,7 +780,7 @@ mod tests {
730780
fn reject_venv_that_does_not_exist() {
731781
let system = TestSystem::default();
732782
assert!(matches!(
733-
VirtualEnvironment::new("/.venv", &system),
783+
VirtualEnvironment::new("/.venv", SysPrefixPathOrigin::VirtualEnvVar, &system),
734784
Err(SitePackagesDiscoveryError::VenvDirCanonicalizationError(..))
735785
));
736786
}
@@ -743,7 +793,7 @@ mod tests {
743793
.write_file_all("/.venv", "")
744794
.unwrap();
745795
assert!(matches!(
746-
VirtualEnvironment::new("/.venv", &system),
796+
VirtualEnvironment::new("/.venv", SysPrefixPathOrigin::VirtualEnvVar, &system),
747797
Err(SitePackagesDiscoveryError::VenvDirIsNotADirectory(..))
748798
));
749799
}
@@ -756,8 +806,11 @@ mod tests {
756806
.create_directory_all("/.venv")
757807
.unwrap();
758808
assert!(matches!(
759-
VirtualEnvironment::new("/.venv", &system),
760-
Err(SitePackagesDiscoveryError::NoPyvenvCfgFile(_))
809+
VirtualEnvironment::new("/.venv", SysPrefixPathOrigin::VirtualEnvVar, &system),
810+
Err(SitePackagesDiscoveryError::NoPyvenvCfgFile(
811+
SysPrefixPathOrigin::VirtualEnvVar,
812+
_
813+
))
761814
));
762815
}
763816

@@ -769,7 +822,8 @@ mod tests {
769822
memory_fs
770823
.write_file_all(&pyvenv_cfg_path, "home = bar = /.venv/bin")
771824
.unwrap();
772-
let venv_result = VirtualEnvironment::new("/.venv", &system);
825+
let venv_result =
826+
VirtualEnvironment::new("/.venv", SysPrefixPathOrigin::VirtualEnvVar, &system);
773827
assert!(matches!(
774828
venv_result,
775829
Err(SitePackagesDiscoveryError::PyvenvCfgParseError(
@@ -788,7 +842,8 @@ mod tests {
788842
memory_fs
789843
.write_file_all(&pyvenv_cfg_path, "home =")
790844
.unwrap();
791-
let venv_result = VirtualEnvironment::new("/.venv", &system);
845+
let venv_result =
846+
VirtualEnvironment::new("/.venv", SysPrefixPathOrigin::VirtualEnvVar, &system);
792847
assert!(matches!(
793848
venv_result,
794849
Err(SitePackagesDiscoveryError::PyvenvCfgParseError(
@@ -807,7 +862,8 @@ mod tests {
807862
memory_fs
808863
.write_file_all(&pyvenv_cfg_path, "= whatever")
809864
.unwrap();
810-
let venv_result = VirtualEnvironment::new("/.venv", &system);
865+
let venv_result =
866+
VirtualEnvironment::new("/.venv", SysPrefixPathOrigin::VirtualEnvVar, &system);
811867
assert!(matches!(
812868
venv_result,
813869
Err(SitePackagesDiscoveryError::PyvenvCfgParseError(
@@ -824,7 +880,8 @@ mod tests {
824880
let memory_fs = system.memory_file_system();
825881
let pyvenv_cfg_path = SystemPathBuf::from("/.venv/pyvenv.cfg");
826882
memory_fs.write_file_all(&pyvenv_cfg_path, "").unwrap();
827-
let venv_result = VirtualEnvironment::new("/.venv", &system);
883+
let venv_result =
884+
VirtualEnvironment::new("/.venv", SysPrefixPathOrigin::VirtualEnvVar, &system);
828885
assert!(matches!(
829886
venv_result,
830887
Err(SitePackagesDiscoveryError::PyvenvCfgParseError(
@@ -843,7 +900,8 @@ mod tests {
843900
memory_fs
844901
.write_file_all(&pyvenv_cfg_path, "home = foo")
845902
.unwrap();
846-
let venv_result = VirtualEnvironment::new("/.venv", &system);
903+
let venv_result =
904+
VirtualEnvironment::new("/.venv", SysPrefixPathOrigin::VirtualEnvVar, &system);
847905
assert!(matches!(
848906
venv_result,
849907
Err(SitePackagesDiscoveryError::PyvenvCfgParseError(

0 commit comments

Comments
 (0)