mod version;
use std::hash::{Hash, Hasher};
use std::io;
use std::num::NonZeroUsize;
use std::ops::Deref;
use std::str::FromStr;
use std::{fmt, sync::Arc};
use annotate_snippets::{AnnotationKind, Level, Renderer, Snippet};
use camino::Utf8Component;
use indexmap::IndexSet;
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;
pub use version::{PythonVersionFileSource, PythonVersionSource, PythonVersionWithSource};
type SitePackagesDiscoveryResult<T> = Result<T, SitePackagesDiscoveryError>;
type StdlibDiscoveryResult<T> = Result<T, StdlibDiscoveryError>;
#[derive(Debug, PartialEq, Eq, Default)]
pub struct SitePackagesPaths(IndexSet<SitePackagesPath>);
#[derive(Debug, Clone)]
struct SitePackagesPath {
path: SystemPathBuf,
settings_diagnostic_path: Option<SystemPathBuf>,
}
impl SitePackagesPath {
fn new(path: SystemPathBuf, settings_diagnostic_path: Option<SystemPathBuf>) -> Self {
Self {
path,
settings_diagnostic_path,
}
}
fn into_path(self) -> SystemPathBuf {
self.path
}
}
impl PartialEq for SitePackagesPath {
fn eq(&self, other: &Self) -> bool {
self.path == other.path
}
}
impl Eq for SitePackagesPath {}
impl Hash for SitePackagesPath {
fn hash<H: Hasher>(&self, state: &mut H) {
self.path.hash(state);
}
}
impl SitePackagesPaths {
fn is_empty(&self) -> bool {
self.0.is_empty()
}
fn insert_with_settings_diagnostic_path(
&mut self,
path: SystemPathBuf,
settings_diagnostic_path: Option<SystemPathBuf>,
) {
self.0
.insert(SitePackagesPath::new(path, settings_diagnostic_path));
}
fn extend(&mut self, other: Self) {
self.0.extend(other.0);
}
#[must_use]
pub fn concatenate(mut self, other: Self) -> Self {
for path in other.0 {
self.0.insert(path);
}
self
}
pub fn python_version_from_layout(&self) -> Option<PythonVersionWithSource> {
if cfg!(windows) {
return None;
}
let primary_site_packages = self.0.first()?;
let mut site_packages_ancestor_components = primary_site_packages
.path
.components()
.rev()
.skip(1)
.map(|c| {
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),
source: primary_site_packages
.settings_diagnostic_path
.clone()
.map(|path| PythonVersionFileSource::new(Arc::new(path), None)),
};
Some(PythonVersionWithSource { version, source })
}
pub fn into_vec(self) -> Vec<SystemPathBuf> {
self.into_iter().collect()
}
}
impl fmt::Display for SitePackagesPaths {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_list()
.entries(self.0.iter().map(|path| &path.path))
.finish()
}
}
fn settings_diagnostic_path_from_sys_prefix(
sys_prefix: &SystemPath,
system: &dyn System,
) -> Option<SystemPathBuf> {
let pyvenv_cfg = sys_prefix.join("pyvenv.cfg");
if system.is_file(&pyvenv_cfg) {
return Some(pyvenv_cfg);
}
let bin_dir = if cfg!(windows) {
sys_prefix.to_path_buf()
} else {
sys_prefix.join("bin")
};
for candidate in [
bin_dir.join("python3"),
bin_dir.join("python"),
bin_dir.join("pypy3"),
bin_dir.join("pypy"),
] {
if system.is_file(&candidate) {
return Some(candidate);
}
}
let entries = system.read_directory(&bin_dir).ok()?;
for entry in entries.flatten() {
if entry.file_type().is_directory() {
continue;
}
let path = entry.into_path();
if let Some(file_name) = path.file_name()
&& is_versioned_interpreter_path(file_name)
{
return Some(path);
}
}
None
}
fn is_versioned_interpreter_path(file_name: &str) -> bool {
matches!(
PythonInterpreterLayout::from_file_name(file_name),
Some(PythonInterpreterLayout {
version: Some(_),
..
})
)
}
impl<const N: usize> From<[SystemPathBuf; N]> for SitePackagesPaths {
fn from(paths: [SystemPathBuf; N]) -> Self {
Self(
paths
.into_iter()
.map(|path| SitePackagesPath::new(path, None))
.collect(),
)
}
}
impl IntoIterator for SitePackagesPaths {
type Item = SystemPathBuf;
type IntoIter = std::vec::IntoIter<SystemPathBuf>;
fn into_iter(self) -> Self::IntoIter {
self.0
.into_iter()
.map(SitePackagesPath::into_path)
.collect::<Vec<_>>()
.into_iter()
}
}
impl PartialEq<&[SystemPathBuf]> for SitePackagesPaths {
fn eq(&self, other: &&[SystemPathBuf]) -> bool {
self.0.iter().map(|path| &path.path).eq(other.iter())
}
}
#[derive(Debug)]
pub enum PythonEnvironment {
Virtual(VirtualEnvironment),
System(SystemEnvironment),
}
impl PythonEnvironment {
pub fn discover(
project_root: Option<&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);
}
if let Some(project_root) = project_root {
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);
}
if let Some(env) = environment_from_binary(system, "python3")
.or_else(|| environment_from_binary(system, "python"))
{
return Ok(Some(env));
}
Ok(None)
}
pub fn new(
path: impl AsRef<SystemPath>,
origin: SysPrefixPathOrigin,
system: &dyn System,
) -> SitePackagesDiscoveryResult<Self> {
let path = PythonEnvironmentPath::new(path.as_ref(), origin, system)?;
match VirtualEnvironment::new(path.sys_prefix(), system) {
Ok(venv) => Ok(Self::Virtual(venv)),
Err(SitePackagesDiscoveryError::NoPyvenvCfgFile(_, _, _))
if !path.origin().must_be_virtual_env() =>
{
tracing::trace!(
"Resolved system Python environment from `{}`",
path.selected_path()
);
Ok(Self::System(SystemEnvironment { path }))
}
Err(err) => Err(err),
}
}
pub fn sys_prefix(&self) -> &SysPrefixPath {
match self {
Self::Virtual(env) => &env.root_path,
Self::System(env) => env.path.sys_prefix(),
}
}
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),
}
}
pub fn origin(&self) -> &SysPrefixPathOrigin {
match self {
Self::Virtual(env) => &env.root_path.origin,
Self::System(env) => env.path.origin(),
}
}
pub fn is_virtual(&self) -> bool {
matches!(self, Self::Virtual(_))
}
}
#[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
}
}
#[derive(Debug, Clone, Copy, Eq, PartialEq, strum_macros::EnumIter)]
enum InstallationDir {
SitePackages,
DistPackages,
}
impl InstallationDir {
const fn as_str(self) -> &'static str {
match self {
Self::SitePackages => "site-packages",
Self::DistPackages => "dist-packages",
}
}
}
impl std::fmt::Display for InstallationDir {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
pub(crate) enum PythonImplementation {
CPython,
PyPy,
GraalPy,
#[default]
Unknown,
}
impl PythonImplementation {
const fn is_cpython(self) -> bool {
matches!(self, Self::CPython)
}
fn split_executable_name(file_name: &str) -> Option<(Self, &str)> {
if let Some(suffix) = file_name.strip_prefix("python") {
Some((Self::CPython, suffix))
} else if let Some(suffix) = file_name.strip_prefix("pypy") {
Some((Self::PyPy, suffix))
} else {
Some((Self::GraalPy, file_name.strip_prefix("graalpy")?))
}
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
struct PythonInterpreterLayout {
version: Option<PythonVersion>,
implementation: PythonImplementation,
variant: PythonBuildVariant,
}
impl PythonInterpreterLayout {
const fn unknown(implementation: PythonImplementation, version: Option<PythonVersion>) -> Self {
Self {
version,
implementation,
variant: PythonBuildVariant::Unknown,
}
}
fn from_path(path: &SystemPath, system: &dyn System) -> Option<Self> {
let mut layout = Self::from_file_name(path.file_name()?)?;
if layout.implementation.is_cpython()
&& layout.variant.is_unknown()
&& let Some(version) = layout.version
&& version.free_threaded_build_available()
&& let Some(parent) = path.parent()
{
let mut variant = PythonBuildVariant::Default;
for abi_flags in ["t", "td"] {
let free_threaded_executable = parent.join(format!(
"python{version}{abi_flags}{}",
std::env::consts::EXE_SUFFIX
));
if !system.is_file(&free_threaded_executable) {
continue;
}
match system.is_same_file(path, &free_threaded_executable) {
Ok(true) => {
variant = PythonBuildVariant::FreeThreaded;
break;
}
Ok(false) => {}
Err(_) => variant = PythonBuildVariant::Unknown,
}
}
layout.variant = variant;
}
Some(layout)
}
fn from_file_name(file_name: &str) -> Option<Self> {
let file_name = file_name
.strip_suffix(std::env::consts::EXE_SUFFIX)
.unwrap_or(file_name);
let (implementation, version) = PythonImplementation::split_executable_name(file_name)?;
let (version, variant) = PythonBuildVariant::split_executable_suffix(version);
let version = match PythonVersion::from_str(version) {
Ok(version) => version,
Err(_) if variant == PythonBuildVariant::FreeThreaded => return None,
Err(_) => {
let implementation = if implementation.is_cpython() {
PythonImplementation::Unknown
} else {
implementation
};
return Some(Self::unknown(implementation, None));
}
};
if variant == PythonBuildVariant::FreeThreaded
&& (!implementation.is_cpython() || !version.free_threaded_build_available())
{
return None;
}
let variant = if variant == PythonBuildVariant::Default
&& implementation.is_cpython()
&& version.free_threaded_build_available()
{
PythonBuildVariant::Unknown
} else {
variant
};
Some(Self {
version: Some(version),
implementation,
variant,
})
}
fn relative_stdlib_path(self) -> Option<String> {
let version = self.version?;
match self.implementation {
PythonImplementation::CPython => {
let variant = self.variant.suffix();
Some(format!("lib/python{version}{variant}"))
}
PythonImplementation::GraalPy => Some(format!("lib/python{version}")),
PythonImplementation::PyPy => Some(format!("lib/pypy{version}")),
PythonImplementation::Unknown => None,
}
}
fn free_threaded_stdlib_fallback_path(self) -> Option<String> {
let version = self.version?;
(self.implementation.is_cpython()
&& self.variant.is_unknown()
&& version.free_threaded_build_available())
.then(|| format!("lib/python{version}t"))
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
enum PythonBuildVariant {
#[default]
Unknown,
Default,
FreeThreaded,
}
impl PythonBuildVariant {
const fn is_unknown(self) -> bool {
matches!(self, Self::Unknown)
}
fn split_executable_suffix(version: &str) -> (&str, Self) {
let version = version.strip_suffix('d').unwrap_or(version);
if let Some(version) = version.strip_suffix('t') {
(version, Self::FreeThreaded)
} else {
(version, Self::Default)
}
}
const fn suffix(self) -> &'static str {
match self {
Self::Unknown | Self::Default => "",
Self::FreeThreaded => "t",
}
}
}
#[derive(Debug)]
pub struct VirtualEnvironment {
root_path: SysPrefixPath,
base_executable_home_path: Option<PythonHomePath>,
include_system_site_packages: bool,
version: Option<PythonVersionWithSource>,
implementation: PythonImplementation,
parent_environment: Option<Box<PythonEnvironment>>,
}
impl VirtualEnvironment {
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.clone(),
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;
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 = match PythonHomePath::new(base_executable_home_path, system)
{
Ok(home_path) => Some(home_path),
Err(io_err) if !include_system_site_packages => {
tracing::warn!(
"Failed to resolve the `home` value in the `pyvenv.cfg` file at \
`{pyvenv_cfg_path}`. Goto-definition for stdlib-defined items will not \
be able to jump to the real implementation. Underlying error: {io_err}"
);
None
}
Err(io_err) => {
return Err(SitePackagesDiscoveryError::PyvenvCfgParseError(
pyvenv_cfg_path.clone(),
PyvenvCfgParseErrorKind::InvalidHomeValue(io_err),
));
}
};
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
};
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.clone(),
base_executable_home_path,
include_system_site_packages,
version,
implementation,
parent_environment,
};
tracing::trace!("Resolved metadata for virtual environment: {metadata:?}");
Ok(metadata)
}
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 layout = PythonInterpreterLayout::unknown(*implementation, version);
let mut site_packages_directories =
site_packages_directories_from_sys_prefix(root_path, layout, 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 = base_executable_home_path
.as_ref()
.and_then(SysPrefixPath::from_executable_home_path);
if let Some(sys_prefix_path) = system_sys_prefix {
match site_packages_directories_from_sys_prefix(&sys_prefix_path, layout, 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)
}
fn real_stdlib_directory(&self, system: &dyn System) -> StdlibDiscoveryResult<SystemPathBuf> {
let VirtualEnvironment {
base_executable_home_path,
implementation,
version,
root_path,
include_system_site_packages: _,
parent_environment: _,
} = self;
let version = version.as_ref().map(|v| v.version);
let layout = PythonInterpreterLayout::unknown(*implementation, version);
if let Some(system_sys_prefix) = base_executable_home_path
.as_ref()
.and_then(|home_path| SysPrefixPath::from_executable_home_path_real(system, home_path))
{
let real_stdlib_directory =
real_stdlib_directory_from_sys_prefix(&system_sys_prefix, layout, 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))
}
}
}
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
pub(crate) enum CondaEnvironmentKind {
Base,
Child,
}
impl CondaEnvironmentKind {
fn from_prefix_path(system: &dyn System, path: &SystemPath) -> Self {
if let Ok(conda_root) = system.env_var(EnvVars::CONDA_ROOT)
&& path.as_str() == conda_root
{
return Self::Base;
}
let Ok(current_env) = system.env_var(EnvVars::CONDA_DEFAULT_ENV) else {
return Self::Child;
};
if current_env == "base" || current_env == "root" {
return Self::Base;
}
let Some(name) = path.file_name() else {
return Self::Child;
};
if name == current_env {
Self::Child
} else {
Self::Base
}
}
}
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)
}
fn environment_from_binary(system: &dyn System, binary: &str) -> Option<PythonEnvironment> {
let binary = system.which(binary).ok()?;
let env = PythonEnvironment::new(binary, SysPrefixPathOrigin::PythonBinary, system).ok()?;
env.site_packages_paths(system).ok()?;
Some(env)
}
#[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(),
}
}
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)
}
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('=') {
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),
"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(())
}
}
#[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>,
}
#[derive(Debug)]
pub struct SystemEnvironment {
path: PythonEnvironmentPath,
}
impl SystemEnvironment {
fn site_packages_directories(
&self,
system: &dyn System,
) -> SitePackagesDiscoveryResult<SitePackagesPaths> {
let site_packages_directories = site_packages_directories_from_sys_prefix(
self.path.sys_prefix(),
self.path.interpreter_layout().unwrap_or_default(),
system,
)?;
tracing::debug!(
"Resolved site-packages directories for this environment are: \
{site_packages_directories}"
);
Ok(site_packages_directories)
}
fn real_stdlib_directory(&self, system: &dyn System) -> StdlibDiscoveryResult<SystemPathBuf> {
let stdlib_directory = real_stdlib_directory_from_sys_prefix(
self.path.sys_prefix(),
self.path.interpreter_layout().unwrap_or_default(),
system,
)?;
tracing::debug!(
"Resolved real stdlib directory for this environment is: {stdlib_directory:?}"
);
Ok(stdlib_directory)
}
}
#[derive(Debug)]
pub enum SitePackagesDiscoveryError {
CanonicalizationError(
SystemPathBuf,
SysPrefixPathOrigin,
io::Error,
Box<dyn System>,
),
PathNotExecutableOrDirectory(
SystemPathBuf,
SysPrefixPathOrigin,
Option<io::Error>,
Box<dyn System>,
),
NoPyvenvCfgFile(SysPrefixPath, io::Error, Box<dyn System>),
PyvenvCfgParseError(SystemPathBuf, PyvenvCfgParseErrorKind),
CouldNotReadLibDirectory(SysPrefixPath, Box<dyn System>),
NoSitePackagesDirFound(SysPrefixPath, Box<dyn System>),
}
#[derive(Debug)]
pub enum StdlibDiscoveryError {
NoStdlibFound(SysPrefixPath, Box<dyn System>),
CouldNotReadLibDirectory(SysPrefixPath, io::Error, Box<dyn System>),
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 = AnnotationKind::Primary.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::ERROR
.no_name()
.primary_title(&primary_message)
.element(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]))
}
#[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}"
)
}
}
}
}
fn probe_package_dirs(
prefix_dir: &SystemPath,
suffix: InstallationDir,
layout: PythonInterpreterLayout,
system: &dyn System,
settings_diagnostic_path: Option<&SystemPath>,
directories: &mut SitePackagesPaths,
) {
let PythonInterpreterLayout {
version: Some(python_version),
implementation,
variant,
} = layout
else {
return;
};
let settings_diagnostic_path = || settings_diagnostic_path.map(SystemPath::to_path_buf);
let probe_cpython_path = |directories: &mut SitePackagesPaths| {
let path = |variant: PythonBuildVariant| {
prefix_dir.join(format!(
"python{python_version}{variant}/{suffix}",
variant = variant.suffix()
))
};
let path = match variant {
PythonBuildVariant::Default | PythonBuildVariant::FreeThreaded => path(variant),
PythonBuildVariant::Unknown => {
let regular = path(PythonBuildVariant::Default);
if system.is_directory(®ular) || !python_version.free_threaded_build_available()
{
regular
} else {
path(PythonBuildVariant::FreeThreaded)
}
}
};
if system.is_directory(&path) {
directories.insert_with_settings_diagnostic_path(path, settings_diagnostic_path());
true
} else {
false
}
};
let probe_pypy_path = |directories: &mut SitePackagesPaths| {
let path = prefix_dir.join(format!("pypy{python_version}/{suffix}"));
if system.is_directory(&path) {
directories.insert_with_settings_diagnostic_path(path, settings_diagnostic_path());
true
} else {
false
}
};
match implementation {
PythonImplementation::CPython | PythonImplementation::GraalPy => {
probe_cpython_path(directories);
}
PythonImplementation::PyPy => {
probe_pypy_path(directories);
}
PythonImplementation::Unknown => {
if !probe_cpython_path(directories) {
probe_pypy_path(directories);
}
}
}
}
fn discover_package_dirs(
prefix_dir: &SystemPath,
suffixes: &(impl IntoIterator<Item = InstallationDir> + Clone),
implementation: PythonImplementation,
system: &dyn System,
settings_diagnostic_path: Option<&SystemPath>,
directories: &mut SitePackagesPaths,
) {
let Ok(dir_iter) = system.read_directory(prefix_dir) else {
return;
};
for entry_result in dir_iter {
let Ok(entry) = entry_result else { continue };
if !entry.file_type().is_directory() {
continue;
}
let 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 `{prefix_dir}`"
)
});
let matches_implementation = match implementation {
PythonImplementation::CPython | PythonImplementation::GraalPy => {
name.starts_with("python3.")
}
PythonImplementation::PyPy => name.starts_with("pypy3."),
PythonImplementation::Unknown => {
name.starts_with("python3.") || name.starts_with("pypy3.")
}
};
if matches_implementation {
for suffix in suffixes.clone() {
let candidate = path.join(suffix.as_str());
if system.is_directory(&candidate) {
directories.insert_with_settings_diagnostic_path(
candidate,
settings_diagnostic_path.map(SystemPath::to_path_buf),
);
}
}
}
}
}
fn site_packages_directories_from_sys_prefix(
sys_prefix_path: &SysPrefixPath,
layout: PythonInterpreterLayout,
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(),
)
});
}
let mut directories = SitePackagesPaths::default();
let is_debian_system_prefix = sys_prefix_path.as_str() == "/usr";
let settings_diagnostic_path =
settings_diagnostic_path_from_sys_prefix(sys_prefix_path, system);
if layout.version.is_some() {
if is_debian_system_prefix {
probe_package_dirs(
SystemPath::new("/usr/local/lib"),
InstallationDir::DistPackages,
layout,
system,
settings_diagnostic_path.as_deref(),
&mut directories,
);
}
for lib_dir in UnixLibDir::iter() {
let prefix = sys_prefix_path.join(lib_dir);
for suffix in InstallationDir::iter() {
probe_package_dirs(
&prefix,
suffix,
layout,
system,
settings_diagnostic_path.as_deref(),
&mut directories,
);
}
}
} else {
if is_debian_system_prefix {
discover_package_dirs(
SystemPath::new("/usr/local/lib"),
&[InstallationDir::DistPackages],
layout.implementation,
system,
settings_diagnostic_path.as_deref(),
&mut directories,
);
}
for lib_dir in UnixLibDir::iter() {
let prefix = sys_prefix_path.join(lib_dir);
discover_package_dirs(
&prefix,
&InstallationDir::iter(),
layout.implementation,
system,
settings_diagnostic_path.as_deref(),
&mut directories,
);
}
}
if matches!(
layout.implementation,
PythonImplementation::CPython | PythonImplementation::Unknown
) {
let debian_dist_packages = sys_prefix_path.join("lib/python3/dist-packages");
if system.is_directory(&debian_dist_packages) {
directories.insert_with_settings_diagnostic_path(
debian_dist_packages,
settings_diagnostic_path,
);
}
}
if directories.is_empty() {
let any_lib_dir =
UnixLibDir::iter().any(|lib_dir| system.is_directory(&sys_prefix_path.join(lib_dir)));
if any_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)
}
}
fn real_stdlib_directory_from_sys_prefix(
sys_prefix_path: &SysPrefixPath,
layout: PythonInterpreterLayout,
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 let Some(expected_relative_path) = layout.relative_stdlib_path() {
let expected_absolute_path = sys_prefix_path.join(expected_relative_path);
if system.is_directory(&expected_absolute_path) {
return Ok(expected_absolute_path);
}
if let Some(alternative_relative_path) = layout.free_threaded_stdlib_fallback_path() {
let alternative_path = sys_prefix_path.join(alternative_relative_path);
if system.is_directory(&alternative_path) {
return Ok(alternative_path);
}
}
}
for entry_result in system
.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(),
))
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct SysPrefixPath {
inner: SystemPathBuf,
origin: SysPrefixPathOrigin,
}
fn sys_prefix_from_executable_path(path: &SystemPath) -> Option<&SystemPath> {
if cfg!(windows) {
let parent = path.parent()?;
if parent.file_name() == Some("Scripts") {
parent.parent()
} else {
Some(parent)
}
} else {
path.ancestors().nth(2)
}
}
#[derive(Debug)]
enum PythonEnvironmentPath {
Prefix(SysPrefixPath),
Executable {
path: SystemPathBuf,
sys_prefix: SysPrefixPath,
layout: Option<PythonInterpreterLayout>,
},
}
impl PythonEnvironmentPath {
fn new(
unvalidated_path: &SystemPath,
origin: SysPrefixPathOrigin,
system: &dyn System,
) -> SitePackagesDiscoveryResult<Self> {
let executable_path = (!origin.must_point_directly_to_sys_prefix()
&& system.is_file(unvalidated_path)
&& unvalidated_path.file_name().is_some_and(|name| {
PythonImplementation::split_executable_name(name).is_some()
|| name.eq_ignore_ascii_case(&format!("ty{}", std::env::consts::EXE_SUFFIX))
}))
.then_some(unvalidated_path);
let unvalidated_sys_prefix = if let Some(executable_path) = executable_path {
let Some(sys_prefix) = sys_prefix_from_executable_path(executable_path) else {
return Err(SitePackagesDiscoveryError::PathNotExecutableOrDirectory(
unvalidated_path.to_path_buf(),
origin,
None,
system.dyn_clone(),
));
};
sys_prefix
} else {
unvalidated_path
};
let sys_prefix = match system.canonicalize_path(unvalidated_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(),
));
}
let layout = executable_path.and_then(|executable_path| {
let canonical_layout = system
.canonicalize_path(executable_path)
.ok()
.and_then(|path| PythonInterpreterLayout::from_path(&path, system));
if let Some(layout) = canonical_layout
&& layout.version.is_some()
{
return Some(layout);
}
let direct_layout = PythonInterpreterLayout::from_path(executable_path, system);
if let Some(layout) = direct_layout
&& layout.version.is_some()
{
return Some(layout);
}
canonical_layout.or(direct_layout)
});
let sys_prefix = SysPrefixPath {
inner: sys_prefix,
origin,
};
if let Some(executable_path) = executable_path {
Ok(Self::Executable {
path: executable_path.to_path_buf(),
sys_prefix,
layout,
})
} else {
Ok(Self::Prefix(sys_prefix))
}
}
fn sys_prefix(&self) -> &SysPrefixPath {
match self {
Self::Prefix(sys_prefix) | Self::Executable { sys_prefix, .. } => sys_prefix,
}
}
fn origin(&self) -> &SysPrefixPathOrigin {
&self.sys_prefix().origin
}
fn interpreter_layout(&self) -> Option<PythonInterpreterLayout> {
match self {
Self::Executable { layout, .. } => *layout,
Self::Prefix(_) => None,
}
}
fn selected_path(&self) -> &SystemPath {
match self {
Self::Prefix(sys_prefix) => sys_prefix,
Self::Executable { path, .. } => path,
}
}
}
impl SysPrefixPath {
fn from_executable_home_path(path: &PythonHomePath) -> Option<Self> {
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,
})
}
}
fn from_executable_home_path_real(system: &dyn System, path: &PythonHomePath) -> Option<Self> {
let mut home_path = path.0.clone();
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;
}
}
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
}
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum SysPrefixPathOrigin {
ConfigFileSetting(Arc<SystemPathBuf>, Option<TextRange>),
ScriptMetadataSetting,
PythonCliFlag,
Editor,
UvWorkspace,
VirtualEnvVar,
CondaPrefixVar,
DerivedFromPyvenvCfg,
LocalVenv,
SelfEnvironment,
PythonBinary,
}
impl SysPrefixPathOrigin {
const fn must_be_virtual_env(&self) -> bool {
match self {
Self::LocalVenv | Self::VirtualEnvVar => true,
Self::ConfigFileSetting(..)
| Self::ScriptMetadataSetting
| Self::PythonCliFlag
| Self::Editor
| Self::DerivedFromPyvenvCfg
| Self::CondaPrefixVar
| Self::PythonBinary
| Self::UvWorkspace
| Self::SelfEnvironment => false,
}
}
const fn must_point_directly_to_sys_prefix(&self) -> bool {
match self {
Self::PythonCliFlag
| Self::ConfigFileSetting(..)
| Self::ScriptMetadataSetting
| Self::Editor
| Self::SelfEnvironment
| Self::PythonBinary => false,
Self::VirtualEnvVar
| Self::CondaPrefixVar
| Self::DerivedFromPyvenvCfg
| Self::LocalVenv
| Self::UvWorkspace => true,
}
}
pub const fn allows_concatenation_with_self_environment(&self) -> bool {
match self {
Self::SelfEnvironment
| Self::CondaPrefixVar
| Self::VirtualEnvVar
| Self::Editor
| Self::DerivedFromPyvenvCfg
| Self::ConfigFileSetting(..)
| Self::ScriptMetadataSetting
| Self::PythonCliFlag
| Self::PythonBinary
| Self::UvWorkspace => false,
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::ScriptMetadataSetting => {
f.write_str("`environment.python` setting in script metadata")
}
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::Editor => f.write_str("selected interpreter in your editor"),
Self::UvWorkspace => f.write_str("uv workspace environment"),
Self::SelfEnvironment => f.write_str("ty environment"),
Self::PythonBinary => f.write_str("Python binary discovered in $PATH"),
}
}
}
#[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();
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;
#[cfg(unix)]
use ruff_db::system::{OsSystem, SystemPath};
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>,
}
impl VirtualEnvironmentTestCase {
fn implementation(&self) -> PythonImplementation {
let Some((_, implementation)) = self
.implementation_field
.and_then(|field| field.split_once('='))
else {
return PythonImplementation::CPython;
};
match implementation.trim().to_ascii_lowercase().as_str() {
"cpython" => PythonImplementation::CPython,
"graalvm" => PythonImplementation::GraalPy,
"pypy" => PythonImplementation::PyPy,
_ => PythonImplementation::Unknown,
}
}
}
struct PythonEnvironmentTestCase {
system: TestSystem,
minor_version: u8,
free_threaded: bool,
origin: SysPrefixPathOrigin,
virtual_env: Option<VirtualEnvironmentTestCase>,
}
impl PythonEnvironmentTestCase {
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 = self.unix_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');
}
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();
assert_eq!(env.sys_prefix().as_std_path(), env_path.as_std_path());
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.as_deref(),
Some(&*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 matches!(
self.virtual_env_implementation(),
PythonImplementation::PyPy
) {
SystemPathBuf::from(&*format!(
"/.venv/lib/pypy3.{}/site-packages",
self.minor_version
))
} 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.path.sys_prefix(),
&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 matches!(
self.virtual_env_implementation(),
PythonImplementation::PyPy
) {
SystemPathBuf::from(&*format!(
"/Python3.{minor_version}/lib/pypy3.{minor_version}/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 matches!(
self.virtual_env_implementation(),
PythonImplementation::PyPy
) {
SystemPathBuf::from(&*format!(
"/Python3.{minor_version}/lib/pypy3.{minor_version}"
))
} 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}"
))
}
}
fn unix_site_packages(&self) -> String {
let minor_version = self.minor_version;
match self.virtual_env_implementation() {
PythonImplementation::PyPy => format!("lib/pypy3.{minor_version}/site-packages"),
PythonImplementation::CPython
| PythonImplementation::GraalPy
| PythonImplementation::Unknown => {
if self.free_threaded {
format!("lib/python3.{minor_version}t/site-packages")
} else {
format!("lib/python3.{minor_version}/site-packages")
}
}
}
}
fn virtual_env_implementation(&self) -> PythonImplementation {
self.virtual_env
.as_ref()
.map(VirtualEnvironmentTestCase::implementation)
.unwrap_or(PythonImplementation::CPython)
}
}
#[test]
fn can_find_site_packages_directory_no_virtual_env() {
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_at_origin_uv_workspace() {
let test = PythonEnvironmentTestCase {
system: TestSystem::default(),
minor_version: 12,
free_threaded: false,
origin: SysPrefixPathOrigin::UvWorkspace,
virtual_env: None,
};
test.run();
}
#[test]
fn can_find_site_packages_directory_no_virtual_env_freethreaded() {
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_no_virtual_env_at_origin_self_environment() {
let test = PythonEnvironmentTestCase {
system: TestSystem::default(),
minor_version: 13,
free_threaded: false,
origin: SysPrefixPathOrigin::SelfEnvironment,
virtual_env: None,
};
test.run();
}
#[test]
fn can_find_site_packages_directory_venv_style_version_field_in_pyvenv_cfg() {
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() {
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() {
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() {
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();
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 {
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();
}
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 unresolved_pyvenv_cfg_home_is_nonfatal_without_system_site_packages() {
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 site_packages = if cfg!(target_os = "windows") {
SystemPathBuf::from(r"\.venv\Lib\site-packages")
} else {
SystemPathBuf::from("/.venv/lib/python3.13/site-packages")
};
memory_fs.create_directory_all(&site_packages).unwrap();
let venv = PythonEnvironment::new("/.venv", SysPrefixPathOrigin::VirtualEnvVar, &system)
.unwrap()
.expect_venv();
assert_eq!(venv.base_executable_home_path, None);
let expected = [site_packages];
assert_eq!(
venv.site_packages_directories(&system).unwrap(),
&expected[..]
);
}
#[test]
fn unresolved_pyvenv_cfg_home_with_system_site_packages_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\ninclude-system-site-packages = true",
)
.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);
}
#[test]
fn site_packages_paths_display() {
let paths = SitePackagesPaths::default();
assert_eq!(paths.to_string(), "[]");
let mut paths = SitePackagesPaths::default();
paths.insert_with_settings_diagnostic_path(
SystemPathBuf::from("/path/to/site/packages"),
None,
);
assert_eq!(paths.to_string(), r#"["/path/to/site/packages"]"#);
}
#[test]
#[cfg(not(target_os = "windows"))]
fn layout_inference_uses_the_primary_site_packages_source() {
let system = TestSystem::default();
let memory_fs = system.memory_file_system();
let self_site_packages = SystemPathBuf::from("/self/.venv/lib/python3.16/site-packages");
let project_site_packages =
SystemPathBuf::from("/project/.venv/lib/python3.12/site-packages");
let self_pyvenv_cfg = SystemPathBuf::from("/self/.venv/pyvenv.cfg");
let project_pyvenv_cfg = SystemPathBuf::from("/project/.venv/pyvenv.cfg");
memory_fs.create_directory_all(&self_site_packages).unwrap();
memory_fs
.create_directory_all(&project_site_packages)
.unwrap();
memory_fs
.write_file_all(&self_pyvenv_cfg, "home = /python/bin")
.unwrap();
memory_fs
.write_file_all(&project_pyvenv_cfg, "home = /python/bin")
.unwrap();
let mut paths = SitePackagesPaths::default();
paths.insert_with_settings_diagnostic_path(
self_site_packages,
Some(self_pyvenv_cfg.clone()),
);
paths.insert_with_settings_diagnostic_path(project_site_packages, Some(project_pyvenv_cfg));
let PythonVersionWithSource { version, source } =
paths.python_version_from_layout().unwrap();
assert_eq!(version, PythonVersion::from((3, 16)));
assert_eq!(
source,
PythonVersionSource::InstallationDirectoryLayout {
site_packages_parent_dir: Box::from("python3.16"),
source: Some(PythonVersionFileSource::new(
Arc::new(self_pyvenv_cfg),
None
)),
}
);
}
#[test]
#[cfg(not(target_os = "windows"))]
fn settings_diagnostic_path_ignores_python_helper_scripts() {
let system = TestSystem::default();
let memory_fs = system.memory_file_system();
let sys_prefix = SystemPathBuf::from("/python");
let bin_dir = sys_prefix.join("bin");
let config_helper = bin_dir.join("python3.16-config");
let interpreter = bin_dir.join("python3.16");
let windowed_interpreter = bin_dir.join("pythonw");
memory_fs.create_directory_all(&bin_dir).unwrap();
memory_fs.write_file_all(&config_helper, "").unwrap();
memory_fs.write_file_all(&interpreter, "").unwrap();
memory_fs.write_file_all(&windowed_interpreter, "").unwrap();
assert_eq!(
settings_diagnostic_path_from_sys_prefix(&sys_prefix, &system),
Some(interpreter)
);
}
#[test]
#[cfg(not(target_os = "windows"))]
fn finds_dist_packages_in_system_environment() {
let system = TestSystem::default();
let memory_fs = system.memory_file_system();
let sys_prefix = SystemPathBuf::from("/usr");
let site_packages = sys_prefix.join("lib/python3.12/site-packages");
let dist_packages = sys_prefix.join("lib/python3.12/dist-packages");
memory_fs.create_directory_all(&site_packages).unwrap();
memory_fs.create_directory_all(&dist_packages).unwrap();
let sys_prefix_path = SysPrefixPath {
inner: sys_prefix,
origin: SysPrefixPathOrigin::PythonCliFlag,
};
let directories = site_packages_directories_from_sys_prefix(
&sys_prefix_path,
PythonInterpreterLayout::unknown(
PythonImplementation::CPython,
Some(PythonVersion {
major: 3,
minor: 12,
}),
),
&system,
)
.unwrap();
let dirs: Vec<_> = directories.into_iter().collect();
assert!(
dirs.iter().any(|p| p.as_str().ends_with("site-packages")),
"Expected to find site-packages, got: {dirs:?}"
);
assert!(
dirs.iter().any(|p| p.as_str().ends_with("dist-packages")),
"Expected to find dist-packages, got: {dirs:?}"
);
}
#[test]
#[cfg(not(target_os = "windows"))]
fn finds_debian_python3_dist_packages() {
let system = TestSystem::default();
let memory_fs = system.memory_file_system();
let sys_prefix = SystemPathBuf::from("/usr");
let site_packages = sys_prefix.join("lib/python3.12/site-packages");
let debian_dist_packages = sys_prefix.join("lib/python3/dist-packages");
memory_fs.create_directory_all(&site_packages).unwrap();
memory_fs
.create_directory_all(&debian_dist_packages)
.unwrap();
let sys_prefix_path = SysPrefixPath {
inner: sys_prefix,
origin: SysPrefixPathOrigin::PythonCliFlag,
};
let directories = site_packages_directories_from_sys_prefix(
&sys_prefix_path,
PythonInterpreterLayout::unknown(
PythonImplementation::CPython,
Some(PythonVersion {
major: 3,
minor: 12,
}),
),
&system,
)
.unwrap();
let dirs: Vec<_> = directories.into_iter().collect();
assert!(
dirs.iter()
.any(|p| p.as_str().contains("python3/dist-packages")),
"Expected to find Debian's python3/dist-packages, got: {dirs:?}"
);
}
#[test]
#[cfg(not(target_os = "windows"))]
fn finds_pypy_site_packages_when_version_known() {
let system = TestSystem::default();
let memory_fs = system.memory_file_system();
let sys_prefix = SystemPathBuf::from("/env");
let pypy_site_packages = sys_prefix.join("lib/pypy3.12/site-packages");
let cpython_site_packages = sys_prefix.join("lib/python3.12/site-packages");
memory_fs.create_directory_all(&pypy_site_packages).unwrap();
memory_fs
.create_directory_all(&cpython_site_packages)
.unwrap();
let sys_prefix_path = SysPrefixPath {
inner: sys_prefix,
origin: SysPrefixPathOrigin::PythonCliFlag,
};
let directories = site_packages_directories_from_sys_prefix(
&sys_prefix_path,
PythonInterpreterLayout::unknown(
PythonImplementation::PyPy,
Some(PythonVersion {
major: 3,
minor: 12,
}),
),
&system,
)
.unwrap();
let dirs: Vec<_> = directories.into_iter().collect();
assert!(
dirs.iter()
.any(|p| p.as_str() == "/env/lib/pypy3.12/site-packages"),
"Expected to find /env/lib/pypy3.12/site-packages, got: {dirs:?}"
);
assert!(
dirs.iter()
.all(|p| p.as_str() != "/env/lib/python3.12/site-packages"),
"Should NOT find CPython site-packages when implementation=PyPy, got: {dirs:?}"
);
}
#[test]
#[cfg(not(target_os = "windows"))]
fn finds_usr_local_dist_packages_in_system_environment() {
let system = TestSystem::default();
let memory_fs = system.memory_file_system();
let sys_prefix = SystemPathBuf::from("/usr");
let site_packages = sys_prefix.join("lib/python3.12/site-packages");
let usr_local_dist_packages =
SystemPathBuf::from("/usr/local/lib/python3.12/dist-packages");
memory_fs.create_directory_all(&site_packages).unwrap();
memory_fs
.create_directory_all(&usr_local_dist_packages)
.unwrap();
let sys_prefix_path = SysPrefixPath {
inner: sys_prefix,
origin: SysPrefixPathOrigin::PythonCliFlag,
};
let directories = site_packages_directories_from_sys_prefix(
&sys_prefix_path,
PythonInterpreterLayout::unknown(
PythonImplementation::CPython,
Some(PythonVersion {
major: 3,
minor: 12,
}),
),
&system,
)
.unwrap();
let dirs: Vec<_> = directories.into_iter().collect();
assert!(
dirs.iter()
.any(|p| p.as_str() == "/usr/local/lib/python3.12/dist-packages"),
"Expected to find /usr/local/lib/python3.12/dist-packages, got: {dirs:?}"
);
}
#[test]
#[cfg(not(target_os = "windows"))]
fn usr_local_dist_packages_precedes_sys_prefix_dist_packages() {
let system = TestSystem::default();
let memory_fs = system.memory_file_system();
let sys_prefix = SystemPathBuf::from("/usr");
let usr_lib_dist = sys_prefix.join("lib/python3.12/dist-packages");
let usr_local_dist = SystemPathBuf::from("/usr/local/lib/python3.12/dist-packages");
memory_fs.create_directory_all(&usr_lib_dist).unwrap();
memory_fs.create_directory_all(&usr_local_dist).unwrap();
let sys_prefix_path = SysPrefixPath {
inner: sys_prefix,
origin: SysPrefixPathOrigin::PythonCliFlag,
};
let directories = site_packages_directories_from_sys_prefix(
&sys_prefix_path,
PythonInterpreterLayout::unknown(
PythonImplementation::CPython,
Some(PythonVersion {
major: 3,
minor: 12,
}),
),
&system,
)
.unwrap();
let dirs: Vec<_> = directories.into_iter().collect();
let local_pos = dirs
.iter()
.position(|p| p.as_str() == "/usr/local/lib/python3.12/dist-packages")
.expect("/usr/local/lib/python3.12/dist-packages should be found");
let sys_pos = dirs
.iter()
.position(|p| p.as_str() == "/usr/lib/python3.12/dist-packages")
.expect("/usr/lib/python3.12/dist-packages should be found");
assert!(
local_pos < sys_pos,
"/usr/local should precede /usr/lib for correct precedence, got: {dirs:?}"
);
}
#[test]
#[cfg(not(target_os = "windows"))]
fn does_not_add_usr_local_when_sys_prefix_is_not_usr() {
let system = TestSystem::default();
let memory_fs = system.memory_file_system();
let sys_prefix = SystemPathBuf::from("/home/user/.pyenv/versions/3.12.0");
let site_packages = sys_prefix.join("lib/python3.12/site-packages");
let usr_local_dist = SystemPathBuf::from("/usr/local/lib/python3.12/dist-packages");
memory_fs.create_directory_all(&site_packages).unwrap();
memory_fs.create_directory_all(&usr_local_dist).unwrap();
let sys_prefix_path = SysPrefixPath {
inner: sys_prefix,
origin: SysPrefixPathOrigin::PythonCliFlag,
};
let directories = site_packages_directories_from_sys_prefix(
&sys_prefix_path,
PythonInterpreterLayout::unknown(
PythonImplementation::CPython,
Some(PythonVersion {
major: 3,
minor: 12,
}),
),
&system,
)
.unwrap();
let dirs: Vec<_> = directories.into_iter().collect();
assert!(
dirs.iter().all(|p| !p.as_str().starts_with("/usr/local")),
"Expected no /usr/local paths when sys.prefix != /usr, got: {dirs:?}"
);
}
#[test]
#[cfg(not(target_os = "windows"))]
fn finds_dist_packages_in_fallback_enumeration() {
let system = TestSystem::default();
let memory_fs = system.memory_file_system();
let sys_prefix = SystemPathBuf::from("/usr");
let site_packages = sys_prefix.join("lib/python3.11/site-packages");
let dist_packages = sys_prefix.join("lib/python3.11/dist-packages");
memory_fs.create_directory_all(&site_packages).unwrap();
memory_fs.create_directory_all(&dist_packages).unwrap();
let sys_prefix_path = SysPrefixPath {
inner: sys_prefix,
origin: SysPrefixPathOrigin::PythonCliFlag,
};
let directories = site_packages_directories_from_sys_prefix(
&sys_prefix_path,
PythonInterpreterLayout::unknown(PythonImplementation::Unknown, None),
&system,
)
.unwrap();
let dirs: Vec<_> = directories.into_iter().collect();
assert!(
dirs.iter().any(|p| p.as_str().ends_with("site-packages")),
"Expected to find site-packages in fallback, got: {dirs:?}"
);
assert!(
dirs.iter()
.any(|p| p.as_str().contains("python3.11/dist-packages")),
"Expected to find version-specific dist-packages in fallback, got: {dirs:?}"
);
}
#[test]
#[cfg(not(target_os = "windows"))]
fn finds_debian_dist_packages_in_fallback_enumeration() {
let system = TestSystem::default();
let memory_fs = system.memory_file_system();
let sys_prefix = SystemPathBuf::from("/usr");
let site_packages = sys_prefix.join("lib/python3.11/site-packages");
let debian_dist_packages = sys_prefix.join("lib/python3/dist-packages");
memory_fs.create_directory_all(&site_packages).unwrap();
memory_fs
.create_directory_all(&debian_dist_packages)
.unwrap();
let sys_prefix_path = SysPrefixPath {
inner: sys_prefix,
origin: SysPrefixPathOrigin::PythonCliFlag,
};
let directories = site_packages_directories_from_sys_prefix(
&sys_prefix_path,
PythonInterpreterLayout::unknown(PythonImplementation::Unknown, None),
&system,
)
.unwrap();
let dirs: Vec<_> = directories.into_iter().collect();
assert!(
dirs.iter()
.any(|p| p.as_str().contains("python3/dist-packages")),
"Expected to find Debian's python3/dist-packages, got: {dirs:?}"
);
}
#[test]
#[cfg(not(target_os = "windows"))]
fn finds_usr_local_dist_packages_in_fallback_enumeration() {
let system = TestSystem::default();
let memory_fs = system.memory_file_system();
let sys_prefix = SystemPathBuf::from("/usr");
let usr_local_dist_packages =
SystemPathBuf::from("/usr/local/lib/python3.12/dist-packages");
memory_fs
.create_directory_all(&usr_local_dist_packages)
.unwrap();
let sys_prefix_path = SysPrefixPath {
inner: sys_prefix,
origin: SysPrefixPathOrigin::PythonCliFlag,
};
let directories = site_packages_directories_from_sys_prefix(
&sys_prefix_path,
PythonInterpreterLayout::unknown(PythonImplementation::Unknown, None),
&system,
)
.unwrap();
let dirs: Vec<_> = directories.into_iter().collect();
assert!(
dirs.iter()
.any(|p| p.as_str() == "/usr/local/lib/python3.12/dist-packages"),
"Expected to find /usr/local/lib/python3.12/dist-packages in fallback, got: {dirs:?}"
);
}
#[test]
#[cfg(not(target_os = "windows"))]
fn fallback_enumeration_for_cpython_excludes_pypy_directories() {
let system = TestSystem::default();
let memory_fs = system.memory_file_system();
let sys_prefix = SystemPathBuf::from("/env");
let cpython_site_packages = sys_prefix.join("lib/python3.11/site-packages");
let pypy_site_packages = sys_prefix.join("lib/pypy3.11/site-packages");
memory_fs
.create_directory_all(&cpython_site_packages)
.unwrap();
memory_fs.create_directory_all(&pypy_site_packages).unwrap();
let sys_prefix_path = SysPrefixPath {
inner: sys_prefix,
origin: SysPrefixPathOrigin::PythonCliFlag,
};
let directories = site_packages_directories_from_sys_prefix(
&sys_prefix_path,
PythonInterpreterLayout::unknown(PythonImplementation::CPython, None),
&system,
)
.unwrap();
let dirs: Vec<_> = directories.into_iter().collect();
assert!(
dirs.iter()
.any(|p| p.as_str() == "/env/lib/python3.11/site-packages"),
"Expected to find /env/lib/python3.11/site-packages, got: {dirs:?}"
);
assert!(
dirs.iter()
.all(|p| p.as_str() != "/env/lib/pypy3.11/site-packages"),
"Should NOT find PyPy site-packages when implementation=CPython, got: {dirs:?}"
);
}
#[test]
#[cfg(not(target_os = "windows"))]
fn fallback_enumeration_for_pypy_excludes_cpython_directories() {
let system = TestSystem::default();
let memory_fs = system.memory_file_system();
let sys_prefix = SystemPathBuf::from("/env");
let pypy_site_packages = sys_prefix.join("lib/pypy3.11/site-packages");
let cpython_site_packages = sys_prefix.join("lib/python3.11/site-packages");
memory_fs.create_directory_all(&pypy_site_packages).unwrap();
memory_fs
.create_directory_all(&cpython_site_packages)
.unwrap();
let sys_prefix_path = SysPrefixPath {
inner: sys_prefix,
origin: SysPrefixPathOrigin::PythonCliFlag,
};
let directories = site_packages_directories_from_sys_prefix(
&sys_prefix_path,
PythonInterpreterLayout::unknown(PythonImplementation::PyPy, None),
&system,
)
.unwrap();
let dirs: Vec<_> = directories.into_iter().collect();
assert!(
dirs.iter()
.any(|p| p.as_str() == "/env/lib/pypy3.11/site-packages"),
"Expected to find /env/lib/pypy3.11/site-packages, got: {dirs:?}"
);
assert!(
dirs.iter()
.all(|p| p.as_str() != "/env/lib/python3.11/site-packages"),
"Should NOT find CPython site-packages when implementation=PyPy, got: {dirs:?}"
);
}
#[test]
#[cfg(not(target_os = "windows"))]
fn does_not_enumerate_usr_local_for_wrong_version_when_version_known() {
let system = TestSystem::default();
let memory_fs = system.memory_file_system();
let sys_prefix = SystemPathBuf::from("/usr");
let correct = SystemPathBuf::from("/usr/local/lib/python3.12/dist-packages");
let wrong = SystemPathBuf::from("/usr/local/lib/python3.11/dist-packages");
memory_fs.create_directory_all(&correct).unwrap();
memory_fs.create_directory_all(&wrong).unwrap();
let sys_prefix_path = SysPrefixPath {
inner: sys_prefix,
origin: SysPrefixPathOrigin::PythonCliFlag,
};
let directories = site_packages_directories_from_sys_prefix(
&sys_prefix_path,
PythonInterpreterLayout::unknown(
PythonImplementation::CPython,
Some(PythonVersion {
major: 3,
minor: 12,
}),
),
&system,
)
.unwrap();
let dirs: Vec<_> = directories.into_iter().collect();
assert!(
dirs.iter()
.any(|p| p.as_str() == "/usr/local/lib/python3.12/dist-packages"),
"Expected to find /usr/local/lib/python3.12/dist-packages, got: {dirs:?}"
);
assert!(
dirs.iter()
.all(|p| p.as_str() != "/usr/local/lib/python3.11/dist-packages"),
"Should NOT find python3.11 packages when version=3.12 is known, got: {dirs:?}"
);
}
#[test]
#[cfg(not(target_os = "windows"))]
fn does_not_include_wrong_version_when_implementation_unknown() {
let system = TestSystem::default();
let memory_fs = system.memory_file_system();
let sys_prefix = SystemPathBuf::from("/usr");
let correct = SystemPathBuf::from("/usr/local/lib/python3.12/dist-packages");
let wrong = SystemPathBuf::from("/usr/local/lib/python3.11/dist-packages");
memory_fs.create_directory_all(&correct).unwrap();
memory_fs.create_directory_all(&wrong).unwrap();
let sys_prefix_path = SysPrefixPath {
inner: sys_prefix,
origin: SysPrefixPathOrigin::PythonCliFlag,
};
let directories = site_packages_directories_from_sys_prefix(
&sys_prefix_path,
PythonInterpreterLayout::unknown(
PythonImplementation::Unknown,
Some(PythonVersion {
major: 3,
minor: 12,
}),
),
&system,
)
.unwrap();
let dirs: Vec<_> = directories.into_iter().collect();
assert!(
dirs.iter()
.any(|p| p.as_str() == "/usr/local/lib/python3.12/dist-packages"),
"Expected to find /usr/local/lib/python3.12/dist-packages, got: {dirs:?}"
);
assert!(
dirs.iter()
.all(|p| p.as_str() != "/usr/local/lib/python3.11/dist-packages"),
"Should NOT find python3.11 packages when version=3.12 is known, got: {dirs:?}"
);
}
#[test]
#[cfg(not(target_os = "windows"))]
fn unknown_known_version_prefers_cpython_layout() {
let system = TestSystem::default();
let memory_fs = system.memory_file_system();
let sys_prefix = SystemPathBuf::from("/env");
let cpython_site_packages = sys_prefix.join("lib/python3.12/site-packages");
let pypy_site_packages = sys_prefix.join("lib/pypy3.12/site-packages");
memory_fs
.create_directory_all(&cpython_site_packages)
.unwrap();
memory_fs.create_directory_all(&pypy_site_packages).unwrap();
let sys_prefix_path = SysPrefixPath {
inner: sys_prefix,
origin: SysPrefixPathOrigin::PythonCliFlag,
};
let directories = site_packages_directories_from_sys_prefix(
&sys_prefix_path,
PythonInterpreterLayout::unknown(
PythonImplementation::Unknown,
Some(PythonVersion {
major: 3,
minor: 12,
}),
),
&system,
)
.unwrap();
let dirs: Vec<_> = directories.into_iter().collect();
assert_eq!(
dirs,
vec![SystemPathBuf::from("/env/lib/python3.12/site-packages")]
);
}
#[test]
#[cfg(not(target_os = "windows"))]
fn includes_matching_version_pypy_without_wrong_version_cpython_when_unknown() {
let system = TestSystem::default();
let memory_fs = system.memory_file_system();
let sys_prefix = SystemPathBuf::from("/env");
let correct = sys_prefix.join("lib/pypy3.12/site-packages");
let wrong = sys_prefix.join("lib/python3.11/site-packages");
memory_fs.create_directory_all(&correct).unwrap();
memory_fs.create_directory_all(&wrong).unwrap();
let sys_prefix_path = SysPrefixPath {
inner: sys_prefix,
origin: SysPrefixPathOrigin::PythonCliFlag,
};
let directories = site_packages_directories_from_sys_prefix(
&sys_prefix_path,
PythonInterpreterLayout::unknown(
PythonImplementation::Unknown,
Some(PythonVersion {
major: 3,
minor: 12,
}),
),
&system,
)
.unwrap();
let dirs: Vec<_> = directories.into_iter().collect();
assert!(
dirs.iter()
.any(|p| p.as_str() == "/env/lib/pypy3.12/site-packages"),
"Expected to find /env/lib/pypy3.12/site-packages, got: {dirs:?}"
);
assert!(
dirs.iter()
.all(|p| p.as_str() != "/env/lib/python3.11/site-packages"),
"Should NOT find python3.11 packages when version=3.12 is known, got: {dirs:?}"
);
}
#[test]
#[cfg(not(target_os = "windows"))]
fn does_not_include_wrong_version_dist_packages_when_exact_dist_missing() {
let system = TestSystem::default();
let memory_fs = system.memory_file_system();
let sys_prefix = SystemPathBuf::from("/env");
let site_packages = sys_prefix.join("lib/python3.12/site-packages");
let wrong_dist_packages = sys_prefix.join("lib/python3.11/dist-packages");
memory_fs.create_directory_all(&site_packages).unwrap();
memory_fs
.create_directory_all(&wrong_dist_packages)
.unwrap();
let sys_prefix_path = SysPrefixPath {
inner: sys_prefix,
origin: SysPrefixPathOrigin::PythonCliFlag,
};
let directories = site_packages_directories_from_sys_prefix(
&sys_prefix_path,
PythonInterpreterLayout::unknown(
PythonImplementation::CPython,
Some(PythonVersion {
major: 3,
minor: 12,
}),
),
&system,
)
.unwrap();
let dirs: Vec<_> = directories.into_iter().collect();
assert!(
dirs.iter()
.any(|p| p.as_str() == "/env/lib/python3.12/site-packages"),
"Expected to find /env/lib/python3.12/site-packages, got: {dirs:?}"
);
assert!(
dirs.iter()
.all(|p| p.as_str() != "/env/lib/python3.11/dist-packages"),
"Should NOT find python3.11 dist-packages when version=3.12 is known, got: {dirs:?}"
);
}
#[cfg(not(windows))]
#[test]
fn uses_selected_system_interpreter_layout() {
let system = TestSystem::default();
let memory_fs = system.memory_file_system();
let sys_prefix = SystemPathBuf::from("/opt");
memory_fs
.create_directory_all(sys_prefix.join("lib/python3.13/site-packages"))
.unwrap();
let layouts = [
(
"python3.14",
"lib/python3.14/site-packages",
"lib/python3.14",
),
(
"python3.14t",
"lib/python3.14t/site-packages",
"lib/python3.14t",
),
("pypy3.14", "lib/pypy3.14/site-packages", "lib/pypy3.14"),
(
"graalpy3.14",
"lib/python3.14/site-packages",
"lib/python3.14",
),
];
for (executable, site_packages, _) in layouts {
memory_fs
.write_file_all(sys_prefix.join("bin").join(executable), "")
.unwrap();
memory_fs
.create_directory_all(sys_prefix.join(site_packages))
.unwrap();
}
for (executable, site_packages, stdlib) in layouts {
let environment = PythonEnvironment::new(
sys_prefix.join("bin").join(executable),
SysPrefixPathOrigin::PythonCliFlag,
&system,
)
.unwrap();
assert_eq!(
environment.site_packages_paths(&system).unwrap().into_vec(),
[sys_prefix.join(site_packages)]
);
assert_eq!(
environment.real_stdlib_path(&system).unwrap(),
sys_prefix.join(stdlib)
);
}
}
#[cfg(not(windows))]
#[test]
fn recognizes_unversioned_graalpy_executable() {
let system = TestSystem::default();
let memory_fs = system.memory_file_system();
let sys_prefix = SystemPathBuf::from("/graal");
memory_fs
.write_file_all(sys_prefix.join("bin/graalpy"), "")
.unwrap();
memory_fs
.create_directory_all(sys_prefix.join("lib/python3.14/site-packages"))
.unwrap();
let environment = PythonEnvironment::new(
sys_prefix.join("bin/graalpy"),
SysPrefixPathOrigin::PythonCliFlag,
&system,
)
.unwrap();
assert_eq!(
environment.site_packages_paths(&system).unwrap().into_vec(),
[sys_prefix.join("lib/python3.14/site-packages")]
);
}
#[cfg(unix)]
#[test]
fn distinguishes_unsuffixed_default_from_free_threaded_executable() {
let temp_dir = tempfile::tempdir().unwrap();
let root = SystemPath::from_std_path(temp_dir.path()).unwrap();
let system = OsSystem::new(root);
let prefix = root.join("prefix");
let bin = prefix.join("bin");
let versioned_executable = bin.join("python3.14");
let site_packages = prefix.join("lib/python3.14/site-packages");
std::fs::create_dir_all(bin.as_std_path()).unwrap();
std::fs::File::create(versioned_executable.as_std_path()).unwrap();
std::fs::File::create(bin.join("python3.14t").as_std_path()).unwrap();
std::fs::create_dir_all(site_packages.as_std_path()).unwrap();
std::fs::create_dir_all(prefix.join("lib/python3.14t/site-packages").as_std_path())
.unwrap();
let environment = PythonEnvironment::new(
&versioned_executable,
SysPrefixPathOrigin::PythonCliFlag,
&system,
)
.unwrap();
assert_eq!(
environment.site_packages_paths(&system).unwrap().into_vec(),
[system.canonicalize_path(&site_packages).unwrap()]
);
}
#[cfg(unix)]
#[test]
fn identifies_free_threaded_hard_links() {
let temp_dir = tempfile::tempdir().unwrap();
let root = SystemPath::from_std_path(temp_dir.path()).unwrap();
let system = OsSystem::new(root);
for executable_name in ["python3.14t", "python3.14td"] {
let prefix = root.join(executable_name);
let bin = prefix.join("bin");
let versioned_executable = bin.join("python3.14");
let free_threaded_executable = bin.join(executable_name);
let free_threaded_site_packages = prefix.join("lib/python3.14t/site-packages");
std::fs::create_dir_all(bin.as_std_path()).unwrap();
std::fs::File::create(free_threaded_executable.as_std_path()).unwrap();
std::fs::hard_link(
free_threaded_executable.as_std_path(),
versioned_executable.as_std_path(),
)
.unwrap();
std::fs::create_dir_all(prefix.join("lib/python3.14/site-packages").as_std_path())
.unwrap();
std::fs::create_dir_all(free_threaded_site_packages.as_std_path()).unwrap();
for executable in [&versioned_executable, &free_threaded_executable] {
let environment =
PythonEnvironment::new(executable, SysPrefixPathOrigin::PythonCliFlag, &system)
.unwrap();
assert_eq!(
environment.site_packages_paths(&system).unwrap().into_vec(),
[system
.canonicalize_path(&free_threaded_site_packages)
.unwrap()]
);
}
}
}
#[cfg(unix)]
#[test]
fn uses_symlink_target_layout() {
let temp_dir = tempfile::tempdir().unwrap();
let root = SystemPath::from_std_path(temp_dir.path()).unwrap();
let system = OsSystem::new(root);
let prefix = root.join("prefix");
let bin = prefix.join("bin");
std::fs::create_dir_all(bin.as_std_path()).unwrap();
std::fs::File::create(bin.join("python3.14").as_std_path()).unwrap();
std::os::unix::fs::symlink("python3.14", bin.join("python").as_std_path()).unwrap();
let path = PythonEnvironmentPath::new(
&bin.join("python"),
SysPrefixPathOrigin::PythonCliFlag,
&system,
)
.unwrap();
assert_eq!(
path.interpreter_layout(),
Some(PythonInterpreterLayout {
version: Some(PythonVersion::PY314),
implementation: PythonImplementation::CPython,
variant: PythonBuildVariant::Default,
})
);
let other_prefix = root.join("other");
let other_bin = other_prefix.join("bin");
std::fs::create_dir_all(other_bin.as_std_path()).unwrap();
std::fs::File::create(other_bin.join("python3.14").as_std_path()).unwrap();
let facade_prefix = root.join("facade");
let facade_bin = facade_prefix.join("bin");
let python313_site_packages = facade_prefix.join("lib/python3.13/site-packages");
let python314_site_packages = facade_prefix.join("lib/python3.14/site-packages");
std::fs::create_dir_all(facade_bin.as_std_path()).unwrap();
std::fs::create_dir_all(python313_site_packages.as_std_path()).unwrap();
std::fs::create_dir_all(python314_site_packages.as_std_path()).unwrap();
for executable_name in ["python", "python3.13"] {
std::os::unix::fs::symlink(
other_bin.join("python3.14").as_std_path(),
facade_bin.join(executable_name).as_std_path(),
)
.unwrap();
let executable = facade_bin.join(executable_name);
assert_eq!(
PythonEnvironmentPath::new(
&executable,
SysPrefixPathOrigin::PythonCliFlag,
&system,
)
.unwrap()
.interpreter_layout(),
Some(PythonInterpreterLayout {
version: Some(PythonVersion::PY314),
implementation: PythonImplementation::CPython,
variant: PythonBuildVariant::Default,
})
);
let environment =
PythonEnvironment::new(&executable, SysPrefixPathOrigin::PythonCliFlag, &system)
.unwrap();
assert_eq!(
environment.site_packages_paths(&system).unwrap().into_vec(),
[system.canonicalize_path(&python314_site_packages).unwrap()]
);
}
}
}