diff --git a/crates/ty_python_semantic/resources/mdtest/import/cyclic.md b/crates/ty_python_semantic/resources/mdtest/import/cyclic.md new file mode 100644 index 0000000000..4af714c7de --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/import/cyclic.md @@ -0,0 +1,108 @@ +## Cyclic imports + +### Regression tests + +#### Issue 261 + +See: + +`main.py`: + +```py +from foo import bar + +reveal_type(bar) # revealed: +``` + +`foo/__init__.py`: + +```py +from foo import bar + +__all__ = ["bar"] +``` + +`foo/bar/__init__.py`: + +```py +# empty +``` + +#### Issue 113 + +See: + +`main.py`: + +```py +from pkg.sub import A + +# TODO: This should be `` +reveal_type(A) # revealed: Never +``` + +`pkg/outer.py`: + +```py +class A: ... +``` + +`pkg/sub/__init__.py`: + +```py +from ..outer import * +from .inner import * +``` + +`pkg/sub/inner.py`: + +```py +from pkg.sub import A +``` + +### Actual cycle + +The following example fails at runtime. Ideally, we would emit a diagnostic here. For now, we only +make sure that this does not lead to a module resolution cycle. + +`main.py`: + +```py +from module import x + +reveal_type(x) # revealed: Unknown +``` + +`module.py`: + +```py +# error: [unresolved-import] +from module import x +``` + +### Normal self-referential import + +Some modules like `sys` in typeshed import themselves. Here, we make sure that this does not lead to +cycles or unresolved imports. + +`module/__init__.py`: + +```py +import module # self-referential import + +from module.sub import x +``` + +`module/sub.py`: + +```py +x: int = 1 +``` + +`main.py`: + +```py +from module import x + +reveal_type(x) # revealed: int +``` diff --git a/crates/ty_python_semantic/src/types/infer.rs b/crates/ty_python_semantic/src/types/infer.rs index 7d85d5dd52..16ebb5b830 100644 --- a/crates/ty_python_semantic/src/types/infer.rs +++ b/crates/ty_python_semantic/src/types/infer.rs @@ -3946,26 +3946,34 @@ impl<'db> TypeInferenceBuilder<'db> { &alias.name.id }; + // Avoid looking up attributes on a module if a module imports from itself + // (e.g. `from parent import submodule` inside the `parent` module). + let import_is_self_referential = module_ty + .into_module_literal() + .is_some_and(|module| self.file() == module.module(self.db()).file()); + // First try loading the requested attribute from the module. - if let Symbol::Type(ty, boundness) = module_ty.member(self.db(), name).symbol { - if &alias.name != "*" && boundness == Boundness::PossiblyUnbound { - // TODO: Consider loading _both_ the attribute and any submodule and unioning them - // together if the attribute exists but is possibly-unbound. - if let Some(builder) = self - .context - .report_lint(&POSSIBLY_UNBOUND_IMPORT, AnyNodeRef::Alias(alias)) - { - builder.into_diagnostic(format_args!( - "Member `{name}` of module `{module_name}` is possibly unbound", - )); + if !import_is_self_referential { + if let Symbol::Type(ty, boundness) = module_ty.member(self.db(), name).symbol { + if &alias.name != "*" && boundness == Boundness::PossiblyUnbound { + // TODO: Consider loading _both_ the attribute and any submodule and unioning them + // together if the attribute exists but is possibly-unbound. + if let Some(builder) = self + .context + .report_lint(&POSSIBLY_UNBOUND_IMPORT, AnyNodeRef::Alias(alias)) + { + builder.into_diagnostic(format_args!( + "Member `{name}` of module `{module_name}` is possibly unbound", + )); + } } + self.add_declaration_with_binding( + alias.into(), + definition, + &DeclaredAndInferredType::AreTheSame(ty), + ); + return; } - self.add_declaration_with_binding( - alias.into(), - definition, - &DeclaredAndInferredType::AreTheSame(ty), - ); - return; } // If the module doesn't bind the symbol, check if it's a submodule. This won't get