Skip to content
Draft
Original file line number Diff line number Diff line change
Expand Up @@ -647,8 +647,8 @@ reveal_type(mypackage.imported.X) # revealed: int

## `from` Import of Other Package's Submodule

`from mypackage import submodule` from outside the package is not modeled as a side-effect on
`mypackage`, even in the importing file (this could be changed!).
`from mypackage import submodule` and `from mypackage.submodule import not_a_submodule` from outside
the package are both modeled as a side-effects on `mypackage`.

### In Stub

Expand All @@ -663,18 +663,28 @@ reveal_type(mypackage.imported.X) # revealed: int
X: int = 42
```

`package2/__init__.pyi`:

```pyi
```

`package2/submodule.pyi`:

```pyi
not_a_submodule: int
```

`main.py`:

```py
import mypackage
import package2
from mypackage import imported
from package2.submodule import not_a_submodule

reveal_type(imported.X) # revealed: int

# TODO: this would be nice to support, but it's dangerous with available_submodule_attributes
# for details, see: https://github.com/astral-sh/ty/issues/1488
# error: [possibly-missing-attribute] "Submodule `imported` may not be available"
reveal_type(mypackage.imported.X) # revealed: Unknown
reveal_type(mypackage.imported.X) # revealed: int
reveal_type(package2.submodule.not_a_submodule) # revealed: int
```

### In Non-Stub
Expand All @@ -690,17 +700,28 @@ reveal_type(mypackage.imported.X) # revealed: Unknown
X: int = 42
```

`package2/__init__.py`:

```py
```

`package2/submodule.py`:

```py
not_a_submodule: int
```

`main.py`:

```py
import mypackage
import package2
from mypackage import imported
from package2.submodule import not_a_submodule

reveal_type(imported.X) # revealed: int

# TODO: this would be nice to support, as it works at runtime
# error: [possibly-missing-attribute] "Submodule `imported` may not be available"
reveal_type(mypackage.imported.X) # revealed: Unknown
reveal_type(mypackage.imported.X) # revealed: int
reveal_type(package2.submodule.not_a_submodule) # revealed: int
```

## `from` Import of Sibling Module
Expand Down Expand Up @@ -859,6 +880,51 @@ from mypackage import funcmod
x = funcmod(1)
```

## A Tale of Two Modules

This is a nonsensical regression test for some incredibly cursed interaction in `ty` where we get
confused about `mypackage.conflicted`. The worst part is that resolving an import
`from typing import <anything that resolves>` is load-bearing, and `typing` seems to be special
here. There is no known reason why `typing` should be special here.

### In Stub

`mypackage/__init__.py`:

```py
from .conflicted.b import x
```

`mypackage/conflicted/__init__.py`:

`mypackage/conflicted/other1/__init__.py`:

```py
x: int = 1
```

`mypackage/conflicted/b/__init__.py`:

```py
x: int = 1
```

`mypackage/conflicted/b/c/__init__.py`:

```py
y: int = 2
```

`main.py`:

```py
from typing import TYPE_CHECKING
from mypackage.conflicted.other1 import x as x1
import mypackage.conflicted.b.c

reveal_type(mypackage.conflicted.b.c.y) # revealed: int
```

## Re-export Nameclash Problems In Functions

`from` imports in an `__init__.py` at file scope should be visible to functions defined in the file:
Expand Down
40 changes: 40 additions & 0 deletions crates/ty_python_semantic/resources/mdtest/import/tracking.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,3 +116,43 @@ b = 1

```py
```

## Submodule is loaded in a non-global scope

We recognise submodules as being available as attributes even if they are loaded in a function
scope. The function might never be executed, which means that the submodule might never be loaded;
however, we prefer to prioritise avoiding false positives over catching all possible errors here.

`a/b.py`:

```py
```

`a/c.py`:

```py
d = 42
```

`a/e/f.py`:

```py
```

`main.py`:

```py
import a

def f():
import a.b
from a.c import d
from a.e import f

f()

