mirror of
https://github.com/astral-sh/ruff.git
synced 2025-07-28 15:33:50 +00:00

Update to [Rust 1.74](https://blog.rust-lang.org/2023/11/16/Rust-1.74.0.html) and use the new clippy lints table. The update itself introduced a new clippy lint about superfluous hashes in raw strings, which got removed. I moved our lint config from `rustflags` to the newly stabilized [workspace.lints](https://doc.rust-lang.org/stable/cargo/reference/workspaces.html#the-lints-table). One consequence is that we have to `unsafe_code = "warn"` instead of "forbid" because the latter now actually bans unsafe code: ``` error[E0453]: allow(unsafe_code) incompatible with previous forbid --> crates/ruff_source_file/src/newlines.rs:62:17 | 62 | #[allow(unsafe_code)] | ^^^^^^^^^^^ overruled by previous forbid | = note: `forbid` lint level was set on command line ``` --------- Co-authored-by: Charlie Marsh <charlie.r.marsh@gmail.com>
278 lines
9.6 KiB
Rust
278 lines
9.6 KiB
Rust
//! Determine the appropriate search paths for the Python environment.
|
|
|
|
use std::collections::HashMap;
|
|
use std::ffi::OsStr;
|
|
use std::path::{Path, PathBuf};
|
|
use std::{fs, io};
|
|
|
|
use log::debug;
|
|
|
|
use crate::config::Config;
|
|
use crate::host;
|
|
use crate::module_descriptor::ImportModuleDescriptor;
|
|
use crate::python_version::PythonVersion;
|
|
|
|
const SITE_PACKAGES: &str = "site-packages";
|
|
|
|
/// Find the `site-packages` directory for the specified Python version.
|
|
fn find_site_packages_path(
|
|
lib_path: &Path,
|
|
python_version: Option<PythonVersion>,
|
|
) -> Option<PathBuf> {
|
|
if lib_path.is_dir() {
|
|
debug!(
|
|
"Found path `{}`; looking for site-packages",
|
|
lib_path.display()
|
|
);
|
|
} else {
|
|
debug!("Did not find `{}`", lib_path.display());
|
|
}
|
|
|
|
let site_packages_path = lib_path.join(SITE_PACKAGES);
|
|
if site_packages_path.is_dir() {
|
|
debug!("Found path `{}`", site_packages_path.display());
|
|
return Some(site_packages_path);
|
|
}
|
|
|
|
debug!(
|
|
"Did not find `{}`, so looking for Python subdirectory",
|
|
site_packages_path.display()
|
|
);
|
|
|
|
// There's no `site-packages` directory in the library directory; look for a `python3.X`
|
|
// directory instead.
|
|
let candidate_dirs: Vec<PathBuf> = fs::read_dir(lib_path)
|
|
.ok()?
|
|
.filter_map(|entry| {
|
|
let entry = entry.ok()?;
|
|
let metadata = entry.metadata().ok()?;
|
|
|
|
if metadata.file_type().is_dir() {
|
|
let dir_path = entry.path();
|
|
if dir_path
|
|
.file_name()
|
|
.and_then(OsStr::to_str)?
|
|
.starts_with("python3.")
|
|
{
|
|
if dir_path.join(SITE_PACKAGES).is_dir() {
|
|
return Some(dir_path);
|
|
}
|
|
}
|
|
} else if metadata.file_type().is_symlink() {
|
|
let symlink_path = fs::read_link(entry.path()).ok()?;
|
|
if symlink_path
|
|
.file_name()
|
|
.and_then(OsStr::to_str)?
|
|
.starts_with("python3.")
|
|
{
|
|
if symlink_path.join(SITE_PACKAGES).is_dir() {
|
|
return Some(symlink_path);
|
|
}
|
|
}
|
|
}
|
|
|
|
None
|
|
})
|
|
.collect();
|
|
|
|
// If a `python3.X` directory does exist (and `3.X` matches the current Python version),
|
|
// prefer it over any other Python directories.
|
|
if let Some(python_version) = python_version {
|
|
if let Some(preferred_dir) = candidate_dirs.iter().find(|dir| {
|
|
dir.file_name()
|
|
.and_then(OsStr::to_str)
|
|
.is_some_and(|name| name == python_version.dir())
|
|
}) {
|
|
debug!("Found path `{}`", preferred_dir.display());
|
|
return Some(preferred_dir.join(SITE_PACKAGES));
|
|
}
|
|
}
|
|
|
|
// Fallback to the first `python3.X` directory that we found.
|
|
let default_dir = candidate_dirs.first()?;
|
|
debug!("Found path `{}`", default_dir.display());
|
|
Some(default_dir.join(SITE_PACKAGES))
|
|
}
|
|
|
|
fn find_paths_from_pth_files(parent_dir: &Path) -> io::Result<impl Iterator<Item = PathBuf> + '_> {
|
|
Ok(parent_dir
|
|
.read_dir()?
|
|
.flatten()
|
|
.filter(|entry| {
|
|
// Collect all *.pth files.
|
|
let Ok(file_type) = entry.file_type() else {
|
|
return false;
|
|
};
|
|
file_type.is_file() || file_type.is_symlink()
|
|
})
|
|
.map(|entry| entry.path())
|
|
.filter(|path| path.extension() == Some(OsStr::new("pth")))
|
|
.filter(|path| {
|
|
// Skip all files that are much larger than expected.
|
|
let Ok(metadata) = path.metadata() else {
|
|
return false;
|
|
};
|
|
let file_len = metadata.len();
|
|
file_len > 0 && file_len < 64 * 1024
|
|
})
|
|
.filter_map(|path| {
|
|
let data = fs::read_to_string(path).ok()?;
|
|
for line in data.lines() {
|
|
let trimmed_line = line.trim();
|
|
if !trimmed_line.is_empty()
|
|
&& !trimmed_line.starts_with('#')
|
|
&& !trimmed_line.starts_with("import")
|
|
{
|
|
let pth_path = parent_dir.join(trimmed_line);
|
|
if pth_path.is_dir() {
|
|
return Some(pth_path);
|
|
}
|
|
}
|
|
}
|
|
None
|
|
}))
|
|
}
|
|
|
|
/// Find the Python search paths for the given virtual environment.
|
|
fn find_python_search_paths<Host: host::Host>(config: &Config, host: &Host) -> Vec<PathBuf> {
|
|
if let Some(venv_path) = config.venv_path.as_ref() {
|
|
if let Some(venv) = config.venv.as_ref() {
|
|
let mut found_paths = vec![];
|
|
|
|
for lib_name in host.python_platform().lib_names() {
|
|
let lib_path = venv_path.join(venv).join(lib_name);
|
|
if let Some(site_packages_path) = find_site_packages_path(&lib_path, None) {
|
|
// Add paths from any `.pth` files in each of the `site-packages` directories.
|
|
if let Ok(pth_paths) = find_paths_from_pth_files(&site_packages_path) {
|
|
found_paths.extend(pth_paths);
|
|
}
|
|
|
|
// Add the `site-packages` directory to the search path.
|
|
found_paths.push(site_packages_path);
|
|
}
|
|
}
|
|
|
|
if !found_paths.is_empty() {
|
|
found_paths.sort();
|
|
found_paths.dedup();
|
|
|
|
debug!("Found the following `site-packages` dirs");
|
|
for path in &found_paths {
|
|
debug!(" {}", path.display());
|
|
}
|
|
|
|
return found_paths;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fall back to the Python interpreter.
|
|
host.python_search_paths()
|
|
}
|
|
|
|
/// Determine the relevant Python search paths.
|
|
pub(crate) fn python_search_paths<Host: host::Host>(config: &Config, host: &Host) -> Vec<PathBuf> {
|
|
// TODO(charlie): Cache search paths.
|
|
find_python_search_paths(config, host)
|
|
}
|
|
|
|
/// Determine the root of the `typeshed` directory.
|
|
pub(crate) fn typeshed_root<Host: host::Host>(config: &Config, host: &Host) -> Option<PathBuf> {
|
|
if let Some(typeshed_path) = config.typeshed_path.as_ref() {
|
|
// Did the user specify a typeshed path?
|
|
if typeshed_path.is_dir() {
|
|
return Some(typeshed_path.clone());
|
|
}
|
|
} else {
|
|
// If not, we'll look in the Python search paths.
|
|
for python_search_path in python_search_paths(config, host) {
|
|
let possible_typeshed_path = python_search_path.join("typeshed");
|
|
if possible_typeshed_path.is_dir() {
|
|
return Some(possible_typeshed_path);
|
|
}
|
|
}
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
/// Determine the current `typeshed` subdirectory.
|
|
fn typeshed_subdirectory<Host: host::Host>(
|
|
is_stdlib: bool,
|
|
config: &Config,
|
|
host: &Host,
|
|
) -> Option<PathBuf> {
|
|
let typeshed_path =
|
|
typeshed_root(config, host)?.join(if is_stdlib { "stdlib" } else { "stubs" });
|
|
if typeshed_path.is_dir() {
|
|
Some(typeshed_path)
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
/// Generate a map from PyPI-registered package name to a list of paths
|
|
/// containing the package's stubs.
|
|
fn build_typeshed_third_party_package_map(
|
|
third_party_dir: &Path,
|
|
) -> io::Result<HashMap<String, Vec<PathBuf>>> {
|
|
let mut package_map = HashMap::new();
|
|
|
|
// Iterate over every directory.
|
|
for outer_entry in fs::read_dir(third_party_dir)? {
|
|
let outer_entry = outer_entry?;
|
|
if outer_entry.file_type()?.is_dir() {
|
|
// Iterate over any subdirectory children.
|
|
for inner_entry in fs::read_dir(outer_entry.path())? {
|
|
let inner_entry = inner_entry?;
|
|
|
|
if inner_entry.file_type()?.is_dir() {
|
|
package_map
|
|
.entry(inner_entry.file_name().to_string_lossy().to_string())
|
|
.or_insert_with(Vec::new)
|
|
.push(outer_entry.path());
|
|
} else if inner_entry.file_type()?.is_file() {
|
|
if inner_entry
|
|
.path()
|
|
.extension()
|
|
.is_some_and(|extension| extension == "pyi")
|
|
{
|
|
if let Some(stripped_file_name) = inner_entry
|
|
.path()
|
|
.file_stem()
|
|
.and_then(std::ffi::OsStr::to_str)
|
|
.map(std::string::ToString::to_string)
|
|
{
|
|
package_map
|
|
.entry(stripped_file_name)
|
|
.or_insert_with(Vec::new)
|
|
.push(outer_entry.path());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(package_map)
|
|
}
|
|
|
|
/// Determine the current `typeshed` subdirectory for a third-party package.
|
|
pub(crate) fn third_party_typeshed_package_paths<Host: host::Host>(
|
|
module_descriptor: &ImportModuleDescriptor,
|
|
config: &Config,
|
|
host: &Host,
|
|
) -> Option<Vec<PathBuf>> {
|
|
let typeshed_path = typeshed_subdirectory(false, config, host)?;
|
|
let package_paths = build_typeshed_third_party_package_map(&typeshed_path).ok()?;
|
|
let first_name_part = module_descriptor.name_parts.first().map(String::as_str)?;
|
|
package_paths.get(first_name_part).cloned()
|
|
}
|
|
|
|
/// Determine the current `typeshed` subdirectory for the standard library.
|
|
pub(crate) fn stdlib_typeshed_path<Host: host::Host>(
|
|
config: &Config,
|
|
host: &Host,
|
|
) -> Option<PathBuf> {
|
|
typeshed_subdirectory(true, config, host)
|
|
}
|