From f47b9f22f5487341e86a716df87363487f448469 Mon Sep 17 00:00:00 2001 From: Aria Desires Date: Tue, 11 Nov 2025 12:46:31 -0500 Subject: [PATCH] Consider `from thispackage import y` to re-export `y` in `__init__.pyi` Fixes https://github.com/astral-sh/ty/issues/1487 --- .../mdtest/import/nonstandard_conventions.md | 10 ++--- .../src/semantic_index/builder.rs | 45 ++++++++++--------- 2 files changed, 28 insertions(+), 27 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/import/nonstandard_conventions.md b/crates/ty_python_semantic/resources/mdtest/import/nonstandard_conventions.md index 44163c17b4..cfcceb1dd5 100644 --- a/crates/ty_python_semantic/resources/mdtest/import/nonstandard_conventions.md +++ b/crates/ty_python_semantic/resources/mdtest/import/nonstandard_conventions.md @@ -29,7 +29,8 @@ defining symbols *at all* and re-exporting them. ## Relative `from` Import of Direct Submodule in `__init__` -We consider the `from . import submodule` idiom in an `__init__.pyi` an explicit re-export. +We consider the `from . import submodule` idiom in an `__init__.pyi` an explicit re-export. This +pattern is observed in the wild with various stub packages. ### In Stub @@ -94,8 +95,7 @@ reveal_type(mypackage.fails.Y) # revealed: Unknown ## Absolute `from` Import of Direct Submodule in `__init__` If an absolute `from...import` happens to import a submodule (i.e. it's equivalent to -`from . import y`) we do not treat it as a re-export. We could, but we don't. (This is an arbitrary -decision and can be changed!) +`from . import y`) we also treat it as a re-export. ### In Stub @@ -122,9 +122,7 @@ Y: int = 47 ```py import mypackage -# TODO: this could work and would be nice to have? -# error: "has no member `imported`" -reveal_type(mypackage.imported.X) # revealed: Unknown +reveal_type(mypackage.imported.X) # revealed: int # error: "has no member `fails`" reveal_type(mypackage.fails.Y) # revealed: Unknown ``` diff --git a/crates/ty_python_semantic/src/semantic_index/builder.rs b/crates/ty_python_semantic/src/semantic_index/builder.rs index d7784c2cf1..dc3acb1434 100644 --- a/crates/ty_python_semantic/src/semantic_index/builder.rs +++ b/crates/ty_python_semantic/src/semantic_index/builder.rs @@ -1465,9 +1465,8 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> { // reasons but it works well for most practical purposes. In particular it's nice // that `x` can be freely overwritten, and that we don't assume that an import // in one function is visible in another function. - if node.module.is_some() - && self.current_scope().is_global() - && self.file.is_package(self.db) + let mut is_self_import = false; + if self.file.is_package(self.db) && let Ok(module_name) = ModuleName::from_identifier_parts( self.db, self.file, @@ -1475,19 +1474,26 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> { node.level, ) && let Ok(thispackage) = ModuleName::package_for_file(self.db, self.file) - && let Some(relative_submodule) = module_name.relative_to(&thispackage) - && let Some(direct_submodule) = relative_submodule.components().next() - && !self.seen_submodule_imports.contains(direct_submodule) { - self.seen_submodule_imports - .insert(direct_submodule.to_owned()); + // Record whether this is equivalent to `from . import ...` + is_self_import = module_name == thispackage; - let direct_submodule_name = Name::new(direct_submodule); - let symbol = self.add_symbol(direct_submodule_name); - self.add_definition( - symbol.into(), - ImportFromSubmoduleDefinitionNodeRef { node }, - ); + if node.module.is_some() + && let Some(relative_submodule) = module_name.relative_to(&thispackage) + && let Some(direct_submodule) = relative_submodule.components().next() + && !self.seen_submodule_imports.contains(direct_submodule) + && self.current_scope().is_global() + { + self.seen_submodule_imports + .insert(direct_submodule.to_owned()); + + let direct_submodule_name = Name::new(direct_submodule); + let symbol = self.add_symbol(direct_submodule_name); + self.add_definition( + symbol.into(), + ImportFromSubmoduleDefinitionNodeRef { node }, + ); + } } let mut found_star = false; @@ -1599,13 +1605,10 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> { // It's re-exported if it's `from ... import x as x` (&asname.id, asname.id == alias.name.id) } else { - // It's re-exported if it's `from . import x` in an `__init__.pyi` - ( - &alias.name.id, - node.level == 1 - && node.module.is_none() - && self.file.is_package(self.db), - ) + // As a non-standard rule to handle stubs in the wild, we consider + // `from . import x` and `from whatever.thispackage import x` in an + // `__init__.pyi` to re-export `x` (as long as it wasn't renamed) + (&alias.name.id, is_self_import) }; // Look for imports `from __future__ import annotations`, ignore `as ...`