Move resolver tests out to top-level (#5424)

## Summary

These are really tests for the entire crate.
This commit is contained in:
Charlie Marsh 2023-06-28 13:25:37 -04:00 committed by GitHub
parent 1d2d015bc5
commit 4d90a5a9bc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 896 additions and 898 deletions

View file

@ -12,3 +12,888 @@ mod python_platform;
mod python_version;
mod resolver;
mod search;
#[cfg(test)]
mod tests {
use insta::assert_debug_snapshot;
use std::fs::{create_dir_all, File};
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();
}
#[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!(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!(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!(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!(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!(result);
}
}

View file

@ -723,7 +723,7 @@ fn resolve_import_strict<Host: host::Host>(
/// 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.
fn resolve_import<Host: host::Host>(
pub(crate) fn resolve_import<Host: host::Host>(
source_file: &Path,
execution_environment: &ExecutionEnvironment,
module_descriptor: &ImportModuleDescriptor,
@ -780,888 +780,3 @@ fn resolve_import<Host: host::Host>(
ImportResult::not_found()
}
#[cfg(test)]
mod tests {
use insta::assert_debug_snapshot;
use std::fs::{create_dir_all, File};
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();
}
#[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!(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!(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!(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!(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!(result);
}
}

View file

@ -1,5 +1,5 @@
---
source: crates/ruff_python_resolver/src/resolver.rs
source: crates/ruff_python_resolver/src/lib.rs
expression: result
---
ImportResult {

View file

@ -1,5 +1,5 @@
---
source: crates/ruff_python_resolver/src/resolver.rs
source: crates/ruff_python_resolver/src/lib.rs
expression: result
---
ImportResult {

View file

@ -1,5 +1,5 @@
---
source: crates/ruff_python_resolver/src/resolver.rs
source: crates/ruff_python_resolver/src/lib.rs
expression: result
---
ImportResult {

View file

@ -1,5 +1,5 @@
---
source: crates/ruff_python_resolver/src/resolver.rs
source: crates/ruff_python_resolver/src/lib.rs
expression: result
---
ImportResult {

View file

@ -1,5 +1,5 @@
---
source: crates/ruff_python_resolver/src/resolver.rs
source: crates/ruff_python_resolver/src/lib.rs
expression: result
---
ImportResult {