Skip to content

Treat .pyc imports as typing.Any when no stubs can be found #512

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 70 additions & 5 deletions pyrefly/lib/module/finder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ use crate::state::loader::FindError;
static PY_TYPED_CACHE: LazyLock<Mutex<SmallMap<PathBuf, PyTyped>>> =
LazyLock::new(|| Mutex::new(SmallMap::new()));

#[derive(PartialEq, Eq, PartialOrd, Ord, Default, Clone, Dupe)]
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Default, Clone, Dupe)]
enum PyTyped {
#[default]
Missing,
Expand All @@ -40,6 +40,9 @@ enum FindResult {
/// The path component indicates where to continue search next. It may contain more than one directories as the namespace package
/// may span across multiple search roots.
NamespacePackage(Vec1<PathBuf>),
/// Found a compiled Python file (.pyc). Represents a module compiled to bytecode, lacking source and type info.
/// Treated as `typing.Any` to handle imports without type errors.
CompiledModule(PathBuf),
}

impl FindResult {
Expand Down Expand Up @@ -81,6 +84,7 @@ impl FindResult {
.map(|path| py_typed_cached(path))
.max()
.unwrap_or_default(),
Self::CompiledModule(_) => PyTyped::Partial,
}
}
}
Expand All @@ -103,6 +107,11 @@ fn find_one_part<'a>(name: &Name, roots: impl Iterator<Item = &'a PathBuf>) -> O
return Some(FindResult::SingleFileModule(candidate_path));
}
}
// Check if `name` corresponds to a compiled module.
let candidate_pyc_path = root.join(format!("{name}.pyc"));
if candidate_pyc_path.exists() {
return Some(FindResult::CompiledModule(candidate_pyc_path));
}
// Finally check if `name` corresponds to a namespace package.
if candidate_dir.is_dir() {
namespace_roots.push(candidate_dir);
Expand All @@ -122,7 +131,7 @@ fn continue_find_module(start_result: FindResult, components_rest: &[Name]) -> O
// Nothing has been found in the previous round. No point keep looking.
break;
}
Some(FindResult::SingleFileModule(_)) => {
Some(FindResult::SingleFileModule(_)) | Some(FindResult::CompiledModule(_)) => {
// We've already reached leaf nodes. Cannot keep searching
current_result = None;
break;
Expand All @@ -136,9 +145,9 @@ fn continue_find_module(start_result: FindResult, components_rest: &[Name]) -> O
}
}
current_result.map(|x| match x {
FindResult::SingleFileModule(path) | FindResult::RegularPackage(path, _) => {
ModulePath::filesystem(path)
}
FindResult::SingleFileModule(path)
| FindResult::RegularPackage(path, _)
| FindResult::CompiledModule(path) => ModulePath::filesystem(path),
FindResult::NamespacePackage(roots) => {
// TODO(grievejia): Preserving all info in the list instead of dropping all but the first one.
ModulePath::namespace(roots.first().clone())
Expand Down Expand Up @@ -758,4 +767,60 @@ mod tests {
ModulePath::filesystem(root.join("baz-stubs/qux/__init__.py")),
);
}

#[test]
fn test_find_compiled_module() {
let tempdir = tempfile::tempdir().unwrap();
let root = tempdir.path();
TestPath::setup_test_directory(root, vec![TestPath::file("compiled_module.pyc")]);
assert_eq!(
find_module_in_search_path(
ModuleName::from_str("compiled_module"),
[root.to_path_buf()].iter(),
),
Some(ModulePath::filesystem(root.join("compiled_module.pyc")))
);
}

#[test]
fn test_pyc_file_treated_as_partial() {
let tempdir = tempfile::tempdir().unwrap();
let root = tempdir.path();
TestPath::setup_test_directory(root, vec![TestPath::file("compiled_module.pyc")]);
if let Some(find_result) =
find_one_part(&Name::new("compiled_module"), [root.to_path_buf()].iter())
{
assert_eq!(find_result.py_typed(), PyTyped::Partial);
} else {
panic!("Expected to find a compiled module");
}
}

#[test]
fn test_find_one_part_with_pyc() {
let tempdir = tempfile::tempdir().unwrap();
let root = tempdir.path();
TestPath::setup_test_directory(root, vec![TestPath::file("module.pyc")]);
let result = find_one_part(&Name::new("module"), [root.to_path_buf()].iter());
match result {
Some(FindResult::CompiledModule(path)) => {
assert_eq!(path, root.join("module.pyc"));
}
_ => panic!("Expected to find a CompiledModule"),
}
}

#[test]
fn test_continue_find_module_with_pyc() {
let tempdir = tempfile::tempdir().unwrap();
let root = tempdir.path();
TestPath::setup_test_directory(root, vec![TestPath::file("module.pyc")]);
let start_result =
find_one_part(&Name::new("module"), [root.to_path_buf()].iter()).unwrap();
let result = continue_find_module(start_result, &[]);
assert_eq!(
result,
Some(ModulePath::filesystem(root.join("module.pyc")))
);
}
}
2 changes: 2 additions & 0 deletions website/docs/configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,8 @@ information will still be replaced with `typing.Any`.
- Notes:
- `errors = {import-error = false}` (TOML inline table for `errors`) has similar behavior in Pyrefly, but ignores
*all* import errors instead of import errors from specific modules, and won't replace findable modules with `typing.Any`.
- Automatic Handling of `.pyc` Files: When a `.pyc` file is encountered and no source/stub files are available, Pyrefly automatically treats module as `typing.Any`.
This behavior ensures that compiled Python files without available source code do not cause import errors and are handled permissively.

### `ignore-errors-in-generated-code`

Expand Down