mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-04 10:48:32 +00:00
Delete the ruff_python_resolver
crate (#18933)
This commit is contained in:
parent
689797a984
commit
c1fed55d51
47 changed files with 0 additions and 3004 deletions
33
Cargo.lock
generated
33
Cargo.lock
generated
|
@ -930,35 +930,12 @@ version = "1.0.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
|
||||
|
||||
[[package]]
|
||||
name = "env_filter"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0"
|
||||
dependencies = [
|
||||
"log",
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "env_home"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe"
|
||||
|
||||
[[package]]
|
||||
name = "env_logger"
|
||||
version = "0.11.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"env_filter",
|
||||
"jiff",
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
|
@ -3046,16 +3023,6 @@ dependencies = [
|
|||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ruff_python_resolver"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"env_logger",
|
||||
"insta",
|
||||
"log",
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ruff_python_semantic"
|
||||
version = "0.0.0"
|
||||
|
|
|
@ -75,7 +75,6 @@ dashmap = { version = "6.0.1" }
|
|||
dir-test = { version = "0.4.0" }
|
||||
dunce = { version = "1.0.5" }
|
||||
drop_bomb = { version = "0.1.5" }
|
||||
env_logger = { version = "0.11.0" }
|
||||
etcetera = { version = "0.10.0" }
|
||||
fern = { version = "0.7.0" }
|
||||
filetime = { version = "0.2.23" }
|
||||
|
|
|
@ -1,26 +0,0 @@
|
|||
[package]
|
||||
name = "ruff_python_resolver"
|
||||
version = "0.0.0"
|
||||
description = "A Python module resolver for Ruff"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
rust-version = { workspace = true }
|
||||
homepage = { workspace = true }
|
||||
documentation = { workspace = true }
|
||||
repository = { workspace = true }
|
||||
license = { workspace = true }
|
||||
|
||||
[lib]
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
log = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
env_logger = { workspace = true }
|
||||
tempfile = { workspace = true }
|
||||
insta = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
|
@ -1,3 +0,0 @@
|
|||
# airflow
|
||||
|
||||
This is a mock subset of the Airflow repository, used to test module resolution.
|
|
@ -1,14 +0,0 @@
|
|||
# Standard library.
|
||||
import os
|
||||
|
||||
# First-party.
|
||||
from airflow.jobs.scheduler_job_runner import SchedulerJobRunner
|
||||
|
||||
# Stub file.
|
||||
from airflow.compat.functools import cached_property
|
||||
|
||||
# Namespace package.
|
||||
from airflow.providers.google.cloud.hooks.gcs import GCSHook
|
||||
|
||||
# Third-party.
|
||||
from sqlalchemy.orm import Query
|
|
@ -1,16 +0,0 @@
|
|||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
|
@ -1 +0,0 @@
|
|||
"""Empty file included to support filesystem-based resolver tests."""
|
|
@ -1 +0,0 @@
|
|||
"""Empty file included to support filesystem-based resolver tests."""
|
|
@ -1 +0,0 @@
|
|||
"""Empty file included to support filesystem-based resolver tests."""
|
|
@ -1 +0,0 @@
|
|||
"""Empty file included to support filesystem-based resolver tests."""
|
|
@ -1 +0,0 @@
|
|||
# Empty file included to support filesystem-based resolver tests.
|
|
@ -1 +0,0 @@
|
|||
# Empty file included to support filesystem-based resolver tests.
|
|
@ -1 +0,0 @@
|
|||
|
|
@ -1 +0,0 @@
|
|||
"""Empty file included to support filesystem-based resolver tests."""
|
|
@ -1 +0,0 @@
|
|||
"""Empty file included to support filesystem-based resolver tests."""
|
|
@ -1 +0,0 @@
|
|||
"""Empty file included to support filesystem-based resolver tests."""
|
|
@ -1,18 +0,0 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
pub(crate) struct Config {
|
||||
/// Path to use for typeshed definitions.
|
||||
pub(crate) typeshed_path: Option<PathBuf>,
|
||||
|
||||
/// Path to custom typings (stub) modules.
|
||||
pub(crate) stub_path: Option<PathBuf>,
|
||||
|
||||
/// Path to a directory containing one or more virtual environment
|
||||
/// directories. This is used in conjunction with the "venv" name in
|
||||
/// the config file to identify the python environment used for resolving
|
||||
/// third-party modules.
|
||||
pub(crate) venv_path: Option<PathBuf>,
|
||||
|
||||
/// Default venv environment.
|
||||
pub(crate) venv: Option<PathBuf>,
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use crate::python_platform::PythonPlatform;
|
||||
use crate::python_version::PythonVersion;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct ExecutionEnvironment {
|
||||
/// The root directory of the execution environment.
|
||||
pub(crate) root: PathBuf,
|
||||
|
||||
/// The Python version of the execution environment.
|
||||
pub(crate) python_version: PythonVersion,
|
||||
|
||||
/// The Python platform of the execution environment.
|
||||
pub(crate) python_platform: PythonPlatform,
|
||||
|
||||
/// The extra search paths of the execution environment.
|
||||
pub(crate) extra_paths: Vec<PathBuf>,
|
||||
}
|
|
@ -1,43 +0,0 @@
|
|||
//! Expose the host environment to the resolver.
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::python_platform::PythonPlatform;
|
||||
use crate::python_version::PythonVersion;
|
||||
|
||||
/// A trait to expose the host environment to the resolver.
|
||||
pub(crate) trait Host {
|
||||
/// The search paths to use when resolving Python modules.
|
||||
fn python_search_paths(&self) -> Vec<PathBuf>;
|
||||
|
||||
/// The Python version to use when resolving Python modules.
|
||||
fn python_version(&self) -> PythonVersion;
|
||||
|
||||
/// The OS platform to use when resolving Python modules.
|
||||
fn python_platform(&self) -> PythonPlatform;
|
||||
}
|
||||
|
||||
/// A host that exposes a fixed set of search paths.
|
||||
pub(crate) struct StaticHost {
|
||||
search_paths: Vec<PathBuf>,
|
||||
}
|
||||
|
||||
impl StaticHost {
|
||||
pub(crate) fn new(search_paths: Vec<PathBuf>) -> Self {
|
||||
Self { search_paths }
|
||||
}
|
||||
}
|
||||
|
||||
impl Host for StaticHost {
|
||||
fn python_search_paths(&self) -> Vec<PathBuf> {
|
||||
self.search_paths.clone()
|
||||
}
|
||||
|
||||
fn python_version(&self) -> PythonVersion {
|
||||
PythonVersion::Py312
|
||||
}
|
||||
|
||||
fn python_platform(&self) -> PythonPlatform {
|
||||
PythonPlatform::Darwin
|
||||
}
|
||||
}
|
|
@ -1,176 +0,0 @@
|
|||
use std::collections::BTreeMap;
|
||||
use std::ffi::OsStr;
|
||||
use std::io;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use crate::{native_module, py_typed};
|
||||
|
||||
/// A map of the submodules that are present in a namespace package.
|
||||
///
|
||||
/// Namespace packages lack an `__init__.py` file. So when resolving symbols from a namespace
|
||||
/// package, the symbols must be present as submodules. This map contains the submodules that are
|
||||
/// present in the namespace package, keyed by their module name.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||
pub(crate) struct ImplicitImports(BTreeMap<String, ImplicitImport>);
|
||||
|
||||
impl ImplicitImports {
|
||||
/// Find the "implicit" imports within the namespace package at the given path.
|
||||
pub(crate) fn find(dir_path: &Path, exclusions: &[&Path]) -> io::Result<Self> {
|
||||
let mut submodules: BTreeMap<String, ImplicitImport> = BTreeMap::new();
|
||||
|
||||
// Enumerate all files and directories in the path, expanding links.
|
||||
for entry in dir_path.read_dir()?.flatten() {
|
||||
let file_type = entry.file_type()?;
|
||||
|
||||
let path = entry.path();
|
||||
if exclusions.contains(&path.as_path()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// TODO(charlie): Support symlinks.
|
||||
if file_type.is_file() {
|
||||
// Add implicit file-based modules.
|
||||
let Some(extension) = path.extension() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let (file_stem, is_native_lib) = if extension == "py" || extension == "pyi" {
|
||||
// E.g., `foo.py` becomes `foo`.
|
||||
let file_stem = path.file_stem().and_then(OsStr::to_str);
|
||||
let is_native_lib = false;
|
||||
(file_stem, is_native_lib)
|
||||
} else if native_module::is_native_module_file_extension(extension) {
|
||||
// E.g., `foo.abi3.so` becomes `foo`.
|
||||
let file_stem = native_module::native_module_name(&path);
|
||||
let is_native_lib = true;
|
||||
(file_stem, is_native_lib)
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let Some(name) = file_stem else {
|
||||
continue;
|
||||
};
|
||||
|
||||
// Always prefer stub files over non-stub files.
|
||||
if submodules
|
||||
.get(name)
|
||||
.is_none_or(|implicit_import| !implicit_import.is_stub_file)
|
||||
{
|
||||
submodules.insert(
|
||||
name.to_string(),
|
||||
ImplicitImport {
|
||||
is_stub_file: extension == "pyi",
|
||||
is_native_lib,
|
||||
path,
|
||||
py_typed: None,
|
||||
},
|
||||
);
|
||||
}
|
||||
} else if file_type.is_dir() {
|
||||
// Add implicit directory-based modules.
|
||||
let py_file_path = path.join("__init__.py");
|
||||
let pyi_file_path = path.join("__init__.pyi");
|
||||
|
||||
let (path, is_stub_file) = if py_file_path.exists() {
|
||||
(py_file_path, false)
|
||||
} else if pyi_file_path.exists() {
|
||||
(pyi_file_path, true)
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let Some(name) = path.file_name().and_then(OsStr::to_str) else {
|
||||
continue;
|
||||
};
|
||||
submodules.insert(
|
||||
name.to_string(),
|
||||
ImplicitImport {
|
||||
is_stub_file,
|
||||
is_native_lib: false,
|
||||
py_typed: py_typed::get_py_typed_info(&path),
|
||||
path,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Self(submodules))
|
||||
}
|
||||
|
||||
/// Filter [`ImplicitImports`] to only those symbols that were imported.
|
||||
pub(crate) fn filter(&self, imported_symbols: &[String]) -> Option<Self> {
|
||||
if self.is_empty() || imported_symbols.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let filtered: BTreeMap<String, ImplicitImport> = self
|
||||
.iter()
|
||||
.filter(|(name, _)| imported_symbols.contains(name))
|
||||
.map(|(name, implicit_import)| (name.clone(), implicit_import.clone()))
|
||||
.collect();
|
||||
|
||||
if filtered.len() == self.len() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(Self(filtered))
|
||||
}
|
||||
|
||||
/// Returns `true` if the [`ImplicitImports`] resolves all the symbols requested by a
|
||||
/// module descriptor.
|
||||
pub(crate) fn resolves_namespace_package(&self, imported_symbols: &[String]) -> bool {
|
||||
if !imported_symbols.is_empty() {
|
||||
// TODO(charlie): Pyright uses:
|
||||
//
|
||||
// ```typescript
|
||||
// !Array.from(moduleDescriptor.importedSymbols.keys()).some((symbol) => implicitImports.has(symbol))`
|
||||
// ```
|
||||
//
|
||||
// However, that only checks if _any_ of the symbols are in the implicit imports.
|
||||
for symbol in imported_symbols {
|
||||
if !self.has(symbol) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} else if self.is_empty() {
|
||||
return false;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// Returns `true` if the module is present in the namespace package.
|
||||
pub(crate) fn has(&self, name: &str) -> bool {
|
||||
self.0.contains_key(name)
|
||||
}
|
||||
|
||||
/// Returns the number of implicit imports in the namespace package.
|
||||
pub(crate) fn len(&self) -> usize {
|
||||
self.0.len()
|
||||
}
|
||||
|
||||
/// Returns `true` if there are no implicit imports in the namespace package.
|
||||
pub(crate) fn is_empty(&self) -> bool {
|
||||
self.0.is_empty()
|
||||
}
|
||||
|
||||
/// Returns an iterator over the implicit imports in the namespace package.
|
||||
pub(crate) fn iter(&self) -> impl Iterator<Item = (&String, &ImplicitImport)> {
|
||||
self.0.iter()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) struct ImplicitImport {
|
||||
/// Whether the implicit import is a stub file.
|
||||
pub(crate) is_stub_file: bool,
|
||||
|
||||
/// Whether the implicit import is a native module.
|
||||
pub(crate) is_native_lib: bool,
|
||||
|
||||
/// The path to the implicit import.
|
||||
pub(crate) path: PathBuf,
|
||||
|
||||
/// The `py.typed` information for the implicit import, if any.
|
||||
pub(crate) py_typed: Option<py_typed::PyTypedInfo>,
|
||||
}
|
|
@ -1,122 +0,0 @@
|
|||
//! Interface that describes the output of the import resolver.
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::implicit_imports::ImplicitImports;
|
||||
use crate::py_typed::PyTypedInfo;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[expect(clippy::struct_excessive_bools)]
|
||||
pub(crate) struct ImportResult {
|
||||
/// Whether the import name was relative (e.g., ".foo").
|
||||
pub(crate) is_relative: bool,
|
||||
|
||||
/// Whether the import was resolved to a file or module.
|
||||
pub(crate) is_import_found: bool,
|
||||
|
||||
/// The path was partially resolved, but the specific submodule
|
||||
/// defining the import was not found. For example, `foo.bar` was
|
||||
/// not found, but `foo` was found.
|
||||
pub(crate) is_partly_resolved: bool,
|
||||
|
||||
/// The import refers to a namespace package (i.e., a folder without
|
||||
/// an `__init__.py[i]` file at the final level of resolution). By
|
||||
/// convention, we insert empty `PathBuf` segments into the resolved
|
||||
/// paths vector to indicate intermediary namespace packages.
|
||||
pub(crate) is_namespace_package: bool,
|
||||
|
||||
/// The final resolved directory contains an `__init__.py[i]` file.
|
||||
pub(crate) is_init_file_present: bool,
|
||||
|
||||
/// The import resolved to a stub (`.pyi`) file within a stub package.
|
||||
pub(crate) is_stub_package: bool,
|
||||
|
||||
/// The import resolved to a built-in, local, or third-party module.
|
||||
pub(crate) import_type: ImportType,
|
||||
|
||||
/// A vector of resolved absolute paths for each file in the module
|
||||
/// name. Typically includes a sequence of `__init__.py` files, followed
|
||||
/// by the Python file defining the import itself, though the exact
|
||||
/// structure can vary. For example, namespace packages will be represented
|
||||
/// by empty `PathBuf` segments in the vector.
|
||||
///
|
||||
/// For example, resolving `import foo.bar` might yield `./foo/__init__.py` and `./foo/bar.py`,
|
||||
/// or `./foo/__init__.py` and `./foo/bar/__init__.py`.
|
||||
pub(crate) resolved_paths: Vec<PathBuf>,
|
||||
|
||||
/// The search path used to resolve the module.
|
||||
pub(crate) search_path: Option<PathBuf>,
|
||||
|
||||
/// The resolved file is a type hint (i.e., a `.pyi` file), rather
|
||||
/// than a Python (`.py`) file.
|
||||
pub(crate) is_stub_file: bool,
|
||||
|
||||
/// The resolved file is a native library.
|
||||
pub(crate) is_native_lib: bool,
|
||||
|
||||
/// The resolved file is a hint hint (i.e., a `.pyi` file) from
|
||||
/// `typeshed` in the standard library.
|
||||
pub(crate) is_stdlib_typeshed_file: bool,
|
||||
|
||||
/// The resolved file is a hint hint (i.e., a `.pyi` file) from
|
||||
/// `typeshed` in third-party stubs.
|
||||
pub(crate) is_third_party_typeshed_file: bool,
|
||||
|
||||
/// The resolved file is a type hint (i.e., a `.pyi` file) from
|
||||
/// the configured typing directory.
|
||||
pub(crate) is_local_typings_file: bool,
|
||||
|
||||
/// A map from file to resolved path, for all implicitly imported
|
||||
/// modules that are part of a namespace package.
|
||||
pub(crate) implicit_imports: ImplicitImports,
|
||||
|
||||
/// Any implicit imports whose symbols were explicitly imported (i.e., via
|
||||
/// a `from x import y` statement).
|
||||
pub(crate) filtered_implicit_imports: ImplicitImports,
|
||||
|
||||
/// If the import resolved to a type hint (i.e., a `.pyi` file), then
|
||||
/// a non-type-hint resolution will be stored here.
|
||||
#[expect(clippy::struct_field_names)]
|
||||
pub(crate) non_stub_import_result: Option<Box<ImportResult>>,
|
||||
|
||||
/// Information extracted from the `py.typed` in the package used to
|
||||
/// resolve the import, if any.
|
||||
pub(crate) py_typed_info: Option<PyTypedInfo>,
|
||||
|
||||
/// The directory of the package, if any.
|
||||
pub(crate) package_directory: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl ImportResult {
|
||||
/// An import result that indicates that the import was not found.
|
||||
pub(crate) fn not_found() -> Self {
|
||||
Self {
|
||||
is_relative: false,
|
||||
is_import_found: false,
|
||||
is_partly_resolved: false,
|
||||
is_namespace_package: false,
|
||||
is_init_file_present: false,
|
||||
is_stub_package: false,
|
||||
import_type: ImportType::Local,
|
||||
resolved_paths: vec![],
|
||||
search_path: None,
|
||||
is_stub_file: false,
|
||||
is_native_lib: false,
|
||||
is_stdlib_typeshed_file: false,
|
||||
is_third_party_typeshed_file: false,
|
||||
is_local_typings_file: false,
|
||||
implicit_imports: ImplicitImports::default(),
|
||||
filtered_implicit_imports: ImplicitImports::default(),
|
||||
non_stub_import_result: None,
|
||||
py_typed_info: None,
|
||||
package_directory: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum ImportType {
|
||||
BuiltIn,
|
||||
ThirdParty,
|
||||
Local,
|
||||
}
|
|
@ -1,948 +0,0 @@
|
|||
#![allow(dead_code)]
|
||||
|
||||
mod config;
|
||||
mod execution_environment;
|
||||
mod host;
|
||||
mod implicit_imports;
|
||||
mod import_result;
|
||||
mod module_descriptor;
|
||||
mod native_module;
|
||||
mod py_typed;
|
||||
mod python_platform;
|
||||
mod python_version;
|
||||
mod resolver;
|
||||
mod search;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::fs::{File, create_dir_all};
|
||||
use std::io::{self, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use log::debug;
|
||||
use tempfile::TempDir;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::execution_environment::ExecutionEnvironment;
|
||||
use crate::host;
|
||||
use crate::import_result::{ImportResult, ImportType};
|
||||
use crate::module_descriptor::ImportModuleDescriptor;
|
||||
use crate::python_platform::PythonPlatform;
|
||||
use crate::python_version::PythonVersion;
|
||||
use crate::resolver::resolve_import;
|
||||
|
||||
/// Create a file at the given path with the given content.
|
||||
fn create(path: PathBuf, content: &str) -> io::Result<PathBuf> {
|
||||
if let Some(parent) = path.parent() {
|
||||
create_dir_all(parent)?;
|
||||
}
|
||||
let mut f = File::create(&path)?;
|
||||
f.write_all(content.as_bytes())?;
|
||||
f.sync_all()?;
|
||||
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
/// Create an empty file at the given path.
|
||||
fn empty(path: PathBuf) -> io::Result<PathBuf> {
|
||||
create(path, "")
|
||||
}
|
||||
|
||||
/// Create a partial `py.typed` file at the given path.
|
||||
fn partial(path: PathBuf) -> io::Result<PathBuf> {
|
||||
create(path, "partial\n")
|
||||
}
|
||||
|
||||
/// Create a `py.typed` file at the given path.
|
||||
fn typed(path: PathBuf) -> io::Result<PathBuf> {
|
||||
create(path, "# typed")
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct ResolverOptions {
|
||||
extra_paths: Vec<PathBuf>,
|
||||
library: Option<PathBuf>,
|
||||
stub_path: Option<PathBuf>,
|
||||
typeshed_path: Option<PathBuf>,
|
||||
venv_path: Option<PathBuf>,
|
||||
venv: Option<PathBuf>,
|
||||
}
|
||||
|
||||
fn resolve_options(
|
||||
source_file: impl AsRef<Path>,
|
||||
name: &str,
|
||||
root: impl Into<PathBuf>,
|
||||
options: ResolverOptions,
|
||||
) -> ImportResult {
|
||||
let ResolverOptions {
|
||||
extra_paths,
|
||||
library,
|
||||
stub_path,
|
||||
typeshed_path,
|
||||
venv_path,
|
||||
venv,
|
||||
} = options;
|
||||
|
||||
let execution_environment = ExecutionEnvironment {
|
||||
root: root.into(),
|
||||
python_version: PythonVersion::Py37,
|
||||
python_platform: PythonPlatform::Darwin,
|
||||
extra_paths,
|
||||
};
|
||||
|
||||
let module_descriptor = ImportModuleDescriptor {
|
||||
leading_dots: name.chars().take_while(|c| *c == '.').count(),
|
||||
name_parts: name
|
||||
.chars()
|
||||
.skip_while(|c| *c == '.')
|
||||
.collect::<String>()
|
||||
.split('.')
|
||||
.map(std::string::ToString::to_string)
|
||||
.collect(),
|
||||
imported_symbols: Vec::new(),
|
||||
};
|
||||
|
||||
let config = Config {
|
||||
typeshed_path,
|
||||
stub_path,
|
||||
venv_path,
|
||||
venv,
|
||||
};
|
||||
|
||||
let host = host::StaticHost::new(if let Some(library) = library {
|
||||
vec![library]
|
||||
} else {
|
||||
Vec::new()
|
||||
});
|
||||
|
||||
resolve_import(
|
||||
source_file.as_ref(),
|
||||
&execution_environment,
|
||||
&module_descriptor,
|
||||
&config,
|
||||
&host,
|
||||
)
|
||||
}
|
||||
|
||||
fn setup() {
|
||||
env_logger::builder().is_test(true).try_init().ok();
|
||||
}
|
||||
|
||||
macro_rules! assert_debug_snapshot_normalize_paths {
|
||||
($value: ident) => {{
|
||||
// The debug representation for the backslash are two backslashes (escaping)
|
||||
let $value = std::format!("{:#?}", $value).replace("\\\\", "/");
|
||||
insta::assert_snapshot!($value);
|
||||
}};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn partial_stub_file_exists() -> io::Result<()> {
|
||||
setup();
|
||||
|
||||
let temp_dir = TempDir::new()?;
|
||||
let root = temp_dir.path();
|
||||
|
||||
let temp_dir = TempDir::new()?;
|
||||
let library = temp_dir.path().join("lib").join("site-packages");
|
||||
|
||||
partial(library.join("myLib-stubs/py.typed"))?;
|
||||
let partial_stub_pyi = empty(library.join("myLib-stubs").join("partialStub.pyi"))?;
|
||||
let partial_stub_py = empty(library.join("myLib/partialStub.py"))?;
|
||||
|
||||
let result = resolve_options(
|
||||
partial_stub_py,
|
||||
"myLib.partialStub",
|
||||
root,
|
||||
ResolverOptions {
|
||||
library: Some(library),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
assert!(result.is_import_found);
|
||||
assert!(result.is_stub_file);
|
||||
assert_eq!(result.import_type, ImportType::ThirdParty);
|
||||
assert_eq!(
|
||||
result.resolved_paths,
|
||||
// TODO(charlie): Pyright matches on `libraryRoot, 'myLib', 'partialStub.pyi'` here.
|
||||
// But that file doesn't exist. There's some kind of transform.
|
||||
vec![PathBuf::new(), partial_stub_pyi]
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn partial_stub_init_exists() -> io::Result<()> {
|
||||
setup();
|
||||
|
||||
let temp_dir = TempDir::new()?;
|
||||
let root = temp_dir.path();
|
||||
|
||||
let temp_dir = TempDir::new()?;
|
||||
let library = temp_dir.path().join("lib").join("site-packages");
|
||||
|
||||
partial(library.join("myLib-stubs/py.typed"))?;
|
||||
let partial_stub_init_pyi = empty(library.join("myLib-stubs/__init__.pyi"))?;
|
||||
let partial_stub_init_py = empty(library.join("myLib/__init__.py"))?;
|
||||
|
||||
let result = resolve_options(
|
||||
partial_stub_init_py,
|
||||
"myLib",
|
||||
root,
|
||||
ResolverOptions {
|
||||
library: Some(library),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
assert!(result.is_import_found);
|
||||
assert!(result.is_stub_file);
|
||||
assert_eq!(result.import_type, ImportType::ThirdParty);
|
||||
assert_eq!(
|
||||
result.resolved_paths,
|
||||
// TODO(charlie): Pyright matches on `libraryRoot, 'myLib', '__init__.pyi'` here.
|
||||
// But that file doesn't exist. There's some kind of transform.
|
||||
vec![partial_stub_init_pyi]
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn side_by_side_files() -> io::Result<()> {
|
||||
let temp_dir = TempDir::new()?;
|
||||
let root = temp_dir.path();
|
||||
|
||||
let temp_dir = TempDir::new()?;
|
||||
let library = temp_dir.path().join("lib").join("site-packages");
|
||||
|
||||
partial(library.join("myLib-stubs/py.typed"))?;
|
||||
empty(library.join("myLib/partialStub.pyi"))?;
|
||||
empty(library.join("myLib/partialStub.py"))?;
|
||||
empty(library.join("myLib/partialStub2.py"))?;
|
||||
let my_file = empty(root.join("myFile.py"))?;
|
||||
let side_by_side_stub_file = empty(library.join("myLib-stubs/partialStub.pyi"))?;
|
||||
let partial_stub_file = empty(library.join("myLib-stubs/partialStub2.pyi"))?;
|
||||
|
||||
// Stub package wins over original package (per PEP 561 rules).
|
||||
let side_by_side_result = resolve_options(
|
||||
&my_file,
|
||||
"myLib.partialStub",
|
||||
root,
|
||||
ResolverOptions {
|
||||
library: Some(library.clone()),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
assert!(side_by_side_result.is_import_found);
|
||||
assert!(side_by_side_result.is_stub_file);
|
||||
assert_eq!(
|
||||
side_by_side_result.resolved_paths,
|
||||
vec![PathBuf::new(), side_by_side_stub_file]
|
||||
);
|
||||
|
||||
// Side by side stub doesn't completely disable partial stub.
|
||||
let partial_stub_result = resolve_options(
|
||||
&my_file,
|
||||
"myLib.partialStub2",
|
||||
root,
|
||||
ResolverOptions {
|
||||
library: Some(library),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
assert!(partial_stub_result.is_import_found);
|
||||
assert!(partial_stub_result.is_stub_file);
|
||||
assert_eq!(
|
||||
partial_stub_result.resolved_paths,
|
||||
vec![PathBuf::new(), partial_stub_file]
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stub_package() -> io::Result<()> {
|
||||
setup();
|
||||
|
||||
let temp_dir = TempDir::new()?;
|
||||
let root = temp_dir.path();
|
||||
|
||||
let temp_dir = TempDir::new()?;
|
||||
let library = temp_dir.path().join("lib").join("site-packages");
|
||||
|
||||
empty(library.join("myLib-stubs/stub.pyi"))?;
|
||||
empty(library.join("myLib-stubs/__init__.pyi"))?;
|
||||
let partial_stub_py = empty(library.join("myLib/partialStub.py"))?;
|
||||
|
||||
let result = resolve_options(
|
||||
partial_stub_py,
|
||||
"myLib.partialStub",
|
||||
root,
|
||||
ResolverOptions {
|
||||
library: Some(library),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
// If fully typed stub package exists, that wins over the real package.
|
||||
assert!(!result.is_import_found);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stub_namespace_package() -> io::Result<()> {
|
||||
setup();
|
||||
|
||||
let temp_dir = TempDir::new()?;
|
||||
let root = temp_dir.path();
|
||||
|
||||
let temp_dir = TempDir::new()?;
|
||||
let library = temp_dir.path().join("lib").join("site-packages");
|
||||
|
||||
empty(library.join("myLib-stubs/stub.pyi"))?;
|
||||
let partial_stub_py = empty(library.join("myLib/partialStub.py"))?;
|
||||
|
||||
let result = resolve_options(
|
||||
partial_stub_py.clone(),
|
||||
"myLib.partialStub",
|
||||
root,
|
||||
ResolverOptions {
|
||||
library: Some(library),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
// If fully typed stub package exists, that wins over the real package.
|
||||
assert!(result.is_import_found);
|
||||
assert!(!result.is_stub_file);
|
||||
assert_eq!(result.resolved_paths, vec![PathBuf::new(), partial_stub_py]);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stub_in_typing_folder_over_partial_stub_package() -> io::Result<()> {
|
||||
setup();
|
||||
|
||||
let temp_dir = TempDir::new()?;
|
||||
let root = temp_dir.path();
|
||||
let typing_folder = root.join("typing");
|
||||
|
||||
let temp_dir = TempDir::new()?;
|
||||
let library = temp_dir.path().join("lib").join("site-packages");
|
||||
|
||||
partial(library.join("myLib-stubs/py.typed"))?;
|
||||
empty(library.join("myLib-stubs/__init__.pyi"))?;
|
||||
let my_lib_pyi = empty(typing_folder.join("myLib.pyi"))?;
|
||||
let my_lib_init_py = empty(library.join("myLib/__init__.py"))?;
|
||||
|
||||
let result = resolve_options(
|
||||
my_lib_init_py,
|
||||
"myLib",
|
||||
root,
|
||||
ResolverOptions {
|
||||
library: Some(library),
|
||||
stub_path: Some(typing_folder),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
// If the package exists in typing folder, that gets picked up first (so we resolve to
|
||||
// `myLib.pyi`).
|
||||
assert!(result.is_import_found);
|
||||
assert!(result.is_stub_file);
|
||||
assert_eq!(result.resolved_paths, vec![my_lib_pyi]);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn partial_stub_package_in_typing_folder() -> io::Result<()> {
|
||||
setup();
|
||||
|
||||
let temp_dir = TempDir::new()?;
|
||||
let root = temp_dir.path();
|
||||
let typing_folder = root.join("typing");
|
||||
|
||||
let temp_dir = TempDir::new()?;
|
||||
let library = temp_dir.path().join("lib").join("site-packages");
|
||||
|
||||
partial(typing_folder.join("myLib-stubs/py.typed"))?;
|
||||
let my_lib_stubs_init_pyi = empty(typing_folder.join("myLib-stubs/__init__.pyi"))?;
|
||||
let my_lib_init_py = empty(library.join("myLib/__init__.py"))?;
|
||||
|
||||
let result = resolve_options(
|
||||
my_lib_init_py,
|
||||
"myLib",
|
||||
root,
|
||||
ResolverOptions {
|
||||
library: Some(library),
|
||||
stub_path: Some(typing_folder),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
// If the package exists in typing folder, that gets picked up first (so we resolve to
|
||||
// `myLib.pyi`).
|
||||
assert!(result.is_import_found);
|
||||
assert!(result.is_stub_file);
|
||||
assert_eq!(result.resolved_paths, vec![my_lib_stubs_init_pyi]);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn typeshed_folder() -> io::Result<()> {
|
||||
setup();
|
||||
|
||||
let temp_dir = TempDir::new()?;
|
||||
let root = temp_dir.path();
|
||||
let typeshed_folder = root.join("ts");
|
||||
|
||||
let temp_dir = TempDir::new()?;
|
||||
let library = temp_dir.path().join("lib").join("site-packages");
|
||||
|
||||
empty(typeshed_folder.join("stubs/myLibPackage/myLib.pyi"))?;
|
||||
partial(library.join("myLib-stubs/py.typed"))?;
|
||||
let my_lib_stubs_init_pyi = empty(library.join("myLib-stubs/__init__.pyi"))?;
|
||||
let my_lib_init_py = empty(library.join("myLib/__init__.py"))?;
|
||||
|
||||
let result = resolve_options(
|
||||
my_lib_init_py,
|
||||
"myLib",
|
||||
root,
|
||||
ResolverOptions {
|
||||
library: Some(library),
|
||||
typeshed_path: Some(typeshed_folder),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
// Stub packages win over typeshed.
|
||||
assert!(result.is_import_found);
|
||||
assert!(result.is_stub_file);
|
||||
assert_eq!(result.resolved_paths, vec![my_lib_stubs_init_pyi]);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn py_typed_file() -> io::Result<()> {
|
||||
setup();
|
||||
|
||||
let temp_dir = TempDir::new()?;
|
||||
let root = temp_dir.path();
|
||||
|
||||
let temp_dir = TempDir::new()?;
|
||||
let library = temp_dir.path().join("lib").join("site-packages");
|
||||
|
||||
empty(library.join("myLib/__init__.py"))?;
|
||||
partial(library.join("myLib-stubs/py.typed"))?;
|
||||
let partial_stub_init_pyi = empty(library.join("myLib-stubs/__init__.pyi"))?;
|
||||
let package_py_typed = typed(library.join("myLib/py.typed"))?;
|
||||
|
||||
let result = resolve_options(
|
||||
package_py_typed,
|
||||
"myLib",
|
||||
root,
|
||||
ResolverOptions {
|
||||
library: Some(library),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
// Partial stub package always overrides original package.
|
||||
assert!(result.is_import_found);
|
||||
assert!(result.is_stub_file);
|
||||
assert_eq!(result.resolved_paths, vec![partial_stub_init_pyi]);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn py_typed_library() -> io::Result<()> {
|
||||
setup();
|
||||
|
||||
let temp_dir = TempDir::new()?;
|
||||
let root = temp_dir.path();
|
||||
let typeshed_folder = root.join("ts");
|
||||
|
||||
let temp_dir = TempDir::new()?;
|
||||
let library = temp_dir.path().join("lib").join("site-packages");
|
||||
|
||||
typed(library.join("os/py.typed"))?;
|
||||
let init_py = empty(library.join("os/__init__.py"))?;
|
||||
let typeshed_init_pyi = empty(typeshed_folder.join("stubs/os/os/__init__.pyi"))?;
|
||||
|
||||
let result = resolve_options(
|
||||
typeshed_init_pyi,
|
||||
"os",
|
||||
root,
|
||||
ResolverOptions {
|
||||
library: Some(library),
|
||||
typeshed_path: Some(typeshed_folder),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
assert!(result.is_import_found);
|
||||
assert_eq!(result.resolved_paths, vec![init_py]);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_py_typed_library() -> io::Result<()> {
|
||||
setup();
|
||||
|
||||
let temp_dir = TempDir::new()?;
|
||||
let root = temp_dir.path();
|
||||
let typeshed_folder = root.join("ts");
|
||||
|
||||
let temp_dir = TempDir::new()?;
|
||||
let library = temp_dir.path().join("lib").join("site-packages");
|
||||
|
||||
empty(library.join("os/__init__.py"))?;
|
||||
let typeshed_init_pyi = empty(typeshed_folder.join("stubs/os/os/__init__.pyi"))?;
|
||||
|
||||
let result = resolve_options(
|
||||
typeshed_init_pyi.clone(),
|
||||
"os",
|
||||
root,
|
||||
ResolverOptions {
|
||||
library: Some(library),
|
||||
typeshed_path: Some(typeshed_folder),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
assert!(result.is_import_found);
|
||||
assert_eq!(result.import_type, ImportType::ThirdParty);
|
||||
assert_eq!(result.resolved_paths, vec![typeshed_init_pyi]);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_side_by_side_file_root() -> io::Result<()> {
|
||||
setup();
|
||||
|
||||
let temp_dir = TempDir::new()?;
|
||||
let root = temp_dir.path();
|
||||
|
||||
let file1 = empty(root.join("file1.py"))?;
|
||||
let file2 = empty(root.join("file2.py"))?;
|
||||
|
||||
let result = resolve_options(file2, "file1", root, ResolverOptions::default());
|
||||
|
||||
assert!(result.is_import_found);
|
||||
assert_eq!(result.import_type, ImportType::Local);
|
||||
assert_eq!(result.resolved_paths, vec![file1]);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_side_by_side_file_sub_folder() -> io::Result<()> {
|
||||
setup();
|
||||
|
||||
let temp_dir = TempDir::new()?;
|
||||
let root = temp_dir.path();
|
||||
|
||||
let test_init = empty(root.join("test/__init__.py"))?;
|
||||
let test_file1 = empty(root.join("test/file1.py"))?;
|
||||
let test_file2 = empty(root.join("test/file2.py"))?;
|
||||
|
||||
let result = resolve_options(test_file2, "test.file1", root, ResolverOptions::default());
|
||||
|
||||
assert!(result.is_import_found);
|
||||
assert_eq!(result.import_type, ImportType::Local);
|
||||
assert_eq!(result.resolved_paths, vec![test_init, test_file1]);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_side_by_side_file_sub_under_src_folder() -> io::Result<()> {
|
||||
setup();
|
||||
|
||||
let temp_dir = TempDir::new()?;
|
||||
let root = temp_dir.path();
|
||||
|
||||
let nested_init = empty(root.join("src/nested/__init__.py"))?;
|
||||
let nested_file1 = empty(root.join("src/nested/file1.py"))?;
|
||||
let nested_file2 = empty(root.join("src/nested/file2.py"))?;
|
||||
|
||||
let result = resolve_options(
|
||||
nested_file2,
|
||||
"nested.file1",
|
||||
root,
|
||||
ResolverOptions::default(),
|
||||
);
|
||||
|
||||
assert!(result.is_import_found);
|
||||
assert_eq!(result.import_type, ImportType::Local);
|
||||
assert_eq!(result.resolved_paths, vec![nested_init, nested_file1]);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_file_sub_under_containing_folder() -> io::Result<()> {
|
||||
setup();
|
||||
|
||||
let temp_dir = TempDir::new()?;
|
||||
let root = temp_dir.path();
|
||||
|
||||
let nested_file1 = empty(root.join("src/nested/file1.py"))?;
|
||||
let nested_file2 = empty(root.join("src/nested/nested2/file2.py"))?;
|
||||
|
||||
let result = resolve_options(nested_file2, "file1", root, ResolverOptions::default());
|
||||
|
||||
assert!(result.is_import_found);
|
||||
assert_eq!(result.import_type, ImportType::Local);
|
||||
assert_eq!(result.resolved_paths, vec![nested_file1]);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_side_by_side_file_sub_under_lib_folder() -> io::Result<()> {
|
||||
setup();
|
||||
|
||||
let temp_dir = TempDir::new()?;
|
||||
let root = temp_dir.path();
|
||||
|
||||
let temp_dir = TempDir::new()?;
|
||||
let library = temp_dir.path().join("lib").join("site-packages");
|
||||
|
||||
empty(library.join("myLib/file1.py"))?;
|
||||
let file2 = empty(library.join("myLib/file2.py"))?;
|
||||
|
||||
let result = resolve_options(file2, "file1", root, ResolverOptions::default());
|
||||
|
||||
debug!("result: {result:?}");
|
||||
|
||||
assert!(!result.is_import_found);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested_namespace_package_1() -> io::Result<()> {
|
||||
// See: https://github.com/microsoft/pyright/issues/5089.
|
||||
setup();
|
||||
|
||||
let temp_dir = TempDir::new()?;
|
||||
let root = temp_dir.path();
|
||||
|
||||
let file = empty(root.join("package1/a/b/c/d.py"))?;
|
||||
let package1_init = empty(root.join("package1/a/__init__.py"))?;
|
||||
let package2_init = empty(root.join("package2/a/__init__.py"))?;
|
||||
|
||||
let package1 = root.join("package1");
|
||||
let package2 = root.join("package2");
|
||||
|
||||
let result = resolve_options(
|
||||
package2_init,
|
||||
"a.b.c.d",
|
||||
root,
|
||||
ResolverOptions {
|
||||
extra_paths: vec![package1, package2],
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
assert!(result.is_import_found);
|
||||
assert_eq!(result.import_type, ImportType::Local);
|
||||
assert_eq!(
|
||||
result.resolved_paths,
|
||||
vec![package1_init, PathBuf::new(), PathBuf::new(), file]
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested_namespace_package_2() -> io::Result<()> {
|
||||
// See: https://github.com/microsoft/pyright/issues/5089.
|
||||
setup();
|
||||
|
||||
let temp_dir = TempDir::new()?;
|
||||
let root = temp_dir.path();
|
||||
|
||||
let file = empty(root.join("package1/a/b/c/d.py"))?;
|
||||
let package1_init = empty(root.join("package1/a/b/c/__init__.py"))?;
|
||||
let package2_init = empty(root.join("package2/a/b/c/__init__.py"))?;
|
||||
|
||||
let package1 = root.join("package1");
|
||||
let package2 = root.join("package2");
|
||||
|
||||
let result = resolve_options(
|
||||
package2_init,
|
||||
"a.b.c.d",
|
||||
root,
|
||||
ResolverOptions {
|
||||
extra_paths: vec![package1, package2],
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
assert!(result.is_import_found);
|
||||
assert_eq!(result.import_type, ImportType::Local);
|
||||
assert_eq!(
|
||||
result.resolved_paths,
|
||||
vec![PathBuf::new(), PathBuf::new(), package1_init, file]
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested_namespace_package_3() -> io::Result<()> {
|
||||
// See: https://github.com/microsoft/pyright/issues/5089.
|
||||
setup();
|
||||
|
||||
let temp_dir = TempDir::new()?;
|
||||
let root = temp_dir.path();
|
||||
|
||||
empty(root.join("package1/a/b/c/d.py"))?;
|
||||
let package2_init = empty(root.join("package2/a/__init__.py"))?;
|
||||
|
||||
let package1 = root.join("package1");
|
||||
let package2 = root.join("package2");
|
||||
|
||||
let result = resolve_options(
|
||||
package2_init,
|
||||
"a.b.c.d",
|
||||
root,
|
||||
ResolverOptions {
|
||||
extra_paths: vec![package1, package2],
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
assert!(!result.is_import_found);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested_namespace_package_4() -> io::Result<()> {
|
||||
// See: https://github.com/microsoft/pyright/issues/5089.
|
||||
setup();
|
||||
|
||||
let temp_dir = TempDir::new()?;
|
||||
let root = temp_dir.path();
|
||||
|
||||
empty(root.join("package1/a/b/__init__.py"))?;
|
||||
empty(root.join("package1/a/b/c.py"))?;
|
||||
empty(root.join("package2/a/__init__.py"))?;
|
||||
let package2_a_b_init = empty(root.join("package2/a/b/__init__.py"))?;
|
||||
|
||||
let package1 = root.join("package1");
|
||||
let package2 = root.join("package2");
|
||||
|
||||
let result = resolve_options(
|
||||
package2_a_b_init,
|
||||
"a.b.c",
|
||||
root,
|
||||
ResolverOptions {
|
||||
extra_paths: vec![package1, package2],
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
assert!(!result.is_import_found);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// New tests, don't exist upstream.
|
||||
#[test]
|
||||
fn relative_import_side_by_side_file_root() -> io::Result<()> {
|
||||
setup();
|
||||
|
||||
let temp_dir = TempDir::new()?;
|
||||
let root = temp_dir.path();
|
||||
|
||||
let file1 = empty(root.join("file1.py"))?;
|
||||
let file2 = empty(root.join("file2.py"))?;
|
||||
|
||||
let result = resolve_options(file2, ".file1", root, ResolverOptions::default());
|
||||
|
||||
assert!(result.is_import_found);
|
||||
assert_eq!(result.import_type, ImportType::Local);
|
||||
assert_eq!(result.resolved_paths, vec![file1]);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_relative_import_side_by_side_file_root() -> io::Result<()> {
|
||||
setup();
|
||||
|
||||
let temp_dir = TempDir::new()?;
|
||||
let root = temp_dir.path();
|
||||
|
||||
empty(root.join("file1.py"))?;
|
||||
let file2 = empty(root.join("file2.py"))?;
|
||||
|
||||
let result = resolve_options(file2, "..file1", root, ResolverOptions::default());
|
||||
|
||||
assert!(!result.is_import_found);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn airflow_standard_library() {
|
||||
setup();
|
||||
|
||||
let root = PathBuf::from("./resources/test/airflow");
|
||||
let source_file = root.join("airflow/api/common/mark_tasks.py");
|
||||
|
||||
let result = resolve_options(
|
||||
source_file,
|
||||
"os",
|
||||
root.clone(),
|
||||
ResolverOptions {
|
||||
venv_path: Some(root),
|
||||
venv: Some(PathBuf::from("venv")),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
assert_debug_snapshot_normalize_paths!(result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn airflow_first_party() {
|
||||
setup();
|
||||
|
||||
let root = PathBuf::from("./resources/test/airflow");
|
||||
let source_file = root.join("airflow/api/common/mark_tasks.py");
|
||||
|
||||
let result = resolve_options(
|
||||
source_file,
|
||||
"airflow.jobs.scheduler_job_runner",
|
||||
root.clone(),
|
||||
ResolverOptions {
|
||||
venv_path: Some(root),
|
||||
venv: Some(PathBuf::from("venv")),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
assert_debug_snapshot_normalize_paths!(result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn airflow_stub_file() {
|
||||
setup();
|
||||
|
||||
let root = PathBuf::from("./resources/test/airflow");
|
||||
let source_file = root.join("airflow/api/common/mark_tasks.py");
|
||||
|
||||
let result = resolve_options(
|
||||
source_file,
|
||||
"airflow.compat.functools",
|
||||
root.clone(),
|
||||
ResolverOptions {
|
||||
venv_path: Some(root),
|
||||
venv: Some(PathBuf::from("venv")),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
assert_debug_snapshot_normalize_paths!(result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn airflow_namespace_package() {
|
||||
setup();
|
||||
|
||||
let root = PathBuf::from("./resources/test/airflow");
|
||||
let source_file = root.join("airflow/api/common/mark_tasks.py");
|
||||
|
||||
let result = resolve_options(
|
||||
source_file,
|
||||
"airflow.providers.google.cloud.hooks.gcs",
|
||||
root.clone(),
|
||||
ResolverOptions {
|
||||
venv_path: Some(root),
|
||||
venv: Some(PathBuf::from("venv")),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
assert_debug_snapshot_normalize_paths!(result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn airflow_third_party() {
|
||||
setup();
|
||||
|
||||
let root = PathBuf::from("./resources/test/airflow");
|
||||
let source_file = root.join("airflow/api/common/mark_tasks.py");
|
||||
|
||||
let result = resolve_options(
|
||||
source_file,
|
||||
"sqlalchemy.orm",
|
||||
root.clone(),
|
||||
ResolverOptions {
|
||||
venv_path: Some(root),
|
||||
venv: Some(PathBuf::from("venv")),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
assert_debug_snapshot_normalize_paths!(result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn airflow_explicit_native_module() {
|
||||
setup();
|
||||
|
||||
let root = PathBuf::from("./resources/test/airflow");
|
||||
let source_file = root.join("airflow/api/common/mark_tasks.py");
|
||||
|
||||
let result = resolve_options(
|
||||
source_file,
|
||||
"_watchdog_fsevents",
|
||||
root.clone(),
|
||||
ResolverOptions {
|
||||
venv_path: Some(root),
|
||||
venv: Some(PathBuf::from("venv")),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
assert_debug_snapshot_normalize_paths!(result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn airflow_implicit_native_module() {
|
||||
setup();
|
||||
|
||||
let root = PathBuf::from("./resources/test/airflow");
|
||||
let source_file = root.join("airflow/api/common/mark_tasks.py");
|
||||
|
||||
let result = resolve_options(
|
||||
source_file,
|
||||
"orjson",
|
||||
root.clone(),
|
||||
ResolverOptions {
|
||||
venv_path: Some(root),
|
||||
venv: Some(PathBuf::from("venv")),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
assert_debug_snapshot_normalize_paths!(result);
|
||||
}
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) struct ImportModuleDescriptor {
|
||||
pub(crate) leading_dots: usize,
|
||||
pub(crate) name_parts: Vec<String>,
|
||||
pub(crate) imported_symbols: Vec<String>,
|
||||
}
|
||||
|
||||
impl ImportModuleDescriptor {
|
||||
pub(crate) fn name(&self) -> String {
|
||||
format!(
|
||||
"{}{}",
|
||||
".".repeat(self.leading_dots),
|
||||
&self.name_parts.join(".")
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,87 +0,0 @@
|
|||
//! Support for native Python extension modules.
|
||||
|
||||
use std::ffi::OsStr;
|
||||
use std::io;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// Returns `true` if the given file extension is that of a native module.
|
||||
pub(crate) fn is_native_module_file_extension(file_extension: &OsStr) -> bool {
|
||||
file_extension == "so" || file_extension == "pyd" || file_extension == "dylib"
|
||||
}
|
||||
|
||||
/// Given a file name, returns the name of the native module it represents.
|
||||
///
|
||||
/// For example, given `foo.abi3.so`, return `foo`.
|
||||
pub(crate) fn native_module_name(file_name: &Path) -> Option<&str> {
|
||||
file_name
|
||||
.file_stem()
|
||||
.and_then(OsStr::to_str)
|
||||
.map(|file_stem| {
|
||||
file_stem
|
||||
.split_once('.')
|
||||
.map_or(file_stem, |(file_stem, _)| file_stem)
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns `true` if the given file name is that of a native module with the given name.
|
||||
pub(crate) fn is_native_module_file_name(module_name: &str, file_name: &Path) -> bool {
|
||||
// The file name must be that of a native module.
|
||||
if !file_name
|
||||
.extension()
|
||||
.is_some_and(is_native_module_file_extension)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// The file must represent the module name.
|
||||
native_module_name(file_name) == Some(module_name)
|
||||
}
|
||||
|
||||
/// Find the native module within the namespace package at the given path.
|
||||
pub(crate) fn find_native_module(
|
||||
module_name: &str,
|
||||
dir_path: &Path,
|
||||
) -> io::Result<Option<PathBuf>> {
|
||||
Ok(dir_path
|
||||
.read_dir()?
|
||||
.flatten()
|
||||
.filter(|entry| entry.file_type().is_ok_and(|ft| ft.is_file()))
|
||||
.map(|entry| entry.path())
|
||||
.find(|path| is_native_module_file_name(module_name, path)))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[test]
|
||||
fn module_name() {
|
||||
assert_eq!(
|
||||
super::native_module_name(&PathBuf::from("foo.so")),
|
||||
Some("foo")
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
super::native_module_name(&PathBuf::from("foo.abi3.so")),
|
||||
Some("foo")
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
super::native_module_name(&PathBuf::from("foo.cpython-38-x86_64-linux-gnu.so")),
|
||||
Some("foo")
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
super::native_module_name(&PathBuf::from("foo.cp39-win_amd64.pyd")),
|
||||
Some("foo")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn module_file_extension() {
|
||||
assert!(super::is_native_module_file_extension("so".as_ref()));
|
||||
assert!(super::is_native_module_file_extension("pyd".as_ref()));
|
||||
assert!(super::is_native_module_file_extension("dylib".as_ref()));
|
||||
assert!(!super::is_native_module_file_extension("py".as_ref()));
|
||||
}
|
||||
}
|
|
@ -1,40 +0,0 @@
|
|||
//! Support for [PEP 561] (`py.typed` files).
|
||||
//!
|
||||
//! [PEP 561]: https://peps.python.org/pep-0561/
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) struct PyTypedInfo {
|
||||
/// The path to the `py.typed` file.
|
||||
py_typed_path: PathBuf,
|
||||
|
||||
/// Whether the package is partially typed (as opposed to fully typed).
|
||||
is_partially_typed: bool,
|
||||
}
|
||||
|
||||
/// Returns the `py.typed` information for the given directory, if any.
|
||||
pub(crate) fn get_py_typed_info(dir_path: &Path) -> Option<PyTypedInfo> {
|
||||
let py_typed_path = dir_path.join("py.typed");
|
||||
if py_typed_path.is_file() {
|
||||
// Do a quick sanity check on the size before we attempt to read it. This
|
||||
// file should always be really small - typically zero bytes in length.
|
||||
let file_len = py_typed_path.metadata().ok()?.len();
|
||||
if file_len < 64 * 1024 {
|
||||
// PEP 561 doesn't specify the format of "py.typed" in any detail other than
|
||||
// to say that "If a stub package is partial it MUST include partial\n in a top
|
||||
// level py.typed file."
|
||||
let contents = std::fs::read_to_string(&py_typed_path).ok()?;
|
||||
let is_partially_typed =
|
||||
contents.contains("partial\n") || contents.contains("partial\r\n");
|
||||
Some(PyTypedInfo {
|
||||
py_typed_path,
|
||||
is_partially_typed,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
/// Enum to represent a Python platform.
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
pub(crate) enum PythonPlatform {
|
||||
Darwin,
|
||||
Linux,
|
||||
Windows,
|
||||
}
|
||||
|
||||
impl PythonPlatform {
|
||||
/// Returns the platform-specific library names. These are the candidate names for the top-level
|
||||
/// subdirectory within a virtual environment that contains the `site-packages` directory
|
||||
/// (with a `pythonX.Y` directory in-between).
|
||||
pub(crate) fn lib_names(&self) -> &[&'static str] {
|
||||
match self {
|
||||
PythonPlatform::Darwin => &["lib"],
|
||||
PythonPlatform::Linux => &["lib", "lib64"],
|
||||
PythonPlatform::Windows => &["Lib"],
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
/// Enum to represent a Python version.
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub(crate) enum PythonVersion {
|
||||
Py37,
|
||||
Py38,
|
||||
Py39,
|
||||
Py310,
|
||||
Py311,
|
||||
Py312,
|
||||
}
|
||||
|
||||
impl PythonVersion {
|
||||
/// The directory name (e.g., in a virtual environment) for this Python version.
|
||||
pub(crate) fn dir(self) -> &'static str {
|
||||
match self {
|
||||
PythonVersion::Py37 => "python3.7",
|
||||
PythonVersion::Py38 => "python3.8",
|
||||
PythonVersion::Py39 => "python3.9",
|
||||
PythonVersion::Py310 => "python3.10",
|
||||
PythonVersion::Py311 => "python3.11",
|
||||
PythonVersion::Py312 => "python3.12",
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,744 +0,0 @@
|
|||
//! Resolves Python imports to their corresponding files on disk.
|
||||
|
||||
use std::ffi::OsStr;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use log::debug;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::execution_environment::ExecutionEnvironment;
|
||||
use crate::implicit_imports::ImplicitImports;
|
||||
use crate::import_result::{ImportResult, ImportType};
|
||||
use crate::module_descriptor::ImportModuleDescriptor;
|
||||
use crate::{host, native_module, py_typed, search};
|
||||
|
||||
#[expect(clippy::fn_params_excessive_bools)]
|
||||
fn resolve_module_descriptor(
|
||||
root: &Path,
|
||||
module_descriptor: &ImportModuleDescriptor,
|
||||
allow_partial: bool,
|
||||
allow_native_lib: bool,
|
||||
use_stub_package: bool,
|
||||
allow_pyi: bool,
|
||||
look_for_py_typed: bool,
|
||||
) -> ImportResult {
|
||||
if use_stub_package {
|
||||
debug!("Attempting to resolve stub package using root path: {root:?}");
|
||||
} else {
|
||||
debug!("Attempting to resolve using root path: {root:?}");
|
||||
}
|
||||
|
||||
// Starting at the specified path, walk the file system to find the specified module.
|
||||
let mut resolved_paths: Vec<PathBuf> = Vec::new();
|
||||
let mut dir_path = root.to_path_buf();
|
||||
let mut is_namespace_package = false;
|
||||
let mut is_init_file_present = false;
|
||||
let mut is_stub_package = false;
|
||||
let mut is_stub_file = false;
|
||||
let mut is_native_lib = false;
|
||||
let mut implicit_imports = None;
|
||||
let mut package_directory = None;
|
||||
let mut py_typed_info = None;
|
||||
|
||||
// Ex) `from . import foo`
|
||||
if module_descriptor.name_parts.is_empty() {
|
||||
let py_file_path = dir_path.join("__init__.py");
|
||||
let pyi_file_path = dir_path.join("__init__.pyi");
|
||||
|
||||
if allow_pyi && pyi_file_path.is_file() {
|
||||
debug!("Resolved import with file: {pyi_file_path:?}");
|
||||
resolved_paths.push(pyi_file_path.clone());
|
||||
} else if py_file_path.is_file() {
|
||||
debug!("Resolved import with file: {py_file_path:?}");
|
||||
resolved_paths.push(py_file_path.clone());
|
||||
} else {
|
||||
debug!("Partially resolved import with directory: {dir_path:?}");
|
||||
|
||||
// Add an empty path to indicate that the import is partially resolved.
|
||||
resolved_paths.push(PathBuf::new());
|
||||
is_namespace_package = true;
|
||||
}
|
||||
|
||||
implicit_imports = ImplicitImports::find(&dir_path, &[&py_file_path, &pyi_file_path]).ok();
|
||||
} else {
|
||||
for (i, part) in module_descriptor.name_parts.iter().enumerate() {
|
||||
let is_first_part = i == 0;
|
||||
let is_last_part = i == module_descriptor.name_parts.len() - 1;
|
||||
|
||||
// Extend the directory path with the next segment.
|
||||
let module_dir_path = if use_stub_package && is_first_part {
|
||||
is_stub_package = true;
|
||||
dir_path.join(format!("{part}-stubs"))
|
||||
} else {
|
||||
dir_path.join(part)
|
||||
};
|
||||
|
||||
let found_directory = module_dir_path.is_dir();
|
||||
if found_directory {
|
||||
if is_first_part {
|
||||
package_directory = Some(module_dir_path.clone());
|
||||
}
|
||||
|
||||
// Look for an `__init__.py[i]` in the directory.
|
||||
let py_file_path = module_dir_path.join("__init__.py");
|
||||
let pyi_file_path = module_dir_path.join("__init__.pyi");
|
||||
is_init_file_present = false;
|
||||
|
||||
if allow_pyi && pyi_file_path.is_file() {
|
||||
debug!("Resolved import with file: {pyi_file_path:?}");
|
||||
resolved_paths.push(pyi_file_path.clone());
|
||||
if is_last_part {
|
||||
is_stub_file = true;
|
||||
}
|
||||
is_init_file_present = true;
|
||||
} else if py_file_path.is_file() {
|
||||
debug!("Resolved import with file: {py_file_path:?}");
|
||||
resolved_paths.push(py_file_path.clone());
|
||||
is_init_file_present = true;
|
||||
}
|
||||
|
||||
if look_for_py_typed {
|
||||
py_typed_info =
|
||||
py_typed_info.or_else(|| py_typed::get_py_typed_info(&module_dir_path));
|
||||
}
|
||||
|
||||
// We haven't reached the end of the import, and we found a matching directory.
|
||||
// Proceed to the next segment.
|
||||
if !is_last_part {
|
||||
if !is_init_file_present {
|
||||
resolved_paths.push(PathBuf::new());
|
||||
is_namespace_package = true;
|
||||
py_typed_info = None;
|
||||
}
|
||||
|
||||
dir_path = module_dir_path;
|
||||
continue;
|
||||
}
|
||||
|
||||
if is_init_file_present {
|
||||
implicit_imports =
|
||||
ImplicitImports::find(&module_dir_path, &[&py_file_path, &pyi_file_path])
|
||||
.ok();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// We couldn't find a matching directory, or the directory didn't contain an
|
||||
// `__init__.py[i]` file. Look for an `.py[i]` file with the same name as the
|
||||
// segment, in lieu of a directory.
|
||||
let py_file_path = module_dir_path.with_extension("py");
|
||||
let pyi_file_path = module_dir_path.with_extension("pyi");
|
||||
|
||||
if allow_pyi && pyi_file_path.is_file() {
|
||||
debug!("Resolved import with file: {pyi_file_path:?}");
|
||||
resolved_paths.push(pyi_file_path);
|
||||
if is_last_part {
|
||||
is_stub_file = true;
|
||||
}
|
||||
} else if py_file_path.is_file() {
|
||||
debug!("Resolved import with file: {py_file_path:?}");
|
||||
resolved_paths.push(py_file_path);
|
||||
} else {
|
||||
if allow_native_lib && dir_path.is_dir() {
|
||||
// We couldn't find a `.py[i]` file; search for a native library.
|
||||
if let Some(module_name) = module_dir_path.file_name().and_then(OsStr::to_str) {
|
||||
if let Ok(Some(native_lib_path)) =
|
||||
native_module::find_native_module(module_name, &dir_path)
|
||||
{
|
||||
debug!("Resolved import with file: {native_lib_path:?}");
|
||||
is_native_lib = true;
|
||||
resolved_paths.push(native_lib_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !is_native_lib && found_directory {
|
||||
debug!("Partially resolved import with directory: {dir_path:?}");
|
||||
resolved_paths.push(PathBuf::new());
|
||||
if is_last_part {
|
||||
implicit_imports =
|
||||
ImplicitImports::find(&dir_path, &[&py_file_path, &pyi_file_path]).ok();
|
||||
is_namespace_package = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let import_found = if allow_partial {
|
||||
!resolved_paths.is_empty()
|
||||
} else {
|
||||
resolved_paths.len() == module_descriptor.name_parts.len()
|
||||
};
|
||||
|
||||
let is_partly_resolved = if resolved_paths.is_empty() {
|
||||
false
|
||||
} else {
|
||||
resolved_paths.len() < module_descriptor.name_parts.len()
|
||||
};
|
||||
|
||||
ImportResult {
|
||||
is_relative: false,
|
||||
is_import_found: import_found,
|
||||
is_partly_resolved,
|
||||
is_namespace_package,
|
||||
is_init_file_present,
|
||||
is_stub_package,
|
||||
import_type: ImportType::Local,
|
||||
resolved_paths,
|
||||
search_path: Some(root.into()),
|
||||
is_stub_file,
|
||||
is_native_lib,
|
||||
is_stdlib_typeshed_file: false,
|
||||
is_third_party_typeshed_file: false,
|
||||
is_local_typings_file: false,
|
||||
implicit_imports: implicit_imports.unwrap_or_default(),
|
||||
filtered_implicit_imports: ImplicitImports::default(),
|
||||
non_stub_import_result: None,
|
||||
py_typed_info,
|
||||
package_directory,
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve an absolute module import based on the import resolution algorithm
|
||||
/// defined in [PEP 420].
|
||||
///
|
||||
/// [PEP 420]: https://peps.python.org/pep-0420/
|
||||
#[expect(clippy::fn_params_excessive_bools)]
|
||||
fn resolve_absolute_import(
|
||||
root: &Path,
|
||||
module_descriptor: &ImportModuleDescriptor,
|
||||
allow_partial: bool,
|
||||
allow_native_lib: bool,
|
||||
use_stub_package: bool,
|
||||
allow_pyi: bool,
|
||||
look_for_py_typed: bool,
|
||||
) -> ImportResult {
|
||||
if allow_pyi && use_stub_package {
|
||||
// Search for packaged stubs first. PEP 561 indicates that package authors can ship
|
||||
// stubs separately from the package implementation by appending `-stubs` to its
|
||||
// top-level directory name.
|
||||
let import_result = resolve_module_descriptor(
|
||||
root,
|
||||
module_descriptor,
|
||||
allow_partial,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
|
||||
if import_result.package_directory.is_some() {
|
||||
// If this is a namespace package that wasn't resolved, assume that
|
||||
// it's a partial stub package and continue looking for a real package.
|
||||
if !import_result.is_namespace_package || import_result.is_import_found {
|
||||
return import_result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Search for a "real" package.
|
||||
resolve_module_descriptor(
|
||||
root,
|
||||
module_descriptor,
|
||||
allow_partial,
|
||||
allow_native_lib,
|
||||
false,
|
||||
allow_pyi,
|
||||
look_for_py_typed,
|
||||
)
|
||||
}
|
||||
|
||||
/// Resolve an absolute module import based on the import resolution algorithm,
|
||||
/// taking into account the various competing files to which the import could
|
||||
/// resolve.
|
||||
///
|
||||
/// For example, prefers local imports over third-party imports, and stubs over
|
||||
/// non-stubs.
|
||||
fn resolve_best_absolute_import<Host: host::Host>(
|
||||
execution_environment: &ExecutionEnvironment,
|
||||
module_descriptor: &ImportModuleDescriptor,
|
||||
allow_pyi: bool,
|
||||
config: &Config,
|
||||
host: &Host,
|
||||
) -> Option<ImportResult> {
|
||||
let import_name = module_descriptor.name();
|
||||
|
||||
// Search for local stub files (using `stub_path`).
|
||||
if allow_pyi {
|
||||
if let Some(stub_path) = config.stub_path.as_ref() {
|
||||
debug!("Looking in stub path: {}", stub_path.display());
|
||||
|
||||
let mut typings_import = resolve_absolute_import(
|
||||
stub_path,
|
||||
module_descriptor,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
allow_pyi,
|
||||
false,
|
||||
);
|
||||
|
||||
if typings_import.is_import_found {
|
||||
// Treat stub files as "local".
|
||||
typings_import.import_type = ImportType::Local;
|
||||
typings_import.is_local_typings_file = true;
|
||||
|
||||
// If we resolved to a namespace package, ensure that all imported symbols are
|
||||
// present in the namespace package's "implicit" imports.
|
||||
if typings_import.is_namespace_package
|
||||
&& typings_import
|
||||
.resolved_paths
|
||||
.last()
|
||||
.is_some_and(|path| path.as_os_str().is_empty())
|
||||
{
|
||||
if typings_import
|
||||
.implicit_imports
|
||||
.resolves_namespace_package(&module_descriptor.imported_symbols)
|
||||
{
|
||||
return Some(typings_import);
|
||||
}
|
||||
} else {
|
||||
return Some(typings_import);
|
||||
}
|
||||
}
|
||||
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
// Look in the root directory of the execution environment.
|
||||
debug!(
|
||||
"Looking in root directory of execution environment: {}",
|
||||
execution_environment.root.display()
|
||||
);
|
||||
|
||||
let mut local_import = resolve_absolute_import(
|
||||
&execution_environment.root,
|
||||
module_descriptor,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
allow_pyi,
|
||||
false,
|
||||
);
|
||||
local_import.import_type = ImportType::Local;
|
||||
|
||||
let mut best_result_so_far = Some(local_import);
|
||||
|
||||
// Look in any extra paths.
|
||||
for extra_path in &execution_environment.extra_paths {
|
||||
debug!("Looking in extra path: {}", extra_path.display());
|
||||
|
||||
let mut local_import = resolve_absolute_import(
|
||||
extra_path,
|
||||
module_descriptor,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
allow_pyi,
|
||||
false,
|
||||
);
|
||||
local_import.import_type = ImportType::Local;
|
||||
|
||||
best_result_so_far = Some(pick_best_import(
|
||||
best_result_so_far,
|
||||
local_import,
|
||||
module_descriptor,
|
||||
));
|
||||
}
|
||||
|
||||
// Look for third-party imports in Python's `sys` path.
|
||||
for search_path in search::python_search_paths(config, host) {
|
||||
debug!("Looking in Python search path: {}", search_path.display());
|
||||
|
||||
let mut third_party_import = resolve_absolute_import(
|
||||
&search_path,
|
||||
module_descriptor,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
allow_pyi,
|
||||
true,
|
||||
);
|
||||
third_party_import.import_type = ImportType::ThirdParty;
|
||||
|
||||
best_result_so_far = Some(pick_best_import(
|
||||
best_result_so_far,
|
||||
third_party_import,
|
||||
module_descriptor,
|
||||
));
|
||||
}
|
||||
|
||||
// If a library is fully `py.typed`, prefer the current result. There's one exception:
|
||||
// we're executing from `typeshed` itself. In that case, use the `typeshed` lookup below,
|
||||
// rather than favoring `py.typed` libraries.
|
||||
if let Some(typeshed_root) = search::typeshed_root(config, host) {
|
||||
debug!(
|
||||
"Looking in typeshed root directory: {}",
|
||||
typeshed_root.display()
|
||||
);
|
||||
if typeshed_root != execution_environment.root {
|
||||
if best_result_so_far
|
||||
.as_ref()
|
||||
.is_some_and(|result| result.py_typed_info.is_some() && !result.is_partly_resolved)
|
||||
{
|
||||
return best_result_so_far;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if allow_pyi && !module_descriptor.name_parts.is_empty() {
|
||||
// Check for a stdlib typeshed file.
|
||||
debug!("Looking for typeshed stdlib path: {import_name}");
|
||||
if let Some(mut typeshed_stdilib_import) =
|
||||
find_typeshed_path(module_descriptor, true, config, host)
|
||||
{
|
||||
typeshed_stdilib_import.is_stdlib_typeshed_file = true;
|
||||
return Some(typeshed_stdilib_import);
|
||||
}
|
||||
|
||||
// Check for a third-party typeshed file.
|
||||
debug!("Looking for typeshed third-party path: {import_name}");
|
||||
if let Some(mut typeshed_third_party_import) =
|
||||
find_typeshed_path(module_descriptor, false, config, host)
|
||||
{
|
||||
typeshed_third_party_import.is_third_party_typeshed_file = true;
|
||||
|
||||
best_result_so_far = Some(pick_best_import(
|
||||
best_result_so_far,
|
||||
typeshed_third_party_import,
|
||||
module_descriptor,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// We weren't able to find an exact match, so return the best
|
||||
// partial match.
|
||||
best_result_so_far
|
||||
}
|
||||
|
||||
/// Finds the `typeshed` path for the given module descriptor.
|
||||
///
|
||||
/// Supports both standard library and third-party `typeshed` lookups.
|
||||
fn find_typeshed_path<Host: host::Host>(
|
||||
module_descriptor: &ImportModuleDescriptor,
|
||||
is_std_lib: bool,
|
||||
config: &Config,
|
||||
host: &Host,
|
||||
) -> Option<ImportResult> {
|
||||
if is_std_lib {
|
||||
debug!("Looking for typeshed `stdlib` path");
|
||||
} else {
|
||||
debug!("Looking for typeshed `stubs` path");
|
||||
}
|
||||
|
||||
let mut typeshed_paths = vec![];
|
||||
|
||||
if is_std_lib {
|
||||
if let Some(path) = search::stdlib_typeshed_path(config, host) {
|
||||
typeshed_paths.push(path);
|
||||
}
|
||||
} else {
|
||||
if let Some(paths) =
|
||||
search::third_party_typeshed_package_paths(module_descriptor, config, host)
|
||||
{
|
||||
typeshed_paths.extend(paths);
|
||||
}
|
||||
}
|
||||
|
||||
for typeshed_path in typeshed_paths {
|
||||
if typeshed_path.is_dir() {
|
||||
let mut import_info = resolve_absolute_import(
|
||||
&typeshed_path,
|
||||
module_descriptor,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
);
|
||||
if import_info.is_import_found {
|
||||
import_info.import_type = if is_std_lib {
|
||||
ImportType::BuiltIn
|
||||
} else {
|
||||
ImportType::ThirdParty
|
||||
};
|
||||
return Some(import_info);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
debug!("Typeshed path not found");
|
||||
None
|
||||
}
|
||||
|
||||
/// Given a current "best" import and a newly discovered result, returns the
|
||||
/// preferred result.
|
||||
fn pick_best_import(
|
||||
best_import_so_far: Option<ImportResult>,
|
||||
new_import: ImportResult,
|
||||
module_descriptor: &ImportModuleDescriptor,
|
||||
) -> ImportResult {
|
||||
let Some(best_import_so_far) = best_import_so_far else {
|
||||
return new_import;
|
||||
};
|
||||
|
||||
if new_import.is_import_found {
|
||||
// Prefer traditional over namespace packages.
|
||||
let so_far_index = best_import_so_far
|
||||
.resolved_paths
|
||||
.iter()
|
||||
.position(|path| !path.as_os_str().is_empty());
|
||||
let new_index = new_import
|
||||
.resolved_paths
|
||||
.iter()
|
||||
.position(|path| !path.as_os_str().is_empty());
|
||||
if so_far_index != new_index {
|
||||
match (so_far_index, new_index) {
|
||||
(None, Some(_)) => return new_import,
|
||||
(Some(_), None) => return best_import_so_far,
|
||||
(Some(so_far_index), Some(new_index)) => {
|
||||
return if so_far_index < new_index {
|
||||
best_import_so_far
|
||||
} else {
|
||||
new_import
|
||||
};
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
// Prefer "found" over "not found".
|
||||
if !best_import_so_far.is_import_found {
|
||||
return new_import;
|
||||
}
|
||||
|
||||
// If both results are namespace imports, prefer the result that resolves all
|
||||
// imported symbols.
|
||||
if best_import_so_far.is_namespace_package && new_import.is_namespace_package {
|
||||
if !module_descriptor.imported_symbols.is_empty() {
|
||||
if !best_import_so_far
|
||||
.implicit_imports
|
||||
.resolves_namespace_package(&module_descriptor.imported_symbols)
|
||||
{
|
||||
if new_import
|
||||
.implicit_imports
|
||||
.resolves_namespace_package(&module_descriptor.imported_symbols)
|
||||
{
|
||||
return new_import;
|
||||
}
|
||||
|
||||
// Prefer the namespace package that has an `__init__.py[i]` file present in the
|
||||
// final directory over one that does not.
|
||||
if best_import_so_far.is_init_file_present && !new_import.is_init_file_present {
|
||||
return best_import_so_far;
|
||||
}
|
||||
if !best_import_so_far.is_init_file_present && new_import.is_init_file_present {
|
||||
return new_import;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Prefer "py.typed" over "non-py.typed".
|
||||
if best_import_so_far.py_typed_info.is_some() && new_import.py_typed_info.is_none() {
|
||||
return best_import_so_far;
|
||||
}
|
||||
if best_import_so_far.py_typed_info.is_none() && best_import_so_far.py_typed_info.is_some()
|
||||
{
|
||||
return new_import;
|
||||
}
|
||||
|
||||
// Prefer stub files (`.pyi`) over non-stub files (`.py`).
|
||||
if best_import_so_far.is_stub_file && !new_import.is_stub_file {
|
||||
return best_import_so_far;
|
||||
}
|
||||
if !best_import_so_far.is_stub_file && new_import.is_stub_file {
|
||||
return new_import;
|
||||
}
|
||||
|
||||
// If we're still tied, prefer a shorter resolution path.
|
||||
if best_import_so_far.resolved_paths.len() > new_import.resolved_paths.len() {
|
||||
return new_import;
|
||||
}
|
||||
} else if new_import.is_partly_resolved {
|
||||
let so_far_index = best_import_so_far
|
||||
.resolved_paths
|
||||
.iter()
|
||||
.position(|path| !path.as_os_str().is_empty());
|
||||
let new_index = new_import
|
||||
.resolved_paths
|
||||
.iter()
|
||||
.position(|path| !path.as_os_str().is_empty());
|
||||
if so_far_index != new_index {
|
||||
match (so_far_index, new_index) {
|
||||
(None, Some(_)) => return new_import,
|
||||
(Some(_), None) => return best_import_so_far,
|
||||
(Some(so_far_index), Some(new_index)) => {
|
||||
return if so_far_index < new_index {
|
||||
best_import_so_far
|
||||
} else {
|
||||
new_import
|
||||
};
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
best_import_so_far
|
||||
}
|
||||
|
||||
/// Resolve a relative import.
|
||||
fn resolve_relative_import(
|
||||
source_file: &Path,
|
||||
module_descriptor: &ImportModuleDescriptor,
|
||||
) -> Option<ImportResult> {
|
||||
// Determine which search path this file is part of.
|
||||
let mut directory = source_file;
|
||||
for _ in 0..module_descriptor.leading_dots {
|
||||
directory = directory.parent()?;
|
||||
}
|
||||
|
||||
// Now try to match the module parts from the current directory location.
|
||||
let mut abs_import = resolve_absolute_import(
|
||||
directory,
|
||||
module_descriptor,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
);
|
||||
|
||||
if abs_import.is_stub_file {
|
||||
// If we found a stub for a relative import, only search
|
||||
// the same folder for the real module. Otherwise, it will
|
||||
// error out on runtime.
|
||||
abs_import.non_stub_import_result = Some(Box::new(resolve_absolute_import(
|
||||
directory,
|
||||
module_descriptor,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
)));
|
||||
}
|
||||
|
||||
Some(abs_import)
|
||||
}
|
||||
|
||||
/// Resolve an absolute or relative import.
|
||||
fn resolve_import_strict<Host: host::Host>(
|
||||
source_file: &Path,
|
||||
execution_environment: &ExecutionEnvironment,
|
||||
module_descriptor: &ImportModuleDescriptor,
|
||||
config: &Config,
|
||||
host: &Host,
|
||||
) -> ImportResult {
|
||||
let import_name = module_descriptor.name();
|
||||
|
||||
if module_descriptor.leading_dots > 0 {
|
||||
debug!("Resolving relative import for: {import_name}");
|
||||
|
||||
let relative_import = resolve_relative_import(source_file, module_descriptor);
|
||||
|
||||
if let Some(mut relative_import) = relative_import {
|
||||
relative_import.is_relative = true;
|
||||
return relative_import;
|
||||
}
|
||||
} else {
|
||||
debug!("Resolving best absolute import for: {import_name}");
|
||||
|
||||
let best_import = resolve_best_absolute_import(
|
||||
execution_environment,
|
||||
module_descriptor,
|
||||
true,
|
||||
config,
|
||||
host,
|
||||
);
|
||||
|
||||
if let Some(mut best_import) = best_import {
|
||||
if best_import.is_stub_file {
|
||||
debug!("Resolving best non-stub absolute import for: {import_name}");
|
||||
|
||||
best_import.non_stub_import_result = Some(Box::new(
|
||||
resolve_best_absolute_import(
|
||||
execution_environment,
|
||||
module_descriptor,
|
||||
false,
|
||||
config,
|
||||
host,
|
||||
)
|
||||
.unwrap_or_else(ImportResult::not_found),
|
||||
));
|
||||
}
|
||||
return best_import;
|
||||
}
|
||||
}
|
||||
|
||||
ImportResult::not_found()
|
||||
}
|
||||
|
||||
/// Resolves an import, given the current file and the import descriptor.
|
||||
///
|
||||
/// The algorithm is as follows:
|
||||
///
|
||||
/// 1. If the import is relative, convert it to an absolute import.
|
||||
/// 2. Find the "best" match for the import, allowing stub files. Search local imports, any
|
||||
/// configured search paths, the Python path, the typeshed path, etc.
|
||||
/// 3. If a stub file was found, find the "best" match for the import, disallowing stub files.
|
||||
/// 4. If the import wasn't resolved, try to resolve it in the parent directory, then the parent's
|
||||
/// parent, and so on, until the import root is reached.
|
||||
pub(crate) fn resolve_import<Host: host::Host>(
|
||||
source_file: &Path,
|
||||
execution_environment: &ExecutionEnvironment,
|
||||
module_descriptor: &ImportModuleDescriptor,
|
||||
config: &Config,
|
||||
host: &Host,
|
||||
) -> ImportResult {
|
||||
let import_result = resolve_import_strict(
|
||||
source_file,
|
||||
execution_environment,
|
||||
module_descriptor,
|
||||
config,
|
||||
host,
|
||||
);
|
||||
if import_result.is_import_found || module_descriptor.leading_dots > 0 {
|
||||
return import_result;
|
||||
}
|
||||
|
||||
// If we weren't able to resolve an absolute import, try resolving it in the
|
||||
// importing file's directory, then the parent directory, and so on, until the
|
||||
// import root is reached.
|
||||
let root = execution_environment.root.as_path();
|
||||
let mut current = source_file;
|
||||
while let Some(parent) = current.parent() {
|
||||
if !parent.starts_with(root) {
|
||||
break;
|
||||
}
|
||||
|
||||
debug!("Resolving absolute import in parent: {}", parent.display());
|
||||
|
||||
let mut result =
|
||||
resolve_absolute_import(parent, module_descriptor, false, false, false, true, false);
|
||||
|
||||
if result.is_import_found {
|
||||
if let Some(implicit_imports) = result
|
||||
.implicit_imports
|
||||
.filter(&module_descriptor.imported_symbols)
|
||||
{
|
||||
result.implicit_imports = implicit_imports;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
current = parent;
|
||||
}
|
||||
|
||||
ImportResult::not_found()
|
||||
}
|
|
@ -1,278 +0,0 @@
|
|||
//! 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)
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
---
|
||||
source: crates/ruff_python_resolver/src/lib.rs
|
||||
expression: result
|
||||
snapshot_kind: text
|
||||
---
|
||||
ImportResult {
|
||||
is_relative: false,
|
||||
is_import_found: true,
|
||||
is_partly_resolved: false,
|
||||
is_namespace_package: false,
|
||||
is_init_file_present: false,
|
||||
is_stub_package: false,
|
||||
import_type: ThirdParty,
|
||||
resolved_paths: [
|
||||
"./resources/test/airflow/venv/lib/python3.11/site-packages/_watchdog_fsevents.cpython-311-darwin.so",
|
||||
],
|
||||
search_path: Some(
|
||||
"./resources/test/airflow/venv/lib/python3.11/site-packages",
|
||||
),
|
||||
is_stub_file: false,
|
||||
is_native_lib: true,
|
||||
is_stdlib_typeshed_file: false,
|
||||
is_third_party_typeshed_file: false,
|
||||
is_local_typings_file: false,
|
||||
implicit_imports: ImplicitImports(
|
||||
{},
|
||||
),
|
||||
filtered_implicit_imports: ImplicitImports(
|
||||
{},
|
||||
),
|
||||
non_stub_import_result: None,
|
||||
py_typed_info: None,
|
||||
package_directory: None,
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
---
|
||||
source: crates/ruff_python_resolver/src/lib.rs
|
||||
expression: result
|
||||
snapshot_kind: text
|
||||
---
|
||||
ImportResult {
|
||||
is_relative: false,
|
||||
is_import_found: true,
|
||||
is_partly_resolved: false,
|
||||
is_namespace_package: false,
|
||||
is_init_file_present: true,
|
||||
is_stub_package: false,
|
||||
import_type: Local,
|
||||
resolved_paths: [
|
||||
"./resources/test/airflow/airflow/__init__.py",
|
||||
"./resources/test/airflow/airflow/jobs/__init__.py",
|
||||
"./resources/test/airflow/airflow/jobs/scheduler_job_runner.py",
|
||||
],
|
||||
search_path: Some(
|
||||
"./resources/test/airflow",
|
||||
),
|
||||
is_stub_file: false,
|
||||
is_native_lib: false,
|
||||
is_stdlib_typeshed_file: false,
|
||||
is_third_party_typeshed_file: false,
|
||||
is_local_typings_file: false,
|
||||
implicit_imports: ImplicitImports(
|
||||
{},
|
||||
),
|
||||
filtered_implicit_imports: ImplicitImports(
|
||||
{},
|
||||
),
|
||||
non_stub_import_result: None,
|
||||
py_typed_info: None,
|
||||
package_directory: Some(
|
||||
"./resources/test/airflow/airflow",
|
||||
),
|
||||
}
|
|
@ -1,92 +0,0 @@
|
|||
---
|
||||
source: crates/ruff_python_resolver/src/lib.rs
|
||||
expression: result
|
||||
snapshot_kind: text
|
||||
---
|
||||
ImportResult {
|
||||
is_relative: false,
|
||||
is_import_found: true,
|
||||
is_partly_resolved: false,
|
||||
is_namespace_package: false,
|
||||
is_init_file_present: true,
|
||||
is_stub_package: false,
|
||||
import_type: ThirdParty,
|
||||
resolved_paths: [
|
||||
"./resources/test/airflow/venv/lib/python3.11/site-packages/orjson/__init__.pyi",
|
||||
],
|
||||
search_path: Some(
|
||||
"./resources/test/airflow/venv/lib/python3.11/site-packages",
|
||||
),
|
||||
is_stub_file: true,
|
||||
is_native_lib: false,
|
||||
is_stdlib_typeshed_file: false,
|
||||
is_third_party_typeshed_file: false,
|
||||
is_local_typings_file: false,
|
||||
implicit_imports: ImplicitImports(
|
||||
{
|
||||
"orjson": ImplicitImport {
|
||||
is_stub_file: false,
|
||||
is_native_lib: true,
|
||||
path: "./resources/test/airflow/venv/lib/python3.11/site-packages/orjson/orjson.cpython-311-darwin.so",
|
||||
py_typed: None,
|
||||
},
|
||||
},
|
||||
),
|
||||
filtered_implicit_imports: ImplicitImports(
|
||||
{},
|
||||
),
|
||||
non_stub_import_result: Some(
|
||||
ImportResult {
|
||||
is_relative: false,
|
||||
is_import_found: true,
|
||||
is_partly_resolved: false,
|
||||
is_namespace_package: false,
|
||||
is_init_file_present: true,
|
||||
is_stub_package: false,
|
||||
import_type: ThirdParty,
|
||||
resolved_paths: [
|
||||
"./resources/test/airflow/venv/lib/python3.11/site-packages/orjson/__init__.py",
|
||||
],
|
||||
search_path: Some(
|
||||
"./resources/test/airflow/venv/lib/python3.11/site-packages",
|
||||
),
|
||||
is_stub_file: false,
|
||||
is_native_lib: false,
|
||||
is_stdlib_typeshed_file: false,
|
||||
is_third_party_typeshed_file: false,
|
||||
is_local_typings_file: false,
|
||||
implicit_imports: ImplicitImports(
|
||||
{
|
||||
"orjson": ImplicitImport {
|
||||
is_stub_file: false,
|
||||
is_native_lib: true,
|
||||
path: "./resources/test/airflow/venv/lib/python3.11/site-packages/orjson/orjson.cpython-311-darwin.so",
|
||||
py_typed: None,
|
||||
},
|
||||
},
|
||||
),
|
||||
filtered_implicit_imports: ImplicitImports(
|
||||
{},
|
||||
),
|
||||
non_stub_import_result: None,
|
||||
py_typed_info: Some(
|
||||
PyTypedInfo {
|
||||
py_typed_path: "./resources/test/airflow/venv/lib/python3.11/site-packages/orjson/py.typed",
|
||||
is_partially_typed: false,
|
||||
},
|
||||
),
|
||||
package_directory: Some(
|
||||
"./resources/test/airflow/venv/lib/python3.11/site-packages/orjson",
|
||||
),
|
||||
},
|
||||
),
|
||||
py_typed_info: Some(
|
||||
PyTypedInfo {
|
||||
py_typed_path: "./resources/test/airflow/venv/lib/python3.11/site-packages/orjson/py.typed",
|
||||
is_partially_typed: false,
|
||||
},
|
||||
),
|
||||
package_directory: Some(
|
||||
"./resources/test/airflow/venv/lib/python3.11/site-packages/orjson",
|
||||
),
|
||||
}
|
|
@ -1,41 +0,0 @@
|
|||
---
|
||||
source: crates/ruff_python_resolver/src/lib.rs
|
||||
expression: result
|
||||
snapshot_kind: text
|
||||
---
|
||||
ImportResult {
|
||||
is_relative: false,
|
||||
is_import_found: true,
|
||||
is_partly_resolved: false,
|
||||
is_namespace_package: true,
|
||||
is_init_file_present: true,
|
||||
is_stub_package: false,
|
||||
import_type: Local,
|
||||
resolved_paths: [
|
||||
"./resources/test/airflow/airflow/__init__.py",
|
||||
"",
|
||||
"./resources/test/airflow/airflow/providers/google/__init__.py",
|
||||
"./resources/test/airflow/airflow/providers/google/cloud/__init__.py",
|
||||
"./resources/test/airflow/airflow/providers/google/cloud/hooks/__init__.py",
|
||||
"./resources/test/airflow/airflow/providers/google/cloud/hooks/gcs.py",
|
||||
],
|
||||
search_path: Some(
|
||||
"./resources/test/airflow",
|
||||
),
|
||||
is_stub_file: false,
|
||||
is_native_lib: false,
|
||||
is_stdlib_typeshed_file: false,
|
||||
is_third_party_typeshed_file: false,
|
||||
is_local_typings_file: false,
|
||||
implicit_imports: ImplicitImports(
|
||||
{},
|
||||
),
|
||||
filtered_implicit_imports: ImplicitImports(
|
||||
{},
|
||||
),
|
||||
non_stub_import_result: None,
|
||||
py_typed_info: None,
|
||||
package_directory: Some(
|
||||
"./resources/test/airflow/airflow",
|
||||
),
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
---
|
||||
source: crates/ruff_python_resolver/src/lib.rs
|
||||
expression: result
|
||||
snapshot_kind: text
|
||||
---
|
||||
ImportResult {
|
||||
is_relative: false,
|
||||
is_import_found: false,
|
||||
is_partly_resolved: false,
|
||||
is_namespace_package: false,
|
||||
is_init_file_present: false,
|
||||
is_stub_package: false,
|
||||
import_type: Local,
|
||||
resolved_paths: [],
|
||||
search_path: None,
|
||||
is_stub_file: false,
|
||||
is_native_lib: false,
|
||||
is_stdlib_typeshed_file: false,
|
||||
is_third_party_typeshed_file: false,
|
||||
is_local_typings_file: false,
|
||||
implicit_imports: ImplicitImports(
|
||||
{},
|
||||
),
|
||||
filtered_implicit_imports: ImplicitImports(
|
||||
{},
|
||||
),
|
||||
non_stub_import_result: None,
|
||||
py_typed_info: None,
|
||||
package_directory: None,
|
||||
}
|
|
@ -1,72 +0,0 @@
|
|||
---
|
||||
source: crates/ruff_python_resolver/src/lib.rs
|
||||
expression: result
|
||||
snapshot_kind: text
|
||||
---
|
||||
ImportResult {
|
||||
is_relative: false,
|
||||
is_import_found: true,
|
||||
is_partly_resolved: false,
|
||||
is_namespace_package: false,
|
||||
is_init_file_present: true,
|
||||
is_stub_package: false,
|
||||
import_type: Local,
|
||||
resolved_paths: [
|
||||
"./resources/test/airflow/airflow/__init__.py",
|
||||
"./resources/test/airflow/airflow/compat/__init__.py",
|
||||
"./resources/test/airflow/airflow/compat/functools.pyi",
|
||||
],
|
||||
search_path: Some(
|
||||
"./resources/test/airflow",
|
||||
),
|
||||
is_stub_file: true,
|
||||
is_native_lib: false,
|
||||
is_stdlib_typeshed_file: false,
|
||||
is_third_party_typeshed_file: false,
|
||||
is_local_typings_file: false,
|
||||
implicit_imports: ImplicitImports(
|
||||
{},
|
||||
),
|
||||
filtered_implicit_imports: ImplicitImports(
|
||||
{},
|
||||
),
|
||||
non_stub_import_result: Some(
|
||||
ImportResult {
|
||||
is_relative: false,
|
||||
is_import_found: true,
|
||||
is_partly_resolved: false,
|
||||
is_namespace_package: false,
|
||||
is_init_file_present: true,
|
||||
is_stub_package: false,
|
||||
import_type: Local,
|
||||
resolved_paths: [
|
||||
"./resources/test/airflow/airflow/__init__.py",
|
||||
"./resources/test/airflow/airflow/compat/__init__.py",
|
||||
"./resources/test/airflow/airflow/compat/functools.py",
|
||||
],
|
||||
search_path: Some(
|
||||
"./resources/test/airflow",
|
||||
),
|
||||
is_stub_file: false,
|
||||
is_native_lib: false,
|
||||
is_stdlib_typeshed_file: false,
|
||||
is_third_party_typeshed_file: false,
|
||||
is_local_typings_file: false,
|
||||
implicit_imports: ImplicitImports(
|
||||
{},
|
||||
),
|
||||
filtered_implicit_imports: ImplicitImports(
|
||||
{},
|
||||
),
|
||||
non_stub_import_result: None,
|
||||
py_typed_info: None,
|
||||
package_directory: Some(
|
||||
"./resources/test/airflow/airflow",
|
||||
),
|
||||
},
|
||||
),
|
||||
py_typed_info: None,
|
||||
package_directory: Some(
|
||||
"./resources/test/airflow/airflow",
|
||||
),
|
||||
}
|
|
@ -1,56 +0,0 @@
|
|||
---
|
||||
source: crates/ruff_python_resolver/src/lib.rs
|
||||
expression: result
|
||||
snapshot_kind: text
|
||||
---
|
||||
ImportResult {
|
||||
is_relative: false,
|
||||
is_import_found: true,
|
||||
is_partly_resolved: false,
|
||||
is_namespace_package: false,
|
||||
is_init_file_present: true,
|
||||
is_stub_package: false,
|
||||
import_type: ThirdParty,
|
||||
resolved_paths: [
|
||||
"./resources/test/airflow/venv/lib/python3.11/site-packages/sqlalchemy/__init__.py",
|
||||
"./resources/test/airflow/venv/lib/python3.11/site-packages/sqlalchemy/orm/__init__.py",
|
||||
],
|
||||
search_path: Some(
|
||||
"./resources/test/airflow/venv/lib/python3.11/site-packages",
|
||||
),
|
||||
is_stub_file: false,
|
||||
is_native_lib: false,
|
||||
is_stdlib_typeshed_file: false,
|
||||
is_third_party_typeshed_file: false,
|
||||
is_local_typings_file: false,
|
||||
implicit_imports: ImplicitImports(
|
||||
{
|
||||
"base": ImplicitImport {
|
||||
is_stub_file: false,
|
||||
is_native_lib: false,
|
||||
path: "./resources/test/airflow/venv/lib/python3.11/site-packages/sqlalchemy/orm/base.py",
|
||||
py_typed: None,
|
||||
},
|
||||
"dependency": ImplicitImport {
|
||||
is_stub_file: false,
|
||||
is_native_lib: false,
|
||||
path: "./resources/test/airflow/venv/lib/python3.11/site-packages/sqlalchemy/orm/dependency.py",
|
||||
py_typed: None,
|
||||
},
|
||||
"query": ImplicitImport {
|
||||
is_stub_file: false,
|
||||
is_native_lib: false,
|
||||
path: "./resources/test/airflow/venv/lib/python3.11/site-packages/sqlalchemy/orm/query.py",
|
||||
py_typed: None,
|
||||
},
|
||||
},
|
||||
),
|
||||
filtered_implicit_imports: ImplicitImports(
|
||||
{},
|
||||
),
|
||||
non_stub_import_result: None,
|
||||
py_typed_info: None,
|
||||
package_directory: Some(
|
||||
"./resources/test/airflow/venv/lib/python3.11/site-packages/sqlalchemy",
|
||||
),
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue