## Summary We use the `System` abstraction in ty to abstract away the host/system on which ty runs. This has a few benefits: * Tests can run in full isolation using a memory system (that uses an in-memory file system) * The LSP has a custom implementation where `read_to_string` returns the content as seen by the editor (e.g. unsaved changes) instead of always returning the content as it is stored on disk * We don't require any file system polyfills for wasm in the browser However, it does require extra care that we don't accidentally use `std::fs` or `std::env` (etc.) methods in ty's code base (which is very easy). This PR sets up Clippy and disallows the most common methods, instead pointing users towards the corresponding `System` methods. The setup is a bit awkward because clippy doesn't support inheriting configurations. That means, a crate can only override the entire workspace configuration or not at all. The approach taken in this PR is: * Configure the disallowed methods at the workspace level * Allow `disallowed_methods` at the workspace level * Enable the lint at the crate level using the warn attribute (in code) The obvious downside is that it won't work if we ever want to disallow other methods, but we can figure that out once we reach that point. What about false positives: Just add an `allow` and move on with your life :) This isn't something that we have to enforce strictly; the goal is to catch accidental misuse. ## Test Plan Clippy found a place where we incorrectly used `std::fs::read_to_string`
2365 lines
89 KiB
Rust
2365 lines
89 KiB
Rust
//! Utilities for finding the `site-packages` directory,
|
|
//! into which third-party packages are installed.
|
|
//!
|
|
//! The routines exposed by this module have different behaviour depending
|
|
//! on the platform of the *host machine*, which may be
|
|
//! different from the *target platform for type checking*. (A user
|
|
//! might be running ty on a Windows machine, but might
|
|
//! reasonably ask us to type-check code assuming that the code runs
|
|
//! on Linux.)
|
|
|
|
use std::io;
|
|
use std::num::NonZeroUsize;
|
|
use std::ops::Deref;
|
|
use std::str::FromStr;
|
|
use std::{fmt, sync::Arc};
|
|
|
|
use crate::{PythonVersionFileSource, PythonVersionSource, PythonVersionWithSource};
|
|
use camino::Utf8Component;
|
|
use indexmap::IndexSet;
|
|
use ruff_annotate_snippets::{Level, Renderer, Snippet};
|
|
use ruff_db::system::{System, SystemPath, SystemPathBuf};
|
|
use ruff_python_ast::PythonVersion;
|
|
use ruff_python_trivia::Cursor;
|
|
use ruff_source_file::{LineIndex, OneIndexed, SourceCode};
|
|
use ruff_text_size::{TextLen, TextRange};
|
|
use strum::IntoEnumIterator;
|
|
use ty_static::EnvVars;
|
|
|
|
type SitePackagesDiscoveryResult<T> = Result<T, SitePackagesDiscoveryError>;
|
|
type StdlibDiscoveryResult<T> = Result<T, StdlibDiscoveryError>;
|
|
|
|
/// An ordered, deduplicated set of `site-packages` search paths.
|
|
///
|
|
/// Most environments will only have one `site-packages` directory.
|
|
/// Some virtual environments created with `--system-site-packages`
|
|
/// will also have the system installation's `site-packages` packages
|
|
/// available, however. Ephemeral environments created with `uv` in
|
|
/// `uv run --with` invocations, meanwhile, "extend" a parent environment
|
|
/// (which could be another virtual environment or a system installation,
|
|
/// and which could itself have multiple `site-packages` directories).
|
|
///
|
|
/// We use an `IndexSet` here to guard against the (very remote)
|
|
/// possibility that an environment might somehow be marked as being
|
|
/// both a `--system-site-packages` virtual environment *and* an
|
|
/// ephemeral environment that extends the system environment. If this
|
|
/// were the case, the system environment's `site-packages` directory
|
|
/// *might* be added to the `SitePackagesPaths` twice, but we wouldn't
|
|
/// want duplicates to appear in this set.
|
|
#[derive(Debug, PartialEq, Eq, Default)]
|
|
pub struct SitePackagesPaths(IndexSet<SystemPathBuf>);
|
|
|
|
impl SitePackagesPaths {
|
|
fn is_empty(&self) -> bool {
|
|
self.0.is_empty()
|
|
}
|
|
|
|
fn insert(&mut self, path: SystemPathBuf) {
|
|
self.0.insert(path);
|
|
}
|
|
|
|
fn extend(&mut self, other: Self) {
|
|
self.0.extend(other.0);
|
|
}
|
|
|
|
/// Tries to detect the version from the layout of the `site-packages` directory.
|
|
pub fn python_version_from_layout(&self) -> Option<PythonVersionWithSource> {
|
|
if cfg!(windows) {
|
|
// The path to `site-packages` on Unix is
|
|
// `<sys.prefix>/lib/pythonX.Y/site-packages`,
|
|
// but on Windows it's `<sys.prefix>/Lib/site-packages`.
|
|
return None;
|
|
}
|
|
|
|
let primary_site_packages = self.0.first()?;
|
|
|
|
let mut site_packages_ancestor_components =
|
|
primary_site_packages.components().rev().skip(1).map(|c| {
|
|
// This should have all been validated in `site_packages.rs`
|
|
// when we resolved the search paths for the project.
|
|
debug_assert!(
|
|
matches!(c, Utf8Component::Normal(_)),
|
|
"Unexpected component in site-packages path `{c:?}` \
|
|
(expected `site-packages` to be an absolute path with symlinks resolved, \
|
|
located at `<sys.prefix>/lib/pythonX.Y/site-packages`)"
|
|
);
|
|
|
|
c.as_str()
|
|
});
|
|
|
|
let parent_component = site_packages_ancestor_components.next()?;
|
|
|
|
if site_packages_ancestor_components.next()? != UnixLibDir::Lib {
|
|
return None;
|
|
}
|
|
|
|
let version = parent_component
|
|
.strip_prefix("python")
|
|
.or_else(|| parent_component.strip_prefix("pypy"))?
|
|
.trim_end_matches('t');
|
|
|
|
let version = PythonVersion::from_str(version).ok()?;
|
|
let source = PythonVersionSource::InstallationDirectoryLayout {
|
|
site_packages_parent_dir: Box::from(parent_component),
|
|
};
|
|
|
|
Some(PythonVersionWithSource { version, source })
|
|
}
|
|
|
|
pub fn into_vec(self) -> Vec<SystemPathBuf> {
|
|
self.0.into_iter().collect()
|
|
}
|
|
}
|
|
|
|
impl<const N: usize> From<[SystemPathBuf; N]> for SitePackagesPaths {
|
|
fn from(paths: [SystemPathBuf; N]) -> Self {
|
|
Self(IndexSet::from(paths))
|
|
}
|
|
}
|
|
|
|
impl IntoIterator for SitePackagesPaths {
|
|
type Item = SystemPathBuf;
|
|
type IntoIter = indexmap::set::IntoIter<SystemPathBuf>;
|
|
|
|
fn into_iter(self) -> Self::IntoIter {
|
|
self.0.into_iter()
|
|
}
|
|
}
|
|
|
|
impl PartialEq<&[SystemPathBuf]> for SitePackagesPaths {
|
|
fn eq(&self, other: &&[SystemPathBuf]) -> bool {
|
|
self.0.as_slice() == *other
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub enum PythonEnvironment {
|
|
Virtual(VirtualEnvironment),
|
|
System(SystemEnvironment),
|
|
}
|
|
|
|
impl PythonEnvironment {
|
|
/// Discover the python environment using the following priorities:
|
|
///
|
|
/// 1. activated virtual environment
|
|
/// 2. conda (child)
|
|
/// 3. working dir virtual environment
|
|
/// 4. conda (base)
|
|
pub fn discover(
|
|
project_root: &SystemPath,
|
|
system: &dyn System,
|
|
) -> Result<Option<Self>, SitePackagesDiscoveryError> {
|
|
fn resolve_environment(
|
|
system: &dyn System,
|
|
path: &SystemPath,
|
|
origin: SysPrefixPathOrigin,
|
|
) -> Result<PythonEnvironment, SitePackagesDiscoveryError> {
|
|
tracing::debug!("Resolving {origin}: {path}");
|
|
PythonEnvironment::new(path, origin, system)
|
|
}
|
|
|
|
if let Ok(virtual_env) = system.env_var(EnvVars::VIRTUAL_ENV) {
|
|
return resolve_environment(
|
|
system,
|
|
SystemPath::new(&virtual_env),
|
|
SysPrefixPathOrigin::VirtualEnvVar,
|
|
)
|
|
.map(Some);
|
|
}
|
|
|
|
if let Some(conda_env) = conda_environment_from_env(system, CondaEnvironmentKind::Child) {
|
|
return resolve_environment(system, &conda_env, SysPrefixPathOrigin::CondaPrefixVar)
|
|
.map(Some);
|
|
}
|
|
|
|
tracing::debug!("Discovering virtual environment in `{project_root}`");
|
|
let virtual_env_directory = project_root.join(".venv");
|
|
|
|
match PythonEnvironment::new(
|
|
&virtual_env_directory,
|
|
SysPrefixPathOrigin::LocalVenv,
|
|
system,
|
|
) {
|
|
Ok(environment) => return Ok(Some(environment)),
|
|
Err(err) => {
|
|
if system.is_directory(&virtual_env_directory) {
|
|
tracing::debug!(
|
|
"Ignoring automatically detected virtual environment at `{}`: {}",
|
|
&virtual_env_directory,
|
|
err
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
if let Some(conda_env) = conda_environment_from_env(system, CondaEnvironmentKind::Base) {
|
|
return resolve_environment(system, &conda_env, SysPrefixPathOrigin::CondaPrefixVar)
|
|
.map(Some);
|
|
}
|
|
|
|
Ok(None)
|
|
}
|
|
|
|
pub fn new(
|
|
path: impl AsRef<SystemPath>,
|
|
origin: SysPrefixPathOrigin,
|
|
system: &dyn System,
|
|
) -> SitePackagesDiscoveryResult<Self> {
|
|
let path = SysPrefixPath::new(path.as_ref(), origin, system)?;
|
|
|
|
// Attempt to inspect as a virtual environment first
|
|
match VirtualEnvironment::new(path, system) {
|
|
Ok(venv) => Ok(Self::Virtual(venv)),
|
|
// If there's not a `pyvenv.cfg` marker, attempt to inspect as a system environment
|
|
Err(SitePackagesDiscoveryError::NoPyvenvCfgFile(path, _, _))
|
|
if !path.origin.must_be_virtual_env() =>
|
|
{
|
|
Ok(Self::System(SystemEnvironment::new(path)))
|
|
}
|
|
Err(err) => Err(err),
|
|
}
|
|
}
|
|
|
|
/// Returns the Python version that was used to create this environment
|
|
/// (will only be available for virtual environments that specify
|
|
/// the metadata in their `pyvenv.cfg` files).
|
|
pub fn python_version_from_metadata(&self) -> Option<&PythonVersionWithSource> {
|
|
match self {
|
|
Self::Virtual(venv) => venv.version.as_ref(),
|
|
Self::System(_) => None,
|
|
}
|
|
}
|
|
|
|
pub fn site_packages_paths(
|
|
&self,
|
|
system: &dyn System,
|
|
) -> SitePackagesDiscoveryResult<SitePackagesPaths> {
|
|
match self {
|
|
Self::Virtual(env) => env.site_packages_directories(system),
|
|
Self::System(env) => env.site_packages_directories(system),
|
|
}
|
|
}
|
|
|
|
pub fn real_stdlib_path(&self, system: &dyn System) -> StdlibDiscoveryResult<SystemPathBuf> {
|
|
match self {
|
|
Self::Virtual(env) => env.real_stdlib_directory(system),
|
|
Self::System(env) => env.real_stdlib_directory(system),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Enumeration of the subdirectories of `sys.prefix` that could contain a
|
|
/// `site-packages` directory if the host system is Unix-like.
|
|
///
|
|
/// For example, if `sys.prefix` is `.venv` and the Python version is 3.10,
|
|
/// the `site-packages` directory could be located at `.venv/lib/python3.10/site-packages`,
|
|
/// or at `.venv/lib64/python3.10/site-packages`, or there could indeed be `site-packages`
|
|
/// directories at both of these locations.
|
|
#[derive(Debug, Clone, Copy, Eq, PartialEq, strum_macros::EnumIter)]
|
|
enum UnixLibDir {
|
|
Lib,
|
|
Lib64,
|
|
}
|
|
|
|
impl UnixLibDir {
|
|
const fn as_str(self) -> &'static str {
|
|
match self {
|
|
Self::Lib => "lib",
|
|
Self::Lib64 => "lib64",
|
|
}
|
|
}
|
|
}
|
|
|
|
impl std::fmt::Display for UnixLibDir {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
f.write_str(self.as_str())
|
|
}
|
|
}
|
|
|
|
impl AsRef<SystemPath> for UnixLibDir {
|
|
fn as_ref(&self) -> &SystemPath {
|
|
SystemPath::new(self.as_str())
|
|
}
|
|
}
|
|
|
|
impl PartialEq<&str> for UnixLibDir {
|
|
fn eq(&self, other: &&str) -> bool {
|
|
self.as_str() == *other
|
|
}
|
|
}
|
|
|
|
impl PartialEq<UnixLibDir> for &str {
|
|
fn eq(&self, other: &UnixLibDir) -> bool {
|
|
other == self
|
|
}
|
|
}
|
|
|
|
/// The Python runtime that produced the venv.
|
|
///
|
|
/// We only need to distinguish cases that change the on-disk layout.
|
|
/// Everything else can be treated like CPython.
|
|
#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
|
|
pub(crate) enum PythonImplementation {
|
|
CPython,
|
|
PyPy,
|
|
GraalPy,
|
|
/// Fallback when the value is missing or unrecognised.
|
|
/// We treat it like CPython but keep the information for diagnostics.
|
|
#[default]
|
|
Unknown,
|
|
}
|
|
|
|
impl PythonImplementation {
|
|
/// Return the relative path from `sys.prefix` to the `site-packages` directory
|
|
/// if this is a known implementation. Return `None` if this is an unknown implementation.
|
|
fn relative_site_packages_path(
|
|
self,
|
|
lib_dir: UnixLibDir,
|
|
version: Option<PythonVersion>,
|
|
) -> Option<String> {
|
|
match self {
|
|
Self::CPython | Self::GraalPy => {
|
|
version.map(|version| format!("{lib_dir}/python{version}/site-packages"))
|
|
}
|
|
Self::PyPy => version.map(|version| format!("{lib_dir}/pypy{version}/site-packages")),
|
|
Self::Unknown => None,
|
|
}
|
|
}
|
|
|
|
/// Return the relative path from `sys.prefix` to the directory containing the python stdlib's
|
|
/// .pys if this is a known implementation. Return `None` if this is an unknown implementation.
|
|
fn relative_stdlib_path(self, version: Option<PythonVersion>) -> Option<String> {
|
|
match self {
|
|
Self::CPython | Self::GraalPy => version.map(|version| format!("lib/python{version}")),
|
|
Self::PyPy => version.map(|version| format!("lib/pypy{version}")),
|
|
Self::Unknown => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Abstraction for a Python virtual environment.
|
|
///
|
|
/// Most of this information is derived from the virtual environment's `pyvenv.cfg` file.
|
|
/// The format of this file is not defined anywhere, and exactly which keys are present
|
|
/// depends on the tool that was used to create the virtual environment.
|
|
#[derive(Debug)]
|
|
pub struct VirtualEnvironment {
|
|
root_path: SysPrefixPath,
|
|
base_executable_home_path: PythonHomePath,
|
|
include_system_site_packages: bool,
|
|
|
|
/// The version of the Python executable that was used to create this virtual environment.
|
|
///
|
|
/// The Python version is encoded under different keys and in different formats
|
|
/// by different virtual-environment creation tools,
|
|
/// and the key is never read by the standard-library `site.py` module,
|
|
/// so it's possible that we might not be able to find this information
|
|
/// in an acceptable format under any of the keys we expect.
|
|
/// This field will be `None` if so.
|
|
version: Option<PythonVersionWithSource>,
|
|
implementation: PythonImplementation,
|
|
|
|
/// If this virtual environment was created using uv,
|
|
/// it may be an "ephemeral" virtual environment that dynamically adds the `site-packages`
|
|
/// directories of its parent environment to `sys.path` at runtime.
|
|
/// Newer versions of uv record the parent environment in the `pyvenv.cfg` file;
|
|
/// we'll want to add the `site-packages` directories of the parent environment
|
|
/// as search paths as well as the `site-packages` directories of this virtual environment.
|
|
parent_environment: Option<Box<PythonEnvironment>>,
|
|
}
|
|
|
|
impl VirtualEnvironment {
|
|
pub(crate) fn new(
|
|
path: SysPrefixPath,
|
|
system: &dyn System,
|
|
) -> SitePackagesDiscoveryResult<Self> {
|
|
let pyvenv_cfg_path = path.join("pyvenv.cfg");
|
|
tracing::debug!("Attempting to parse virtual environment metadata at '{pyvenv_cfg_path}'");
|
|
|
|
let pyvenv_cfg = match system.read_to_string(&pyvenv_cfg_path) {
|
|
Ok(pyvenv_cfg) => pyvenv_cfg,
|
|
Err(err) => {
|
|
return Err(SitePackagesDiscoveryError::NoPyvenvCfgFile(
|
|
path,
|
|
err,
|
|
system.dyn_clone(),
|
|
));
|
|
}
|
|
};
|
|
|
|
let parsed_pyvenv_cfg =
|
|
PyvenvCfgParser::new(&pyvenv_cfg)
|
|
.parse()
|
|
.map_err(|pyvenv_parse_error| {
|
|
SitePackagesDiscoveryError::PyvenvCfgParseError(
|
|
pyvenv_cfg_path.clone(),
|
|
pyvenv_parse_error,
|
|
)
|
|
})?;
|
|
|
|
let RawPyvenvCfg {
|
|
include_system_site_packages,
|
|
base_executable_home_path,
|
|
version,
|
|
implementation,
|
|
created_with_uv,
|
|
parent_environment,
|
|
} = parsed_pyvenv_cfg;
|
|
|
|
// The `home` key is read by the standard library's `site.py` module,
|
|
// so if it's missing from the `pyvenv.cfg` file
|
|
// (or the provided value is invalid),
|
|
// it's reasonable to consider the virtual environment irredeemably broken.
|
|
let Some(base_executable_home_path) = base_executable_home_path else {
|
|
return Err(SitePackagesDiscoveryError::PyvenvCfgParseError(
|
|
pyvenv_cfg_path,
|
|
PyvenvCfgParseErrorKind::NoHomeKey,
|
|
));
|
|
};
|
|
|
|
let base_executable_home_path = PythonHomePath::new(base_executable_home_path, system)
|
|
.map_err(|io_err| {
|
|
SitePackagesDiscoveryError::PyvenvCfgParseError(
|
|
pyvenv_cfg_path.clone(),
|
|
PyvenvCfgParseErrorKind::InvalidHomeValue(io_err),
|
|
)
|
|
})?;
|
|
|
|
// Since the `extends-environment` key is nonstandard,
|
|
// for now we only trust it if the virtual environment was created with `uv`.
|
|
let parent_environment = if created_with_uv {
|
|
parent_environment
|
|
.and_then(|sys_prefix| {
|
|
PythonEnvironment::new(sys_prefix, SysPrefixPathOrigin::DerivedFromPyvenvCfg, system)
|
|
.inspect_err(|err| {
|
|
tracing::warn!(
|
|
"Failed to resolve the parent environment of this ephemeral uv virtual environment \
|
|
from the `extends-environment` value specified in the `pyvenv.cfg` file at {pyvenv_cfg_path}. \
|
|
Imports will not be resolved correctly if they refer to packages installed into the parent \
|
|
environment. Underlying error: {err}",
|
|
);
|
|
})
|
|
.ok()
|
|
})
|
|
.map(Box::new)
|
|
} else {
|
|
None
|
|
};
|
|
|
|
// but the `version`/`version_info` key is not read by the standard library,
|
|
// and is provided under different keys depending on which virtual-environment creation tool
|
|
// created the `pyvenv.cfg` file. Lenient parsing is appropriate here:
|
|
// the file isn't really *invalid* if it doesn't have this key,
|
|
// or if the value doesn't parse according to our expectations.
|
|
let version = version.and_then(|(version_string, range)| {
|
|
let mut version_info_parts = version_string.split('.');
|
|
let (major, minor) = (version_info_parts.next()?, version_info_parts.next()?);
|
|
let version = PythonVersion::try_from((major, minor)).ok()?;
|
|
let source = PythonVersionSource::PyvenvCfgFile(PythonVersionFileSource::new(
|
|
Arc::new(pyvenv_cfg_path),
|
|
Some(range),
|
|
));
|
|
Some(PythonVersionWithSource { version, source })
|
|
});
|
|
|
|
let metadata = Self {
|
|
root_path: path,
|
|
base_executable_home_path,
|
|
include_system_site_packages,
|
|
version,
|
|
implementation,
|
|
parent_environment,
|
|
};
|
|
|
|
tracing::trace!("Resolved metadata for virtual environment: {metadata:?}");
|
|
Ok(metadata)
|
|
}
|
|
|
|
/// Return a list of `site-packages` directories that are available from this virtual environment
|
|
///
|
|
/// See the documentation for [`site_packages_directories_from_sys_prefix`] for more details.
|
|
pub(crate) fn site_packages_directories(
|
|
&self,
|
|
system: &dyn System,
|
|
) -> SitePackagesDiscoveryResult<SitePackagesPaths> {
|
|
let VirtualEnvironment {
|
|
root_path,
|
|
base_executable_home_path,
|
|
include_system_site_packages,
|
|
implementation,
|
|
version,
|
|
parent_environment,
|
|
} = self;
|
|
|
|
let version = version.as_ref().map(|v| v.version);
|
|
|
|
let mut site_packages_directories =
|
|
site_packages_directories_from_sys_prefix(root_path, version, *implementation, system)?;
|
|
|
|
if let Some(parent_env_site_packages) = parent_environment.as_deref() {
|
|
match parent_env_site_packages.site_packages_paths(system) {
|
|
Ok(parent_environment_site_packages) => {
|
|
site_packages_directories.extend(parent_environment_site_packages);
|
|
}
|
|
Err(err) => {
|
|
tracing::warn!(
|
|
"Failed to resolve the site-packages directories of this ephemeral uv virtual environment's \
|
|
parent environment. Imports will not be resolved correctly if they refer to packages installed \
|
|
into the parent environment. Underlying error: {err}"
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
if *include_system_site_packages {
|
|
let system_sys_prefix =
|
|
SysPrefixPath::from_executable_home_path(base_executable_home_path);
|
|
|
|
// If we fail to resolve the `sys.prefix` path from the base executable home path,
|
|
// or if we fail to resolve the `site-packages` from the `sys.prefix` path,
|
|
// we should probably print a warning but *not* abort type checking
|
|
if let Some(sys_prefix_path) = system_sys_prefix {
|
|
match site_packages_directories_from_sys_prefix(
|
|
&sys_prefix_path,
|
|
version,
|
|
*implementation,
|
|
system,
|
|
) {
|
|
Ok(system_directories) => {
|
|
site_packages_directories.extend(system_directories);
|
|
}
|
|
Err(error) => tracing::warn!(
|
|
"{error}. System site-packages will not be used for module resolution."
|
|
),
|
|
}
|
|
} else {
|
|
tracing::warn!(
|
|
"Failed to resolve `sys.prefix` of the system Python installation \
|
|
from the `home` value in the `pyvenv.cfg` file at `{}`. \
|
|
System site-packages will not be used for module resolution.",
|
|
root_path.join("pyvenv.cfg")
|
|
);
|
|
}
|
|
}
|
|
|
|
tracing::debug!(
|
|
"Resolved site-packages directories for this virtual environment are: {site_packages_directories:?}"
|
|
);
|
|
Ok(site_packages_directories)
|
|
}
|
|
|
|
/// Return the real stdlib path (containing actual .py files, and not some variation of typeshed).
|
|
///
|
|
/// See the documentation for [`real_stdlib_directory_from_sys_prefix`] for more details.
|
|
pub(crate) fn real_stdlib_directory(
|
|
&self,
|
|
system: &dyn System,
|
|
) -> StdlibDiscoveryResult<SystemPathBuf> {
|
|
let VirtualEnvironment {
|
|
base_executable_home_path,
|
|
implementation,
|
|
version,
|
|
// Unlike site-packages, what we're looking for is never inside the virtual environment
|
|
// so this is only used for diagnostics.
|
|
root_path,
|
|
// We don't need to respect this setting
|
|
include_system_site_packages: _,
|
|
// We don't need to inherit any info from the parent environment
|
|
parent_environment: _,
|
|
} = self;
|
|
|
|
// Unconditionally follow the same logic that `site_packages_directories` uses when
|
|
// `include_system_site_packages` is true, as those site-packages should be a subdir
|
|
// of the dir we're looking for.
|
|
let version = version.as_ref().map(|v| v.version);
|
|
if let Some(system_sys_prefix) =
|
|
SysPrefixPath::from_executable_home_path_real(system, base_executable_home_path)
|
|
{
|
|
let real_stdlib_directory = real_stdlib_directory_from_sys_prefix(
|
|
&system_sys_prefix,
|
|
version,
|
|
*implementation,
|
|
system,
|
|
);
|
|
match &real_stdlib_directory {
|
|
Ok(path) => tracing::debug!(
|
|
"Resolved real stdlib path for this virtual environment is: {path}"
|
|
),
|
|
Err(_) => tracing::debug!(
|
|
"Failed to resolve real stdlib path for this virtual environment"
|
|
),
|
|
}
|
|
real_stdlib_directory
|
|
} else {
|
|
let cfg_path = root_path.join("pyvenv.cfg");
|
|
tracing::debug!(
|
|
"Failed to resolve `sys.prefix` of the system Python installation \
|
|
from the `home` value in the `pyvenv.cfg` file at `{cfg_path}`. \
|
|
System stdlib will not be used for module definitions.",
|
|
);
|
|
Err(StdlibDiscoveryError::NoSysPrefixFound(cfg_path))
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Different kinds of conda environment
|
|
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
|
|
pub(crate) enum CondaEnvironmentKind {
|
|
/// The base Conda environment; treated like a system Python environment.
|
|
Base,
|
|
/// Any other Conda environment; treated like a virtual environment.
|
|
Child,
|
|
}
|
|
|
|
impl CondaEnvironmentKind {
|
|
/// Compute the kind of `CONDA_PREFIX` we have.
|
|
///
|
|
/// When the base environment is used, `CONDA_DEFAULT_ENV` will be set to a name, i.e., `base` or
|
|
/// `root` which does not match the prefix, e.g. `/usr/local` instead of
|
|
/// `/usr/local/conda/envs/<name>`.
|
|
fn from_prefix_path(system: &dyn System, path: &SystemPath) -> Self {
|
|
// If we cannot read `CONDA_DEFAULT_ENV`, there's no way to know if the base environment
|
|
let Ok(default_env) = system.env_var(EnvVars::CONDA_DEFAULT_ENV) else {
|
|
return CondaEnvironmentKind::Child;
|
|
};
|
|
|
|
// These are the expected names for the base environment
|
|
if default_env != "base" && default_env != "root" {
|
|
return CondaEnvironmentKind::Child;
|
|
}
|
|
|
|
let Some(name) = path.file_name() else {
|
|
return CondaEnvironmentKind::Child;
|
|
};
|
|
|
|
if name == default_env {
|
|
CondaEnvironmentKind::Base
|
|
} else {
|
|
CondaEnvironmentKind::Child
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Read `CONDA_PREFIX` and confirm that it has the expected kind
|
|
pub(crate) fn conda_environment_from_env(
|
|
system: &dyn System,
|
|
kind: CondaEnvironmentKind,
|
|
) -> Option<SystemPathBuf> {
|
|
let dir = system
|
|
.env_var(EnvVars::CONDA_PREFIX)
|
|
.ok()
|
|
.filter(|value| !value.is_empty())?;
|
|
let path = SystemPathBuf::from(dir);
|
|
|
|
if kind != CondaEnvironmentKind::from_prefix_path(system, &path) {
|
|
return None;
|
|
}
|
|
|
|
Some(path)
|
|
}
|
|
|
|
/// A parser for `pyvenv.cfg` files: metadata files for virtual environments.
|
|
///
|
|
/// Note that a `pyvenv.cfg` file *looks* like a `.ini` file, but actually isn't valid `.ini` syntax!
|
|
///
|
|
/// See also: <https://snarky.ca/how-virtual-environments-work/>
|
|
#[derive(Debug)]
|
|
struct PyvenvCfgParser<'s> {
|
|
source: &'s str,
|
|
cursor: Cursor<'s>,
|
|
line_number: NonZeroUsize,
|
|
data: RawPyvenvCfg<'s>,
|
|
}
|
|
|
|
impl<'s> PyvenvCfgParser<'s> {
|
|
fn new(source: &'s str) -> Self {
|
|
Self {
|
|
source,
|
|
cursor: Cursor::new(source),
|
|
line_number: NonZeroUsize::new(1).unwrap(),
|
|
data: RawPyvenvCfg::default(),
|
|
}
|
|
}
|
|
|
|
/// Parse the `pyvenv.cfg` file and return the parsed data.
|
|
fn parse(mut self) -> Result<RawPyvenvCfg<'s>, PyvenvCfgParseErrorKind> {
|
|
while !self.cursor.is_eof() {
|
|
self.parse_line()?;
|
|
self.line_number = self.line_number.checked_add(1).unwrap();
|
|
}
|
|
Ok(self.data)
|
|
}
|
|
|
|
/// Parse a single line of the `pyvenv.cfg` file and advance the cursor
|
|
/// to the beginning of the next line.
|
|
fn parse_line(&mut self) -> Result<(), PyvenvCfgParseErrorKind> {
|
|
let PyvenvCfgParser {
|
|
source,
|
|
cursor,
|
|
line_number,
|
|
data,
|
|
} = self;
|
|
|
|
let line_number = *line_number;
|
|
|
|
cursor.eat_while(|c| c.is_whitespace() && c != '\n');
|
|
|
|
let key_start = cursor.offset();
|
|
cursor.eat_while(|c| !matches!(c, '\n' | '='));
|
|
let key_end = cursor.offset();
|
|
|
|
if !cursor.eat_char('=') {
|
|
// Skip over any lines that do not contain '=' characters, same as the CPython stdlib
|
|
// <https://github.com/python/cpython/blob/e64395e8eb8d3a9e35e3e534e87d427ff27ab0a5/Lib/site.py#L625-L632>
|
|
cursor.eat_char('\n');
|
|
return Ok(());
|
|
}
|
|
|
|
let key = source[TextRange::new(key_start, key_end)].trim();
|
|
|
|
cursor.eat_while(|c| c.is_whitespace() && c != '\n');
|
|
let value_start = cursor.offset();
|
|
cursor.eat_while(|c| c != '\n');
|
|
let value = source[TextRange::new(value_start, cursor.offset())].trim();
|
|
cursor.eat_char('\n');
|
|
|
|
if value.is_empty() {
|
|
return Err(PyvenvCfgParseErrorKind::MalformedKeyValuePair { line_number });
|
|
}
|
|
|
|
match key {
|
|
"include-system-site-packages" => {
|
|
data.include_system_site_packages = value.eq_ignore_ascii_case("true");
|
|
}
|
|
"home" => data.base_executable_home_path = Some(value),
|
|
// `virtualenv` and `uv` call this key `version_info`,
|
|
// but the stdlib venv module calls it `version`
|
|
"version" | "version_info" => {
|
|
let version_range = TextRange::at(value_start, value.text_len());
|
|
data.version = Some((value, version_range));
|
|
}
|
|
"implementation" => {
|
|
data.implementation = match value.to_ascii_lowercase().as_str() {
|
|
"cpython" => PythonImplementation::CPython,
|
|
"graalvm" => PythonImplementation::GraalPy,
|
|
"pypy" => PythonImplementation::PyPy,
|
|
_ => PythonImplementation::Unknown,
|
|
};
|
|
}
|
|
"uv" => data.created_with_uv = true,
|
|
"extends-environment" => data.parent_environment = Some(value),
|
|
"" => {
|
|
return Err(PyvenvCfgParseErrorKind::MalformedKeyValuePair { line_number });
|
|
}
|
|
_ => {}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// A `key:value` mapping derived from parsing a `pyvenv.cfg` file.
|
|
///
|
|
/// This data contained within is still mostly raw and unvalidated.
|
|
#[derive(Debug, Default)]
|
|
struct RawPyvenvCfg<'s> {
|
|
include_system_site_packages: bool,
|
|
base_executable_home_path: Option<&'s str>,
|
|
version: Option<(&'s str, TextRange)>,
|
|
implementation: PythonImplementation,
|
|
created_with_uv: bool,
|
|
parent_environment: Option<&'s str>,
|
|
}
|
|
|
|
/// A Python environment that is _not_ a virtual environment.
|
|
///
|
|
/// This environment may or may not be one that is managed by the operating system itself, e.g.,
|
|
/// this captures both Homebrew-installed Python versions and the bundled macOS Python installation.
|
|
#[derive(Debug)]
|
|
pub struct SystemEnvironment {
|
|
root_path: SysPrefixPath,
|
|
}
|
|
|
|
impl SystemEnvironment {
|
|
/// Create a new system environment from the given path.
|
|
///
|
|
/// At this time, there is no eager validation and this is infallible. Instead, validation
|
|
/// will occur in [`site_packages_directories_from_sys_prefix`] — which will fail if there is not
|
|
/// a Python environment at the given path.
|
|
pub(crate) fn new(path: SysPrefixPath) -> Self {
|
|
Self { root_path: path }
|
|
}
|
|
|
|
/// Return a list of `site-packages` directories that are available from this environment.
|
|
///
|
|
/// See the documentation for [`site_packages_directories_from_sys_prefix`] for more details.
|
|
pub(crate) fn site_packages_directories(
|
|
&self,
|
|
system: &dyn System,
|
|
) -> SitePackagesDiscoveryResult<SitePackagesPaths> {
|
|
let SystemEnvironment { root_path } = self;
|
|
|
|
let site_packages_directories = site_packages_directories_from_sys_prefix(
|
|
root_path,
|
|
None,
|
|
PythonImplementation::Unknown,
|
|
system,
|
|
)?;
|
|
|
|
tracing::debug!(
|
|
"Resolved site-packages directories for this environment are: {site_packages_directories:?}"
|
|
);
|
|
Ok(site_packages_directories)
|
|
}
|
|
|
|
/// Return a list of `site-packages` directories that are available from this environment.
|
|
///
|
|
/// See the documentation for [`site_packages_directories_from_sys_prefix`] for more details.
|
|
pub(crate) fn real_stdlib_directory(
|
|
&self,
|
|
system: &dyn System,
|
|
) -> StdlibDiscoveryResult<SystemPathBuf> {
|
|
let SystemEnvironment { root_path } = self;
|
|
|
|
let stdlib_directory = real_stdlib_directory_from_sys_prefix(
|
|
root_path,
|
|
None,
|
|
PythonImplementation::Unknown,
|
|
system,
|
|
)?;
|
|
|
|
tracing::debug!(
|
|
"Resolved real stdlib directory for this environment is: {stdlib_directory:?}"
|
|
);
|
|
Ok(stdlib_directory)
|
|
}
|
|
}
|
|
|
|
/// Enumeration of ways in which `site-packages` discovery can fail.
|
|
#[derive(Debug)]
|
|
pub enum SitePackagesDiscoveryError {
|
|
/// `site-packages` discovery failed because the provided path couldn't be canonicalized.
|
|
CanonicalizationError(
|
|
SystemPathBuf,
|
|
SysPrefixPathOrigin,
|
|
io::Error,
|
|
Box<dyn System>,
|
|
),
|
|
|
|
/// `site-packages` discovery failed because the provided path doesn't appear to point to
|
|
/// a Python executable or a `sys.prefix` directory.
|
|
PathNotExecutableOrDirectory(
|
|
SystemPathBuf,
|
|
SysPrefixPathOrigin,
|
|
Option<io::Error>,
|
|
Box<dyn System>,
|
|
),
|
|
|
|
/// `site-packages` discovery failed because the [`SysPrefixPathOrigin`] indicated that
|
|
/// the provided path should point to the `sys.prefix` of a virtual environment,
|
|
/// but there was no file at `<sys.prefix>/pyvenv.cfg`.
|
|
NoPyvenvCfgFile(SysPrefixPath, io::Error, Box<dyn System>),
|
|
|
|
/// `site-packages` discovery failed because the `pyvenv.cfg` file could not be parsed.
|
|
PyvenvCfgParseError(SystemPathBuf, PyvenvCfgParseErrorKind),
|
|
|
|
/// `site-packages` discovery failed because we're on a Unix system,
|
|
/// we weren't able to figure out from the `pyvenv.cfg` file exactly where `site-packages`
|
|
/// would be relative to the `sys.prefix` path, and we tried to fallback to iterating
|
|
/// through the `<sys.prefix>/lib` directory looking for a `site-packages` directory,
|
|
/// but we came across some I/O error while trying to do so.
|
|
CouldNotReadLibDirectory(SysPrefixPath, Box<dyn System>),
|
|
|
|
/// We looked everywhere we could think of for the `site-packages` directory,
|
|
/// but none could be found despite our best endeavours.
|
|
NoSitePackagesDirFound(SysPrefixPath, Box<dyn System>),
|
|
}
|
|
|
|
/// Enumeration of ways in which stdlib discovery can fail.
|
|
#[derive(Debug)]
|
|
pub enum StdlibDiscoveryError {
|
|
/// We looked everywhere we could think of for the standard library's directory,
|
|
/// but none could be found despite our best endeavours.
|
|
NoStdlibFound(SysPrefixPath, Box<dyn System>),
|
|
/// Stdlib discovery failed because we're on a Unix system,
|
|
/// we weren't able to figure out from the `pyvenv.cfg` file exactly where the stdlib
|
|
/// would be relative to the `sys.prefix` path, and we tried to fallback to iterating
|
|
/// through the `<sys.prefix>/lib` directory looking for a stdlib directory,
|
|
/// but we came across some I/O error while trying to do so.
|
|
CouldNotReadLibDirectory(SysPrefixPath, io::Error, Box<dyn System>),
|
|
/// We failed to resolve the value of `sys.prefix`.
|
|
NoSysPrefixFound(SystemPathBuf),
|
|
}
|
|
|
|
impl std::error::Error for SitePackagesDiscoveryError {
|
|
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
|
match self {
|
|
Self::CanonicalizationError(_, _, io_err, _) => Some(io_err),
|
|
Self::PathNotExecutableOrDirectory(_, _, io_err, _) => {
|
|
io_err.as_ref().map(|e| e as &dyn std::error::Error)
|
|
}
|
|
Self::NoPyvenvCfgFile(_, io_err, _) => Some(io_err),
|
|
Self::PyvenvCfgParseError(_, _)
|
|
| Self::CouldNotReadLibDirectory(_, _)
|
|
| Self::NoSitePackagesDirFound(_, _) => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl std::fmt::Display for SitePackagesDiscoveryError {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
match self {
|
|
Self::CanonicalizationError(given_path, origin, _, system) => display_error(
|
|
f,
|
|
origin,
|
|
given_path,
|
|
"Failed to canonicalize",
|
|
None,
|
|
&**system,
|
|
),
|
|
Self::PathNotExecutableOrDirectory(path, origin, _, system) => {
|
|
let thing = if origin.must_point_directly_to_sys_prefix() {
|
|
"directory on disk"
|
|
} else {
|
|
"Python executable or a directory on disk"
|
|
};
|
|
display_error(
|
|
f,
|
|
origin,
|
|
path,
|
|
&format!("Invalid {origin}"),
|
|
Some(&format!("does not point to a {thing}")),
|
|
&**system,
|
|
)
|
|
}
|
|
Self::NoPyvenvCfgFile(SysPrefixPath { inner, origin }, _, system) => display_error(
|
|
f,
|
|
origin,
|
|
inner,
|
|
&format!("Invalid {origin}"),
|
|
Some("points to a broken venv with no pyvenv.cfg file"),
|
|
&**system,
|
|
),
|
|
Self::PyvenvCfgParseError(path, kind) => {
|
|
write!(
|
|
f,
|
|
"Failed to parse the `pyvenv.cfg` file at `{path}` because {kind}"
|
|
)
|
|
}
|
|
Self::CouldNotReadLibDirectory(SysPrefixPath { inner, origin }, system) => {
|
|
display_error(
|
|
f,
|
|
origin,
|
|
inner,
|
|
"Failed to iterate over the contents of the `lib`/`lib64` directories of the Python installation",
|
|
None,
|
|
&**system,
|
|
)
|
|
}
|
|
Self::NoSitePackagesDirFound(SysPrefixPath { inner, origin }, system) => display_error(
|
|
f,
|
|
origin,
|
|
inner,
|
|
&format!("Invalid {origin}"),
|
|
Some(
|
|
"Could not find a `site-packages` directory for this Python installation/executable",
|
|
),
|
|
&**system,
|
|
),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl std::error::Error for StdlibDiscoveryError {
|
|
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
|
match self {
|
|
Self::CouldNotReadLibDirectory(_, io_err, _) => Some(io_err),
|
|
Self::NoStdlibFound(_, _) => None,
|
|
Self::NoSysPrefixFound(_) => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl std::fmt::Display for StdlibDiscoveryError {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
match self {
|
|
Self::NoSysPrefixFound(path) => {
|
|
write!(
|
|
f,
|
|
"Failed to resolve a `sys.prefix` from the `pyvenv.cfg` file at `{path}`"
|
|
)
|
|
}
|
|
Self::CouldNotReadLibDirectory(SysPrefixPath { inner, origin }, _, system) => {
|
|
display_error(
|
|
f,
|
|
origin,
|
|
inner,
|
|
"Failed to iterate over the contents of the `lib` directory of the Python installation",
|
|
None,
|
|
&**system,
|
|
)
|
|
}
|
|
Self::NoStdlibFound(SysPrefixPath { inner, origin }, system) => display_error(
|
|
f,
|
|
origin,
|
|
inner,
|
|
&format!("Invalid {origin}"),
|
|
Some("Could not find a stdlib directory for this Python installation/executable"),
|
|
&**system,
|
|
),
|
|
}
|
|
}
|
|
}
|
|
|
|
fn display_error(
|
|
f: &mut std::fmt::Formatter<'_>,
|
|
sys_prefix_origin: &SysPrefixPathOrigin,
|
|
given_path: &SystemPath,
|
|
primary_message: &str,
|
|
secondary_message: Option<&str>,
|
|
system: &dyn System,
|
|
) -> std::fmt::Result {
|
|
let fallback: &mut dyn FnMut() -> std::fmt::Result = &mut || {
|
|
f.write_str(primary_message)?;
|
|
write!(f, " `{given_path}`")?;
|
|
if let Some(secondary_message) = secondary_message {
|
|
f.write_str(": ")?;
|
|
f.write_str(secondary_message)?;
|
|
}
|
|
Ok(())
|
|
};
|
|
|
|
let SysPrefixPathOrigin::ConfigFileSetting(config_file_path, Some(setting_range)) =
|
|
sys_prefix_origin
|
|
else {
|
|
return fallback();
|
|
};
|
|
|
|
let Ok(config_file_source) = system.read_to_string(config_file_path) else {
|
|
return fallback();
|
|
};
|
|
|
|
let index = LineIndex::from_source_text(&config_file_source);
|
|
let source = SourceCode::new(&config_file_source, &index);
|
|
|
|
let primary_message = format!(
|
|
"{primary_message}
|
|
|
|
--> Invalid setting in configuration file `{config_file_path}`"
|
|
);
|
|
|
|
let start_index = source.line_index(setting_range.start()).saturating_sub(2);
|
|
let end_index = source
|
|
.line_index(setting_range.end())
|
|
.saturating_add(2)
|
|
.min(OneIndexed::from_zero_indexed(source.line_count()));
|
|
|
|
let start_offset = source.line_start(start_index);
|
|
let end_offset = source.line_end(end_index);
|
|
|
|
let mut annotation = Level::Error.span((setting_range - start_offset).into());
|
|
|
|
if let Some(secondary_message) = secondary_message {
|
|
annotation = annotation.label(secondary_message);
|
|
}
|
|
|
|
let snippet = Snippet::source(&config_file_source[TextRange::new(start_offset, end_offset)])
|
|
.annotation(annotation)
|
|
.line_start(start_index.get())
|
|
.fold(false);
|
|
|
|
let message = Level::None.title(&primary_message).snippet(snippet);
|
|
|
|
let renderer = if colored::control::SHOULD_COLORIZE.should_colorize() {
|
|
Renderer::styled()
|
|
} else {
|
|
Renderer::plain()
|
|
};
|
|
let renderer = renderer.cut_indicator("…");
|
|
|
|
writeln!(f, "{}", renderer.render(message))
|
|
}
|
|
|
|
/// The various ways in which parsing a `pyvenv.cfg` file could fail
|
|
#[derive(Debug)]
|
|
pub enum PyvenvCfgParseErrorKind {
|
|
MalformedKeyValuePair { line_number: NonZeroUsize },
|
|
NoHomeKey,
|
|
InvalidHomeValue(io::Error),
|
|
}
|
|
|
|
impl fmt::Display for PyvenvCfgParseErrorKind {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
match self {
|
|
Self::MalformedKeyValuePair { line_number } => write!(
|
|
f,
|
|
"line {line_number} has a malformed `<key> = <value>` pair"
|
|
),
|
|
Self::NoHomeKey => f.write_str("the file does not have a `home` key"),
|
|
Self::InvalidHomeValue(io_err) => {
|
|
write!(
|
|
f,
|
|
"the following error was encountered \
|
|
when trying to resolve the `home` value to a directory on disk: {io_err}"
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Attempt to retrieve the `site-packages` directories
|
|
/// associated with a given Python installation.
|
|
///
|
|
/// The location of the `site-packages` directories can vary according to the
|
|
/// Python version that this installation represents. The Python version may
|
|
/// or may not be known at this point, which is why the `python_version`
|
|
/// parameter is an `Option`.
|
|
fn site_packages_directories_from_sys_prefix(
|
|
sys_prefix_path: &SysPrefixPath,
|
|
python_version: Option<PythonVersion>,
|
|
implementation: PythonImplementation,
|
|
system: &dyn System,
|
|
) -> SitePackagesDiscoveryResult<SitePackagesPaths> {
|
|
tracing::debug!(
|
|
"Searching for site-packages directory in sys.prefix {}",
|
|
sys_prefix_path.inner
|
|
);
|
|
|
|
if cfg!(target_os = "windows") {
|
|
let site_packages = sys_prefix_path.join(r"Lib\site-packages");
|
|
return system
|
|
.is_directory(&site_packages)
|
|
.then(|| SitePackagesPaths::from([site_packages]))
|
|
.ok_or_else(|| {
|
|
SitePackagesDiscoveryError::NoSitePackagesDirFound(
|
|
sys_prefix_path.to_owned(),
|
|
system.dyn_clone(),
|
|
)
|
|
});
|
|
}
|
|
|
|
// In the Python standard library's `site.py` module (used for finding `site-packages`
|
|
// at runtime), we can find this in [the non-Windows branch]:
|
|
//
|
|
// ```py
|
|
// libdirs = [sys.platlibdir]
|
|
// if sys.platlibdir != "lib":
|
|
// libdirs.append("lib")
|
|
// ```
|
|
//
|
|
// We generally only care about the `site-packages` directory insofar as it allows
|
|
// us to discover Python source code that can be used for inferring type
|
|
// information regarding third-party dependencies. In theory, therefore, that means
|
|
// that we don't need to care about any possible `lib64/site-packages` directories,
|
|
// since [the `sys`-module documentation] states that `sys.platlibdir` is *only* ever
|
|
// used for C extensions, never for pure-Python modules. However, in practice,
|
|
// some installers appear to do [some strange things on Fedora] that mean that `.py`
|
|
// files *can* end up in `lib64/site-packages` in some edge cases. And we'll probably
|
|
// need to start looking in `lib64/site-packages` directories in the future anyway, in
|
|
// order to distinguish between "unresolved import" and "resolved to an opaque C
|
|
// extension" in diagnostic messages.
|
|
//
|
|
// [the non-Windows branch]: https://github.com/python/cpython/blob/a8be8fc6c4682089be45a87bd5ee1f686040116c/Lib/site.py#L401-L410
|
|
// [the `sys`-module documentation]: https://docs.python.org/3/library/sys.html#sys.platlibdir
|
|
// [some strange things on Fedora]: https://github.com/astral-sh/ty/issues/1043
|
|
|
|
let mut directories = SitePackagesPaths::default();
|
|
|
|
// If we were able to figure out what Python version this installation is,
|
|
// we should be able to avoid iterating through all items in the `lib/` and `lib64/` directories:
|
|
for lib_dir in UnixLibDir::iter() {
|
|
if let Some(expected_relative_path) =
|
|
implementation.relative_site_packages_path(lib_dir, python_version)
|
|
{
|
|
let expected_absolute_path = sys_prefix_path.join(expected_relative_path);
|
|
if system.is_directory(&expected_absolute_path) {
|
|
directories.insert(expected_absolute_path);
|
|
} else if matches!(implementation, PythonImplementation::CPython)
|
|
&& python_version.is_some_and(PythonVersion::free_threaded_build_available)
|
|
{
|
|
// CPython free-threaded (3.13+) variant: pythonX.Yt
|
|
let alternative_path = sys_prefix_path.join(format!(
|
|
"{lib_dir}/python{}t/site-packages",
|
|
python_version.unwrap()
|
|
));
|
|
if system.is_directory(&alternative_path) {
|
|
directories.insert(alternative_path);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if !directories.is_empty() {
|
|
return Ok(directories);
|
|
}
|
|
|
|
// Either we couldn't figure out the version before calling this function
|
|
// (e.g., from a `pyvenv.cfg` file if this was a venv),
|
|
// or we couldn't find a `site-packages` folder at the expected location given
|
|
// the parsed version
|
|
//
|
|
// Note: the `python3.x` part of the `site-packages` path can't be computed from
|
|
// the `--python-version` the user has passed, as they might be running Python 3.12 locally
|
|
// even if they've requested that we type check their code "as if" they're running 3.8.
|
|
let mut found_at_least_one_lib_dir = false;
|
|
|
|
for lib_dir in UnixLibDir::iter() {
|
|
let Ok(directory_iterator) = system.read_directory(&sys_prefix_path.join(lib_dir)) else {
|
|
tracing::debug!("Could not find a `<sys.prefix>/{lib_dir}` directory; continuing");
|
|
continue;
|
|
};
|
|
|
|
found_at_least_one_lib_dir = true;
|
|
|
|
for entry_result in directory_iterator {
|
|
let Ok(entry) = entry_result else {
|
|
continue;
|
|
};
|
|
|
|
if !entry.file_type().is_directory() {
|
|
continue;
|
|
}
|
|
|
|
let mut path = entry.into_path();
|
|
|
|
let name = path.file_name().unwrap_or_else(|| panic!(
|
|
"File name should be non-null because path is guaranteed to be a child of `{lib_dir}`",
|
|
));
|
|
|
|
if !(name.starts_with("python3.") || name.starts_with("pypy3.")) {
|
|
continue;
|
|
}
|
|
|
|
path.push("site-packages");
|
|
if system.is_directory(&path) {
|
|
directories.insert(path);
|
|
}
|
|
}
|
|
}
|
|
|
|
if directories.is_empty() {
|
|
if found_at_least_one_lib_dir {
|
|
Err(SitePackagesDiscoveryError::NoSitePackagesDirFound(
|
|
sys_prefix_path.to_owned(),
|
|
system.dyn_clone(),
|
|
))
|
|
} else {
|
|
Err(SitePackagesDiscoveryError::CouldNotReadLibDirectory(
|
|
sys_prefix_path.to_owned(),
|
|
system.dyn_clone(),
|
|
))
|
|
}
|
|
} else {
|
|
Ok(directories)
|
|
}
|
|
}
|
|
|
|
/// Attempt to retrieve the real stdlib directory
|
|
/// associated with a given Python installation.
|
|
///
|
|
/// The location of the stdlib directory can vary according to the
|
|
/// Python version that this installation represents. The Python version may
|
|
/// or may not be known at this point, which is why the `python_version`
|
|
/// parameter is an `Option`.
|
|
fn real_stdlib_directory_from_sys_prefix(
|
|
sys_prefix_path: &SysPrefixPath,
|
|
python_version: Option<PythonVersion>,
|
|
implementation: PythonImplementation,
|
|
system: &dyn System,
|
|
) -> StdlibDiscoveryResult<SystemPathBuf> {
|
|
tracing::debug!(
|
|
"Searching for real stdlib directory in sys.prefix {}",
|
|
sys_prefix_path.inner
|
|
);
|
|
|
|
if cfg!(target_os = "windows") {
|
|
let stdlib = sys_prefix_path.join("Lib");
|
|
return system.is_directory(&stdlib).then_some(stdlib).ok_or(
|
|
StdlibDiscoveryError::NoStdlibFound(sys_prefix_path.to_owned(), system.dyn_clone()),
|
|
);
|
|
}
|
|
|
|
// If we were able to figure out what Python version this installation is,
|
|
// we should be able to avoid iterating through all items in the `lib/` directory:
|
|
if let Some(expected_relative_path) = implementation.relative_stdlib_path(python_version) {
|
|
let expected_absolute_path = sys_prefix_path.join(expected_relative_path);
|
|
if system.is_directory(&expected_absolute_path) {
|
|
return Ok(expected_absolute_path);
|
|
}
|
|
|
|
// CPython free-threaded (3.13+) variant: pythonXYt
|
|
if matches!(implementation, PythonImplementation::CPython)
|
|
&& python_version.is_some_and(PythonVersion::free_threaded_build_available)
|
|
{
|
|
let alternative_path =
|
|
sys_prefix_path.join(format!("lib/python{}t", python_version.unwrap()));
|
|
if system.is_directory(&alternative_path) {
|
|
return Ok(alternative_path);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Either we couldn't figure out the version before calling this function
|
|
// (e.g., from a `pyvenv.cfg` file if this was a venv),
|
|
// or we couldn't find a stdlib folder at the expected location given
|
|
// the parsed version
|
|
//
|
|
// Note: the `python3.x` part of the stdlib path can't be computed from
|
|
// the `--python-version` the user has passed, as they might be running Python 3.12 locally
|
|
// even if they've requested that we type check their code "as if" they're running 3.8.
|
|
for entry_result in system
|
|
// must be `lib`, not `lib64`, for the stdlib
|
|
.read_directory(&sys_prefix_path.join(UnixLibDir::Lib))
|
|
.map_err(|io_err| {
|
|
StdlibDiscoveryError::CouldNotReadLibDirectory(
|
|
sys_prefix_path.to_owned(),
|
|
io_err,
|
|
system.dyn_clone(),
|
|
)
|
|
})?
|
|
{
|
|
let Ok(entry) = entry_result else {
|
|
continue;
|
|
};
|
|
|
|
if !entry.file_type().is_directory() {
|
|
continue;
|
|
}
|
|
|
|
let path = entry.into_path();
|
|
|
|
let name = path.file_name().expect(
|
|
"File name should be non-null because path is guaranteed to be a child of `lib`",
|
|
);
|
|
|
|
if !(name.starts_with("python3.") || name.starts_with("pypy3.")) {
|
|
continue;
|
|
}
|
|
|
|
return Ok(path);
|
|
}
|
|
Err(StdlibDiscoveryError::NoStdlibFound(
|
|
sys_prefix_path.to_owned(),
|
|
system.dyn_clone(),
|
|
))
|
|
}
|
|
|
|
/// A path that represents the value of [`sys.prefix`] at runtime in Python
|
|
/// for a given Python executable.
|
|
///
|
|
/// For the case of a virtual environment, where a
|
|
/// Python binary is at `/.venv/bin/python`, `sys.prefix` is the path to
|
|
/// the virtual environment the Python binary lies inside, i.e. `/.venv`,
|
|
/// and `site-packages` will be at `.venv/lib/python3.X/site-packages`.
|
|
/// System Python installations generally work the same way: if a system
|
|
/// Python installation lies at `/opt/homebrew/bin/python`, `sys.prefix`
|
|
/// will be `/opt/homebrew`, and `site-packages` will be at
|
|
/// `/opt/homebrew/lib/python3.X/site-packages`.
|
|
///
|
|
/// [`sys.prefix`]: https://docs.python.org/3/library/sys.html#sys.prefix
|
|
#[derive(Debug, PartialEq, Eq, Clone)]
|
|
pub struct SysPrefixPath {
|
|
inner: SystemPathBuf,
|
|
origin: SysPrefixPathOrigin,
|
|
}
|
|
|
|
impl SysPrefixPath {
|
|
fn new(
|
|
unvalidated_path: &SystemPath,
|
|
origin: SysPrefixPathOrigin,
|
|
system: &dyn System,
|
|
) -> SitePackagesDiscoveryResult<Self> {
|
|
let sys_prefix = if !origin.must_point_directly_to_sys_prefix()
|
|
&& system.is_file(unvalidated_path)
|
|
&& unvalidated_path
|
|
.file_name()
|
|
.is_some_and(|name| name.starts_with("python"))
|
|
{
|
|
// It looks like they passed us a path to a Python executable, e.g. `.venv/bin/python3`.
|
|
// Try to figure out the `sys.prefix` value from the Python executable.
|
|
let sys_prefix = if cfg!(windows) {
|
|
// On Windows, the relative path to the Python executable from `sys.prefix`
|
|
// is different depending on whether it's a virtual environment or a system installation.
|
|
// System installations have their executable at `<sys.prefix>/python.exe`,
|
|
// whereas virtual environments have their executable at `<sys.prefix>/Scripts/python.exe`.
|
|
unvalidated_path.parent().and_then(|parent| {
|
|
if parent.file_name() == Some("Scripts") {
|
|
parent.parent()
|
|
} else {
|
|
Some(parent)
|
|
}
|
|
})
|
|
} else {
|
|
// On Unix, `sys.prefix` is always the grandparent directory of the Python executable,
|
|
// regardless of whether it's a virtual environment or a system installation.
|
|
unvalidated_path.ancestors().nth(2)
|
|
};
|
|
let Some(sys_prefix) = sys_prefix else {
|
|
return Err(SitePackagesDiscoveryError::PathNotExecutableOrDirectory(
|
|
unvalidated_path.to_path_buf(),
|
|
origin,
|
|
None,
|
|
system.dyn_clone(),
|
|
));
|
|
};
|
|
sys_prefix
|
|
} else {
|
|
unvalidated_path
|
|
};
|
|
|
|
// It's important to resolve symlinks here rather than simply making the path absolute,
|
|
// since system Python installations often only put symlinks in the "expected"
|
|
// locations for `home` and `site-packages`
|
|
let sys_prefix = match system.canonicalize_path(sys_prefix) {
|
|
Ok(path) => path,
|
|
Err(io_err) => {
|
|
let unvalidated_path = unvalidated_path.to_path_buf();
|
|
let err = if io_err.kind() == io::ErrorKind::NotFound {
|
|
SitePackagesDiscoveryError::PathNotExecutableOrDirectory(
|
|
unvalidated_path,
|
|
origin,
|
|
Some(io_err),
|
|
system.dyn_clone(),
|
|
)
|
|
} else {
|
|
SitePackagesDiscoveryError::CanonicalizationError(
|
|
unvalidated_path,
|
|
origin,
|
|
io_err,
|
|
system.dyn_clone(),
|
|
)
|
|
};
|
|
return Err(err);
|
|
}
|
|
};
|
|
|
|
if !system.is_directory(&sys_prefix) {
|
|
return Err(SitePackagesDiscoveryError::PathNotExecutableOrDirectory(
|
|
unvalidated_path.to_path_buf(),
|
|
origin,
|
|
None,
|
|
system.dyn_clone(),
|
|
));
|
|
}
|
|
|
|
Ok(Self {
|
|
inner: sys_prefix,
|
|
origin,
|
|
})
|
|
}
|
|
fn from_executable_home_path(path: &PythonHomePath) -> Option<Self> {
|
|
// No need to check whether `path.parent()` is a directory:
|
|
// the parent of a canonicalised path that is known to exist
|
|
// is guaranteed to be a directory.
|
|
if cfg!(target_os = "windows") {
|
|
Some(Self {
|
|
inner: path.to_path_buf(),
|
|
origin: SysPrefixPathOrigin::DerivedFromPyvenvCfg,
|
|
})
|
|
} else {
|
|
path.parent().map(|path| Self {
|
|
inner: path.to_path_buf(),
|
|
origin: SysPrefixPathOrigin::DerivedFromPyvenvCfg,
|
|
})
|
|
}
|
|
}
|
|
/// Like `from_executable_home_path` but attempts to resolve through symlink facades
|
|
/// to find a sys prefix that will actually contain the stdlib.
|
|
fn from_executable_home_path_real(system: &dyn System, path: &PythonHomePath) -> Option<Self> {
|
|
let mut home_path = path.0.clone();
|
|
|
|
// Try to find the python executable in the given directory and canonicalize it
|
|
// to resolve any symlink. This is (at least) necessary for homebrew pythons
|
|
// and the macOS system python.
|
|
//
|
|
// In python installations like homebrew, the home path points to a directory like
|
|
// `/opt/homebrew/opt/python@3.13/bin` and indeed if you look for `../lib/python3.13/`
|
|
// you *will* find `site-packages` but you *won't* find the stdlib! (For the macOS
|
|
// system install you won't even find `site-packages` here.)
|
|
//
|
|
// However if you look at `/opt/homebrew/opt/python@3.13/bin/python3.13` (the actual
|
|
// python executable in that dir) you will find that it's a symlink to something like
|
|
// `../Frameworks/Python.framework/Versions/3.13/bin/python3.13`
|
|
//
|
|
// From this Framework binary path if you go to `../../lib/python3.13/` you will then
|
|
// find the python stdlib as expected (and a different instance of site-packages).
|
|
//
|
|
// FIXME: it would be nice to include a "we know the python name" fastpath like in
|
|
// `real_stdlib_directory_from_sys_prefix`.
|
|
if let Ok(dir) = system.read_directory(&home_path) {
|
|
for entry_result in dir {
|
|
let Ok(entry) = entry_result else {
|
|
continue;
|
|
};
|
|
|
|
if entry.file_type().is_directory() {
|
|
continue;
|
|
}
|
|
|
|
let path = entry.into_path();
|
|
|
|
let name = path.file_name().expect(
|
|
"File name should be non-null because path is guaranteed to be a child of `lib`",
|
|
);
|
|
|
|
if !(name.starts_with("python3.") || name.starts_with("pypy3.")) {
|
|
continue;
|
|
}
|
|
|
|
let Ok(canonical_path) = system.canonicalize_path(&path) else {
|
|
continue;
|
|
};
|
|
|
|
let Some(parent) = canonical_path.parent() else {
|
|
continue;
|
|
};
|
|
|
|
home_path = parent.to_path_buf();
|
|
break;
|
|
}
|
|
}
|
|
|
|
// No need to check whether `path.parent()` is a directory:
|
|
// the parent of a canonicalised path that is known to exist
|
|
// is guaranteed to be a directory.
|
|
if cfg!(target_os = "windows") {
|
|
Some(Self {
|
|
inner: home_path.to_path_buf(),
|
|
origin: SysPrefixPathOrigin::DerivedFromPyvenvCfg,
|
|
})
|
|
} else {
|
|
home_path.parent().map(|home_path| Self {
|
|
inner: home_path.to_path_buf(),
|
|
origin: SysPrefixPathOrigin::DerivedFromPyvenvCfg,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Deref for SysPrefixPath {
|
|
type Target = SystemPath;
|
|
|
|
fn deref(&self) -> &Self::Target {
|
|
&self.inner
|
|
}
|
|
}
|
|
|
|
/// Enumeration of sources a `sys.prefix` path can come from.
|
|
#[derive(Debug, PartialEq, Eq, Clone)]
|
|
pub enum SysPrefixPathOrigin {
|
|
/// The `sys.prefix` path came from a configuration file setting: `pyproject.toml` or `ty.toml`
|
|
ConfigFileSetting(Arc<SystemPathBuf>, Option<TextRange>),
|
|
/// The `sys.prefix` path came from a `--python` CLI flag
|
|
PythonCliFlag,
|
|
/// The selected interpreter in the VS Code's Python extension.
|
|
PythonVSCodeExtension,
|
|
/// The `sys.prefix` path came from the `VIRTUAL_ENV` environment variable
|
|
VirtualEnvVar,
|
|
/// The `sys.prefix` path came from the `CONDA_PREFIX` environment variable
|
|
CondaPrefixVar,
|
|
/// The `sys.prefix` path was derived from a value in a `pyvenv.cfg` file:
|
|
/// either the value associated with the `home` key
|
|
/// or the value associated with the `extends-environment` key
|
|
DerivedFromPyvenvCfg,
|
|
/// A `.venv` directory was found in the current working directory,
|
|
/// and the `sys.prefix` path is the path to that virtual environment.
|
|
LocalVenv,
|
|
}
|
|
|
|
impl SysPrefixPathOrigin {
|
|
/// Whether the given `sys.prefix` path must be a virtual environment (rather than a system
|
|
/// Python environment).
|
|
pub(crate) const fn must_be_virtual_env(&self) -> bool {
|
|
match self {
|
|
Self::LocalVenv | Self::VirtualEnvVar => true,
|
|
Self::ConfigFileSetting(..)
|
|
| Self::PythonCliFlag
|
|
| Self::PythonVSCodeExtension
|
|
| Self::DerivedFromPyvenvCfg
|
|
| Self::CondaPrefixVar => false,
|
|
}
|
|
}
|
|
|
|
/// Whether paths with this origin always point directly to the `sys.prefix` directory.
|
|
///
|
|
/// Some variants can point either directly to `sys.prefix` or to a Python executable inside
|
|
/// the `sys.prefix` directory, e.g. the `--python` CLI flag.
|
|
pub(crate) const fn must_point_directly_to_sys_prefix(&self) -> bool {
|
|
match self {
|
|
Self::PythonCliFlag | Self::ConfigFileSetting(..) | Self::PythonVSCodeExtension => {
|
|
false
|
|
}
|
|
Self::VirtualEnvVar
|
|
| Self::CondaPrefixVar
|
|
| Self::DerivedFromPyvenvCfg
|
|
| Self::LocalVenv => true,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl std::fmt::Display for SysPrefixPathOrigin {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
|
match self {
|
|
Self::PythonCliFlag => f.write_str("`--python` argument"),
|
|
Self::ConfigFileSetting(_, _) => f.write_str("`environment.python` setting"),
|
|
Self::VirtualEnvVar => f.write_str("`VIRTUAL_ENV` environment variable"),
|
|
Self::CondaPrefixVar => f.write_str("`CONDA_PREFIX` environment variable"),
|
|
Self::DerivedFromPyvenvCfg => f.write_str("derived `sys.prefix` path"),
|
|
Self::LocalVenv => f.write_str("local virtual environment"),
|
|
Self::PythonVSCodeExtension => {
|
|
f.write_str("selected interpreter in the VS Code Python extension")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The value given by the `home` key in `pyvenv.cfg` files.
|
|
///
|
|
/// This is equivalent to `{sys_prefix_path}/bin`, and points
|
|
/// to a directory in which a Python executable can be found.
|
|
/// Confusingly, it is *not* the same as the [`PYTHONHOME`]
|
|
/// environment variable that Python provides! However, it's
|
|
/// consistent among all mainstream creators of Python virtual
|
|
/// environments (the stdlib Python `venv` module, the third-party
|
|
/// `virtualenv` library, and `uv`), was specified by
|
|
/// [the original PEP adding the `venv` module],
|
|
/// and it's one of the few fields that's read by the Python
|
|
/// standard library's `site.py` module.
|
|
///
|
|
/// Although it doesn't appear to be specified anywhere,
|
|
/// all existing virtual environment tools always use an absolute path
|
|
/// for the `home` value, and the Python standard library also assumes
|
|
/// that the `home` value will be an absolute path.
|
|
///
|
|
/// Other values, such as the path to the Python executable or the
|
|
/// base-executable `sys.prefix` value, are either only provided in
|
|
/// `pyvenv.cfg` files by some virtual-environment creators,
|
|
/// or are included under different keys depending on which
|
|
/// virtual-environment creation tool you've used.
|
|
///
|
|
/// [`PYTHONHOME`]: https://docs.python.org/3/using/cmdline.html#envvar-PYTHONHOME
|
|
/// [the original PEP adding the `venv` module]: https://peps.python.org/pep-0405/
|
|
#[derive(Debug, PartialEq, Eq)]
|
|
struct PythonHomePath(SystemPathBuf);
|
|
|
|
impl PythonHomePath {
|
|
fn new(path: impl AsRef<SystemPath>, system: &dyn System) -> io::Result<Self> {
|
|
let path = path.as_ref();
|
|
// It's important to resolve symlinks here rather than simply making the path absolute,
|
|
// since system Python installations often only put symlinks in the "expected"
|
|
// locations for `home` and `site-packages`
|
|
let canonicalized = system.canonicalize_path(path)?;
|
|
system
|
|
.is_directory(&canonicalized)
|
|
.then_some(Self(canonicalized))
|
|
.ok_or_else(|| io::Error::other("not a directory"))
|
|
}
|
|
}
|
|
|
|
impl Deref for PythonHomePath {
|
|
type Target = SystemPath;
|
|
|
|
fn deref(&self) -> &Self::Target {
|
|
&self.0
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for PythonHomePath {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
write!(f, "`home` location `{}`", self.0)
|
|
}
|
|
}
|
|
|
|
impl PartialEq<SystemPath> for PythonHomePath {
|
|
fn eq(&self, other: &SystemPath) -> bool {
|
|
&*self.0 == other
|
|
}
|
|
}
|
|
|
|
impl PartialEq<SystemPathBuf> for PythonHomePath {
|
|
fn eq(&self, other: &SystemPathBuf) -> bool {
|
|
self == &**other
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use ruff_db::system::TestSystem;
|
|
|
|
use super::*;
|
|
|
|
impl PythonEnvironment {
|
|
fn expect_venv(self) -> VirtualEnvironment {
|
|
match self {
|
|
Self::Virtual(venv) => venv,
|
|
Self::System(_) => panic!("Expected a virtual environment"),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct VirtualEnvironmentTestCase {
|
|
system_site_packages: bool,
|
|
pyvenv_cfg_version_field: Option<&'static str>,
|
|
command_field: Option<&'static str>,
|
|
implementation_field: Option<&'static str>,
|
|
}
|
|
|
|
struct PythonEnvironmentTestCase {
|
|
system: TestSystem,
|
|
minor_version: u8,
|
|
free_threaded: bool,
|
|
origin: SysPrefixPathOrigin,
|
|
virtual_env: Option<VirtualEnvironmentTestCase>,
|
|
}
|
|
|
|
impl PythonEnvironmentTestCase {
|
|
/// Builds a mock environment, and returns the path to the environment root.
|
|
fn build(&self) -> SystemPathBuf {
|
|
let PythonEnvironmentTestCase {
|
|
system,
|
|
minor_version,
|
|
free_threaded,
|
|
origin: _,
|
|
virtual_env,
|
|
} = self;
|
|
let memory_fs = system.memory_file_system();
|
|
let unix_site_packages = if *free_threaded {
|
|
format!("lib/python3.{minor_version}t/site-packages")
|
|
} else {
|
|
format!("lib/python3.{minor_version}/site-packages")
|
|
};
|
|
|
|
let system_install_sys_prefix =
|
|
SystemPathBuf::from(&*format!("/Python3.{minor_version}"));
|
|
let (system_home_path, system_exe_path, system_site_packages_path) =
|
|
if cfg!(target_os = "windows") {
|
|
let system_home_path = system_install_sys_prefix.clone();
|
|
let system_exe_path = system_home_path.join("python.exe");
|
|
let system_site_packages_path =
|
|
system_install_sys_prefix.join(r"Lib\site-packages");
|
|
(system_home_path, system_exe_path, system_site_packages_path)
|
|
} else {
|
|
let system_home_path = system_install_sys_prefix.join("bin");
|
|
let system_exe_path = system_home_path.join("python");
|
|
let system_site_packages_path =
|
|
system_install_sys_prefix.join(&unix_site_packages);
|
|
(system_home_path, system_exe_path, system_site_packages_path)
|
|
};
|
|
memory_fs.write_file_all(system_exe_path, "").unwrap();
|
|
memory_fs
|
|
.create_directory_all(&system_site_packages_path)
|
|
.unwrap();
|
|
|
|
let Some(VirtualEnvironmentTestCase {
|
|
pyvenv_cfg_version_field,
|
|
system_site_packages,
|
|
command_field,
|
|
implementation_field,
|
|
}) = virtual_env
|
|
else {
|
|
return system_install_sys_prefix;
|
|
};
|
|
|
|
let venv_sys_prefix = SystemPathBuf::from("/.venv");
|
|
let (venv_exe, site_packages_path) = if cfg!(target_os = "windows") {
|
|
(
|
|
venv_sys_prefix.join(r"Scripts\python.exe"),
|
|
venv_sys_prefix.join(r"Lib\site-packages"),
|
|
)
|
|
} else {
|
|
(
|
|
venv_sys_prefix.join("bin/python"),
|
|
venv_sys_prefix.join(&unix_site_packages),
|
|
)
|
|
};
|
|
memory_fs.write_file_all(&venv_exe, "").unwrap();
|
|
memory_fs.create_directory_all(&site_packages_path).unwrap();
|
|
|
|
let pyvenv_cfg_path = venv_sys_prefix.join("pyvenv.cfg");
|
|
let mut pyvenv_cfg_contents = format!("home = {system_home_path}\n");
|
|
if let Some(version_field) = pyvenv_cfg_version_field {
|
|
pyvenv_cfg_contents.push_str(version_field);
|
|
pyvenv_cfg_contents.push('\n');
|
|
}
|
|
if let Some(command_field) = command_field {
|
|
pyvenv_cfg_contents.push_str(command_field);
|
|
pyvenv_cfg_contents.push('\n');
|
|
}
|
|
if let Some(implementation_field) = implementation_field {
|
|
pyvenv_cfg_contents.push_str(implementation_field);
|
|
pyvenv_cfg_contents.push('\n');
|
|
}
|
|
|
|
// Deliberately using weird casing here to test that our pyvenv.cfg parsing is case-insensitive:
|
|
if *system_site_packages {
|
|
pyvenv_cfg_contents.push_str("include-system-site-packages = TRuE\n");
|
|
}
|
|
memory_fs
|
|
.write_file_all(pyvenv_cfg_path, &pyvenv_cfg_contents)
|
|
.unwrap();
|
|
|
|
venv_sys_prefix
|
|
}
|
|
|
|
#[track_caller]
|
|
fn err(self) -> SitePackagesDiscoveryError {
|
|
PythonEnvironment::new(self.build(), self.origin, &self.system)
|
|
.expect_err("Expected environment construction to fail")
|
|
}
|
|
|
|
#[track_caller]
|
|
fn run(self) -> PythonEnvironment {
|
|
let env_path = self.build();
|
|
let env = PythonEnvironment::new(env_path.clone(), self.origin.clone(), &self.system)
|
|
.expect("Expected environment construction to succeed");
|
|
|
|
let expect_virtual_env = self.virtual_env.is_some();
|
|
match &env {
|
|
PythonEnvironment::Virtual(venv) if expect_virtual_env => {
|
|
self.assert_virtual_environment(venv, &env_path);
|
|
}
|
|
PythonEnvironment::Virtual(venv) => {
|
|
panic!(
|
|
"Expected a system environment, but got a virtual environment: {venv:?}"
|
|
);
|
|
}
|
|
PythonEnvironment::System(env) if !expect_virtual_env => {
|
|
self.assert_system_environment(env, &env_path);
|
|
}
|
|
PythonEnvironment::System(env) => {
|
|
panic!("Expected a virtual environment, but got a system environment: {env:?}");
|
|
}
|
|
}
|
|
env
|
|
}
|
|
|
|
#[track_caller]
|
|
fn assert_virtual_environment(
|
|
&self,
|
|
venv: &VirtualEnvironment,
|
|
expected_env_path: &SystemPathBuf,
|
|
) {
|
|
let self_venv = self.virtual_env.as_ref().expect(
|
|
"`assert_virtual_environment` should only be used when `virtual_env` is populated",
|
|
);
|
|
|
|
assert_eq!(
|
|
venv.root_path,
|
|
SysPrefixPath {
|
|
inner: self.system.canonicalize_path(expected_env_path).unwrap(),
|
|
origin: self.origin.clone(),
|
|
}
|
|
);
|
|
assert_eq!(
|
|
venv.include_system_site_packages,
|
|
self_venv.system_site_packages
|
|
);
|
|
|
|
if self_venv.pyvenv_cfg_version_field.is_some() {
|
|
assert_eq!(
|
|
venv.version.as_ref().map(|v| v.version),
|
|
Some(PythonVersion {
|
|
major: 3,
|
|
minor: self.minor_version
|
|
})
|
|
);
|
|
} else {
|
|
assert_eq!(venv.version, None);
|
|
}
|
|
|
|
let expected_home = if cfg!(target_os = "windows") {
|
|
SystemPathBuf::from(&*format!(r"\Python3.{}", self.minor_version))
|
|
} else {
|
|
SystemPathBuf::from(&*format!("/Python3.{}/bin", self.minor_version))
|
|
};
|
|
assert_eq!(venv.base_executable_home_path, expected_home);
|
|
|
|
let site_packages_directories = venv.site_packages_directories(&self.system).unwrap();
|
|
let expected_venv_site_packages = if cfg!(target_os = "windows") {
|
|
SystemPathBuf::from(r"\.venv\Lib\site-packages")
|
|
} else if self.free_threaded {
|
|
SystemPathBuf::from(&*format!(
|
|
"/.venv/lib/python3.{}t/site-packages",
|
|
self.minor_version
|
|
))
|
|
} else {
|
|
SystemPathBuf::from(&*format!(
|
|
"/.venv/lib/python3.{}/site-packages",
|
|
self.minor_version
|
|
))
|
|
};
|
|
|
|
let expected_system_site_packages = self.expected_system_site_packages();
|
|
|
|
if self_venv.system_site_packages {
|
|
assert_eq!(
|
|
site_packages_directories,
|
|
[expected_venv_site_packages, expected_system_site_packages].as_slice()
|
|
);
|
|
} else {
|
|
assert_eq!(
|
|
&site_packages_directories.into_iter().next().unwrap(),
|
|
&expected_venv_site_packages
|
|
);
|
|
}
|
|
|
|
let stdlib_directory = venv.real_stdlib_directory(&self.system).unwrap();
|
|
let expected_stdlib_directory = self.expected_system_stdlib();
|
|
assert_eq!(stdlib_directory, expected_stdlib_directory);
|
|
}
|
|
|
|
#[track_caller]
|
|
fn assert_system_environment(
|
|
&self,
|
|
env: &SystemEnvironment,
|
|
expected_env_path: &SystemPathBuf,
|
|
) {
|
|
assert!(
|
|
self.virtual_env.is_none(),
|
|
"`assert_system_environment` should only be used when `virtual_env` is not populated"
|
|
);
|
|
|
|
assert_eq!(
|
|
env.root_path,
|
|
SysPrefixPath {
|
|
inner: self.system.canonicalize_path(expected_env_path).unwrap(),
|
|
origin: self.origin.clone(),
|
|
}
|
|
);
|
|
|
|
let site_packages_directories = env.site_packages_directories(&self.system).unwrap();
|
|
let expected_site_packages = self.expected_system_site_packages();
|
|
assert_eq!(
|
|
site_packages_directories,
|
|
std::slice::from_ref(&expected_site_packages)
|
|
);
|
|
|
|
let stdlib_directory = env.real_stdlib_directory(&self.system).unwrap();
|
|
let expected_stdlib_directory = self.expected_system_stdlib();
|
|
assert_eq!(stdlib_directory, expected_stdlib_directory);
|
|
}
|
|
|
|
fn expected_system_site_packages(&self) -> SystemPathBuf {
|
|
let minor_version = self.minor_version;
|
|
if cfg!(target_os = "windows") {
|
|
SystemPathBuf::from(&*format!(r"\Python3.{minor_version}\Lib\site-packages"))
|
|
} else if self.free_threaded {
|
|
SystemPathBuf::from(&*format!(
|
|
"/Python3.{minor_version}/lib/python3.{minor_version}t/site-packages"
|
|
))
|
|
} else {
|
|
SystemPathBuf::from(&*format!(
|
|
"/Python3.{minor_version}/lib/python3.{minor_version}/site-packages"
|
|
))
|
|
}
|
|
}
|
|
|
|
fn expected_system_stdlib(&self) -> SystemPathBuf {
|
|
let minor_version = self.minor_version;
|
|
if cfg!(target_os = "windows") {
|
|
SystemPathBuf::from(&*format!(r"\Python3.{minor_version}\Lib"))
|
|
} else if self.free_threaded {
|
|
SystemPathBuf::from(&*format!(
|
|
"/Python3.{minor_version}/lib/python3.{minor_version}t"
|
|
))
|
|
} else {
|
|
SystemPathBuf::from(&*format!(
|
|
"/Python3.{minor_version}/lib/python3.{minor_version}"
|
|
))
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn can_find_site_packages_directory_no_virtual_env() {
|
|
// Shouldn't be converted to an mdtest because mdtest automatically creates a
|
|
// pyvenv.cfg file for you if it sees you creating a `site-packages` directory.
|
|
let test = PythonEnvironmentTestCase {
|
|
system: TestSystem::default(),
|
|
minor_version: 12,
|
|
free_threaded: false,
|
|
origin: SysPrefixPathOrigin::PythonCliFlag,
|
|
virtual_env: None,
|
|
};
|
|
test.run();
|
|
}
|
|
|
|
#[test]
|
|
fn can_find_site_packages_directory_no_virtual_env_freethreaded() {
|
|
// Shouldn't be converted to an mdtest because mdtest automatically creates a
|
|
// pyvenv.cfg file for you if it sees you creating a `site-packages` directory.
|
|
let test = PythonEnvironmentTestCase {
|
|
system: TestSystem::default(),
|
|
minor_version: 13,
|
|
free_threaded: true,
|
|
origin: SysPrefixPathOrigin::PythonCliFlag,
|
|
virtual_env: None,
|
|
};
|
|
test.run();
|
|
}
|
|
|
|
#[test]
|
|
fn cannot_find_site_packages_directory_no_virtual_env_at_origin_virtual_env_var() {
|
|
let test = PythonEnvironmentTestCase {
|
|
system: TestSystem::default(),
|
|
minor_version: 13,
|
|
free_threaded: false,
|
|
origin: SysPrefixPathOrigin::VirtualEnvVar,
|
|
virtual_env: None,
|
|
};
|
|
let err = test.err();
|
|
assert!(
|
|
matches!(err, SitePackagesDiscoveryError::NoPyvenvCfgFile(..)),
|
|
"Got {err:?}",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn cannot_find_site_packages_directory_no_virtual_env_at_origin_local_venv() {
|
|
let test = PythonEnvironmentTestCase {
|
|
system: TestSystem::default(),
|
|
minor_version: 13,
|
|
free_threaded: false,
|
|
origin: SysPrefixPathOrigin::LocalVenv,
|
|
virtual_env: None,
|
|
};
|
|
let err = test.err();
|
|
assert!(
|
|
matches!(err, SitePackagesDiscoveryError::NoPyvenvCfgFile(..)),
|
|
"Got {err:?}",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn can_find_site_packages_directory_venv_style_version_field_in_pyvenv_cfg() {
|
|
// Shouldn't be converted to an mdtest because we want to assert
|
|
// that we parsed the `version` field correctly in `test.run()`.
|
|
let test = PythonEnvironmentTestCase {
|
|
system: TestSystem::default(),
|
|
minor_version: 12,
|
|
free_threaded: false,
|
|
origin: SysPrefixPathOrigin::VirtualEnvVar,
|
|
virtual_env: Some(VirtualEnvironmentTestCase {
|
|
pyvenv_cfg_version_field: Some("version = 3.12"),
|
|
..VirtualEnvironmentTestCase::default()
|
|
}),
|
|
};
|
|
test.run();
|
|
}
|
|
|
|
#[test]
|
|
fn can_find_site_packages_directory_uv_style_version_field_in_pyvenv_cfg() {
|
|
// Shouldn't be converted to an mdtest because we want to assert
|
|
// that we parsed the `version` field correctly in `test.run()`.
|
|
let test = PythonEnvironmentTestCase {
|
|
system: TestSystem::default(),
|
|
minor_version: 12,
|
|
free_threaded: false,
|
|
origin: SysPrefixPathOrigin::VirtualEnvVar,
|
|
virtual_env: Some(VirtualEnvironmentTestCase {
|
|
pyvenv_cfg_version_field: Some("version_info = 3.12"),
|
|
..VirtualEnvironmentTestCase::default()
|
|
}),
|
|
};
|
|
test.run();
|
|
}
|
|
|
|
#[test]
|
|
fn can_find_site_packages_directory_virtualenv_style_version_field_in_pyvenv_cfg() {
|
|
// Shouldn't be converted to an mdtest because we want to assert
|
|
// that we parsed the `version` field correctly in `test.run()`.
|
|
let test = PythonEnvironmentTestCase {
|
|
system: TestSystem::default(),
|
|
minor_version: 12,
|
|
free_threaded: false,
|
|
origin: SysPrefixPathOrigin::VirtualEnvVar,
|
|
virtual_env: Some(VirtualEnvironmentTestCase {
|
|
pyvenv_cfg_version_field: Some("version_info = 3.12.0rc2"),
|
|
..VirtualEnvironmentTestCase::default()
|
|
}),
|
|
};
|
|
test.run();
|
|
}
|
|
|
|
#[test]
|
|
fn can_find_site_packages_directory_freethreaded_build() {
|
|
let test = PythonEnvironmentTestCase {
|
|
system: TestSystem::default(),
|
|
minor_version: 13,
|
|
free_threaded: true,
|
|
origin: SysPrefixPathOrigin::VirtualEnvVar,
|
|
virtual_env: Some(VirtualEnvironmentTestCase {
|
|
pyvenv_cfg_version_field: Some("version_info = 3.13"),
|
|
..VirtualEnvironmentTestCase::default()
|
|
}),
|
|
};
|
|
test.run();
|
|
}
|
|
|
|
#[test]
|
|
fn finds_system_site_packages() {
|
|
// Can't be converted to an mdtest because the system installation's `sys.prefix`
|
|
// path is at a different location relative to the `pyvenv.cfg` file's `home` value
|
|
// on Windows.
|
|
let test = PythonEnvironmentTestCase {
|
|
system: TestSystem::default(),
|
|
minor_version: 13,
|
|
free_threaded: true,
|
|
origin: SysPrefixPathOrigin::VirtualEnvVar,
|
|
virtual_env: Some(VirtualEnvironmentTestCase {
|
|
system_site_packages: true,
|
|
pyvenv_cfg_version_field: Some("version_info = 3.13"),
|
|
..VirtualEnvironmentTestCase::default()
|
|
}),
|
|
};
|
|
test.run();
|
|
}
|
|
|
|
#[test]
|
|
fn detects_pypy_implementation() {
|
|
let test = PythonEnvironmentTestCase {
|
|
system: TestSystem::default(),
|
|
minor_version: 13,
|
|
free_threaded: true,
|
|
origin: SysPrefixPathOrigin::VirtualEnvVar,
|
|
virtual_env: Some(VirtualEnvironmentTestCase {
|
|
implementation_field: Some("implementation = PyPy"),
|
|
..VirtualEnvironmentTestCase::default()
|
|
}),
|
|
};
|
|
let venv = test.run().expect_venv();
|
|
assert_eq!(venv.implementation, PythonImplementation::PyPy);
|
|
}
|
|
|
|
#[test]
|
|
fn detects_cpython_implementation() {
|
|
let test = PythonEnvironmentTestCase {
|
|
system: TestSystem::default(),
|
|
minor_version: 13,
|
|
free_threaded: true,
|
|
origin: SysPrefixPathOrigin::VirtualEnvVar,
|
|
virtual_env: Some(VirtualEnvironmentTestCase {
|
|
implementation_field: Some("implementation = CPython"),
|
|
..VirtualEnvironmentTestCase::default()
|
|
}),
|
|
};
|
|
let venv = test.run().expect_venv();
|
|
assert_eq!(venv.implementation, PythonImplementation::CPython);
|
|
}
|
|
|
|
#[test]
|
|
fn detects_graalpy_implementation() {
|
|
let test = PythonEnvironmentTestCase {
|
|
system: TestSystem::default(),
|
|
minor_version: 13,
|
|
free_threaded: true,
|
|
origin: SysPrefixPathOrigin::VirtualEnvVar,
|
|
virtual_env: Some(VirtualEnvironmentTestCase {
|
|
implementation_field: Some("implementation = GraalVM"),
|
|
..VirtualEnvironmentTestCase::default()
|
|
}),
|
|
};
|
|
let venv = test.run().expect_venv();
|
|
assert_eq!(venv.implementation, PythonImplementation::GraalPy);
|
|
}
|
|
|
|
#[test]
|
|
fn detects_unknown_implementation() {
|
|
let test = PythonEnvironmentTestCase {
|
|
system: TestSystem::default(),
|
|
minor_version: 13,
|
|
free_threaded: true,
|
|
origin: SysPrefixPathOrigin::VirtualEnvVar,
|
|
virtual_env: Some(VirtualEnvironmentTestCase::default()),
|
|
};
|
|
let venv = test.run().expect_venv();
|
|
assert_eq!(venv.implementation, PythonImplementation::Unknown);
|
|
}
|
|
|
|
#[test]
|
|
fn reject_env_that_does_not_exist() {
|
|
let system = TestSystem::default();
|
|
assert!(matches!(
|
|
PythonEnvironment::new("/env", SysPrefixPathOrigin::PythonCliFlag, &system),
|
|
Err(SitePackagesDiscoveryError::PathNotExecutableOrDirectory(..))
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn reject_env_that_is_not_a_directory() {
|
|
let system = TestSystem::default();
|
|
system
|
|
.memory_file_system()
|
|
.write_file_all("/env", "")
|
|
.unwrap();
|
|
assert!(matches!(
|
|
PythonEnvironment::new("/env", SysPrefixPathOrigin::PythonCliFlag, &system),
|
|
Err(SitePackagesDiscoveryError::PathNotExecutableOrDirectory(..))
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn cannot_read_lib_directory() {
|
|
let system = TestSystem::default();
|
|
system
|
|
.memory_file_system()
|
|
.create_directory_all("/env")
|
|
.unwrap();
|
|
// Environment creation succeeds, but site-packages retrieval fails reading the `lib`
|
|
// directory
|
|
let env =
|
|
PythonEnvironment::new("/env", SysPrefixPathOrigin::PythonCliFlag, &system).unwrap();
|
|
let site_packages = env.site_packages_paths(&system);
|
|
if cfg!(unix) {
|
|
assert!(
|
|
matches!(
|
|
site_packages,
|
|
Err(SitePackagesDiscoveryError::CouldNotReadLibDirectory(..)),
|
|
),
|
|
"Got {site_packages:?}",
|
|
);
|
|
} else {
|
|
// On Windows, we look for `Lib/site-packages` directly instead of listing the entries
|
|
// of `lib/...` — so we don't see the intermediate failure
|
|
assert!(
|
|
matches!(
|
|
site_packages,
|
|
Err(SitePackagesDiscoveryError::NoSitePackagesDirFound(..)),
|
|
),
|
|
"Got {site_packages:?}",
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn cannot_find_site_packages_directory() {
|
|
let system = TestSystem::default();
|
|
if cfg!(unix) {
|
|
system
|
|
.memory_file_system()
|
|
.create_directory_all("/env/lib")
|
|
.unwrap();
|
|
} else {
|
|
system
|
|
.memory_file_system()
|
|
.create_directory_all("/env/Lib")
|
|
.unwrap();
|
|
}
|
|
// Environment creation succeeds, but site-packages retrieval fails
|
|
let env =
|
|
PythonEnvironment::new("/env", SysPrefixPathOrigin::PythonCliFlag, &system).unwrap();
|
|
let site_packages = env.site_packages_paths(&system);
|
|
assert!(
|
|
matches!(
|
|
site_packages,
|
|
Err(SitePackagesDiscoveryError::NoSitePackagesDirFound(..)),
|
|
),
|
|
"Got {site_packages:?}",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn parsing_pyvenv_cfg_with_key_but_no_value_fails() {
|
|
let system = TestSystem::default();
|
|
let memory_fs = system.memory_file_system();
|
|
let pyvenv_cfg_path = SystemPathBuf::from("/.venv/pyvenv.cfg");
|
|
memory_fs
|
|
.write_file_all(&pyvenv_cfg_path, "home =")
|
|
.unwrap();
|
|
let venv_result =
|
|
PythonEnvironment::new("/.venv", SysPrefixPathOrigin::VirtualEnvVar, &system);
|
|
assert!(matches!(
|
|
venv_result,
|
|
Err(SitePackagesDiscoveryError::PyvenvCfgParseError(
|
|
path,
|
|
PyvenvCfgParseErrorKind::MalformedKeyValuePair { line_number }
|
|
))
|
|
if path == pyvenv_cfg_path && Some(line_number) == NonZeroUsize::new(1)
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn parsing_pyvenv_cfg_with_value_but_no_key_fails() {
|
|
let system = TestSystem::default();
|
|
let memory_fs = system.memory_file_system();
|
|
let pyvenv_cfg_path = SystemPathBuf::from("/.venv/pyvenv.cfg");
|
|
memory_fs
|
|
.write_file_all(&pyvenv_cfg_path, "= whatever")
|
|
.unwrap();
|
|
let venv_result =
|
|
PythonEnvironment::new("/.venv", SysPrefixPathOrigin::VirtualEnvVar, &system);
|
|
assert!(matches!(
|
|
venv_result,
|
|
Err(SitePackagesDiscoveryError::PyvenvCfgParseError(
|
|
path,
|
|
PyvenvCfgParseErrorKind::MalformedKeyValuePair { line_number }
|
|
))
|
|
if path == pyvenv_cfg_path && Some(line_number) == NonZeroUsize::new(1)
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn parsing_pyvenv_cfg_with_no_home_key_fails() {
|
|
let system = TestSystem::default();
|
|
let memory_fs = system.memory_file_system();
|
|
let pyvenv_cfg_path = SystemPathBuf::from("/.venv/pyvenv.cfg");
|
|
memory_fs.write_file_all(&pyvenv_cfg_path, "").unwrap();
|
|
let venv_result =
|
|
PythonEnvironment::new("/.venv", SysPrefixPathOrigin::VirtualEnvVar, &system);
|
|
assert!(matches!(
|
|
venv_result,
|
|
Err(SitePackagesDiscoveryError::PyvenvCfgParseError(
|
|
path,
|
|
PyvenvCfgParseErrorKind::NoHomeKey
|
|
))
|
|
if path == pyvenv_cfg_path
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn parsing_pyvenv_cfg_with_invalid_home_key_fails() {
|
|
let system = TestSystem::default();
|
|
let memory_fs = system.memory_file_system();
|
|
let pyvenv_cfg_path = SystemPathBuf::from("/.venv/pyvenv.cfg");
|
|
memory_fs
|
|
.write_file_all(&pyvenv_cfg_path, "home = foo")
|
|
.unwrap();
|
|
let venv_result =
|
|
PythonEnvironment::new("/.venv", SysPrefixPathOrigin::VirtualEnvVar, &system);
|
|
assert!(matches!(
|
|
venv_result,
|
|
Err(SitePackagesDiscoveryError::PyvenvCfgParseError(
|
|
path,
|
|
PyvenvCfgParseErrorKind::InvalidHomeValue(_)
|
|
))
|
|
if path == pyvenv_cfg_path
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn pyvenv_cfg_with_carriage_return_line_endings_parses() {
|
|
let pyvenv_cfg = "home = /somewhere/python\r\nversion_info = 3.13\r\nimplementation = PyPy";
|
|
let parsed = PyvenvCfgParser::new(pyvenv_cfg).parse().unwrap();
|
|
assert_eq!(parsed.base_executable_home_path, Some("/somewhere/python"));
|
|
let version = parsed.version.unwrap();
|
|
assert_eq!(version.0, "3.13");
|
|
assert_eq!(&pyvenv_cfg[version.1], version.0);
|
|
assert_eq!(parsed.implementation, PythonImplementation::PyPy);
|
|
}
|
|
|
|
#[test]
|
|
fn pyvenv_cfg_with_strange_whitespace_parses() {
|
|
let pyvenv_cfg = " home= /a path with whitespace/python\t \t \nversion_info = 3.13 \n\n\n\nimplementation =PyPy";
|
|
let parsed = PyvenvCfgParser::new(pyvenv_cfg).parse().unwrap();
|
|
assert_eq!(
|
|
parsed.base_executable_home_path,
|
|
Some("/a path with whitespace/python")
|
|
);
|
|
let version = parsed.version.unwrap();
|
|
assert_eq!(version.0, "3.13");
|
|
assert_eq!(&pyvenv_cfg[version.1], version.0);
|
|
assert_eq!(parsed.implementation, PythonImplementation::PyPy);
|
|
}
|
|
}
|