reveal_type(a.b) # revealed: <module 'a.b'>
reveal_type(a.c) # revealed: <module 'a.c'>
reveal_type(a.e) # revealed: <module 'a.e'>
reveal_type(a.e.f) # revealed: <module 'a.e.f'>
```
13 changes: 11 additions & 2 deletions crates/ty_python_semantic/src/semantic_index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,10 @@ pub(crate) fn place_table<'db>(db: &'db dyn Db, scope: ScopeId<'db>) -> Arc<Plac
/// See [`ModuleLiteralType::available_submodule_attributes`] for discussion
/// of why this analysis is intentionally limited.
#[salsa::tracked(returns(deref), heap_size=ruff_memory_usage::heap_size)]
pub(crate) fn imported_modules<'db>(db: &'db dyn Db, file: File) -> Arc<FxHashSet<ModuleName>> {
pub(crate) fn imported_modules<'db>(
db: &'db dyn Db,
file: File,
) -> Arc<FxHashMap<ModuleName, ImportKind>> {
semantic_index(db, file).imported_modules.clone()
}

Expand Down Expand Up @@ -249,7 +252,7 @@ pub(crate) struct SemanticIndex<'db> {
ast_ids: IndexVec<FileScopeId, AstIds>,

/// The set of modules that are imported anywhere within this file.
imported_modules: Arc<FxHashSet<ModuleName>>,
imported_modules: Arc<FxHashMap<ModuleName, ImportKind>>,

/// Flags about the global scope (code usage impacting inference)
has_future_annotations: bool,
Expand Down Expand Up @@ -586,6 +589,12 @@ impl<'db> SemanticIndex<'db> {
}
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, get_size2::GetSize)]
pub(crate) enum ImportKind {
Import,
ImportFrom,
}

pub(crate) struct AncestorsIter<'a> {
scopes: &'a IndexSlice<FileScopeId, Scope>,
next_id: Option<FileScopeId>,
Expand Down
83 changes: 54 additions & 29 deletions crates/ty_python_semantic/src/semantic_index/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ use crate::semantic_index::symbol::{ScopedSymbolId, Symbol};
use crate::semantic_index::use_def::{
EnclosingSnapshotKey, FlowSnapshot, ScopedEnclosingSnapshotId, UseDefMapBuilder,
};
use crate::semantic_index::{ExpressionsScopeMap, SemanticIndex, VisibleAncestorsIter};
use crate::semantic_index::{ExpressionsScopeMap, ImportKind, SemanticIndex, VisibleAncestorsIter};
use crate::semantic_model::HasTrackedScope;
use crate::unpack::{EvaluationMode, Unpack, UnpackKind, UnpackPosition, UnpackValue};
use crate::{Db, Program};
Expand Down Expand Up @@ -110,7 +110,7 @@ pub(super) struct SemanticIndexBuilder<'db, 'ast> {
scopes_by_expression: ExpressionsScopeMapBuilder,
definitions_by_node: FxHashMap<DefinitionNodeKey, Definitions<'db>>,
expressions_by_node: FxHashMap<ExpressionNodeKey, Expression<'db>>,
imported_modules: FxHashSet<ModuleName>,
imported_modules: FxHashMap<ModuleName, ImportKind>,
seen_submodule_imports: FxHashSet<String>,
/// Hashset of all [`FileScopeId`]s that correspond to [generator functions].
///
Expand Down Expand Up @@ -150,7 +150,7 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
expressions_by_node: FxHashMap::default(),

seen_submodule_imports: FxHashSet::default(),
imported_modules: FxHashSet::default(),
imported_modules: FxHashMap::default(),
generator_functions: FxHashSet::default(),

enclosing_snapshots: FxHashMap::default(),
Expand Down Expand Up @@ -1472,7 +1472,11 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> {
// Mark the imported module, and all of its parents, as being imported in this
// file.
if let Some(module_name) = ModuleName::new(&alias.name) {
self.imported_modules.extend(module_name.ancestors());
self.imported_modules.extend(
module_name
.ancestors()
.zip(std::iter::repeat(ImportKind::Import)),
);
}

let (symbol_name, is_reexported) = if let Some(asname) = &alias.asname {
Expand Down Expand Up @@ -1513,33 +1517,54 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> {
// that `x` can be freely overwritten, and that we don't assume that an import
// in one function is visible in another function.
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,
node.module.as_deref(),
node.level,
)
&& let Ok(thispackage) = ModuleName::package_for_file(self.db, self.file)
{
let is_package = self.file.is_package(self.db);
let this_package = ModuleName::package_for_file(self.db, self.file);

if let Ok(module_name) = ModuleName::from_identifier_parts(
self.db,
self.file,
node.module.as_deref(),
node.level,
) {
// Record whether this is equivalent to `from . import ...`
is_self_import = module_name == thispackage;
if is_package && let Ok(thispackage) = this_package.as_ref() {
is_self_import = &module_name == thispackage;
}

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 },
);
if node.module.is_some() {
if is_package
&& let Ok(thispackage) = this_package
&& self.current_scope().is_global()
&& 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());

let direct_submodule_name = Name::new(direct_submodule);
let symbol = self.add_symbol(direct_submodule_name);
self.add_definition(
symbol.into(),
ImportFromSubmoduleDefinitionNodeRef { node },
);
} else {
for name in module_name.ancestors() {
self.imported_modules
.entry(name)
.or_insert(ImportKind::ImportFrom);
}
for name in &node.names {
let Some(relative_name) = ModuleName::new(&name.name) else {
continue;
};
let mut full_name = module_name.clone();
full_name.extend(&relative_name);
self.imported_modules
.entry(full_name)
.or_insert(ImportKind::ImportFrom);
}
}
}
}

Expand Down
Loading
Loading