Rename Red Knot (#17820)

This commit is contained in:
Micha Reiser 2025-05-03 19:49:15 +02:00 committed by GitHub
parent e6a798b962
commit b51c4f82ea
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
1564 changed files with 1598 additions and 1578 deletions

View file

@ -0,0 +1,166 @@
# Structures
## Class import following
```py
from b import C as D
E = D
reveal_type(E) # revealed: Literal[C]
```
`b.py`:
```py
class C: ...
```
## Module member resolution
```py
import b
D = b.C
reveal_type(D) # revealed: Literal[C]
```
`b.py`:
```py
class C: ...
```
## Nested
```py
import a.b
reveal_type(a.b.C) # revealed: Literal[C]
```
`a/__init__.py`:
```py
```
`a/b.py`:
```py
class C: ...
```
## Deeply nested
```py
import a.b.c
reveal_type(a.b.c.C) # revealed: Literal[C]
```
`a/__init__.py`:
```py
```
`a/b/__init__.py`:
```py
```
`a/b/c.py`:
```py
class C: ...
```
## Nested with rename
```py
import a.b as b
reveal_type(b.C) # revealed: Literal[C]
```
`a/__init__.py`:
```py
```
`a/b.py`:
```py
class C: ...
```
## Deeply nested with rename
```py
import a.b.c as c
reveal_type(c.C) # revealed: Literal[C]
```
`a/__init__.py`:
```py
```
`a/b/__init__.py`:
```py
```
`a/b/c.py`:
```py
class C: ...
```
## Unresolvable module import
<!-- snapshot-diagnostics -->
```py
import zqzqzqzqzqzqzq # error: [unresolved-import] "Cannot resolve import `zqzqzqzqzqzqzq`"
```
## Unresolvable submodule imports
<!-- snapshot-diagnostics -->
```py
# Topmost component resolvable, submodule not resolvable:
import a.foo # error: [unresolved-import] "Cannot resolve import `a.foo`"
# Topmost component unresolvable:
import b.foo # error: [unresolved-import] "Cannot resolve import `b.foo`"
```
`a/__init__.py`:
```py
```
## Long paths
It's unlikely that a single module component is as long as in this example, but Windows treats paths
that are longer than 200 and something specially. This test ensures that ty can handle those paths
gracefully.
```toml
system = "os"
```
`AveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPath/__init__.py`:
```py
class Foo: ...
```
```py
from AveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPath import (
Foo,
)
reveal_type(Foo()) # revealed: Foo
```

View file

@ -0,0 +1,78 @@
# Builtins
## Importing builtin module
Builtin symbols can be explicitly imported:
```py
import builtins
reveal_type(builtins.chr) # revealed: def chr(i: SupportsIndex, /) -> str
```
## Implicit use of builtin
Or used implicitly:
```py
reveal_type(chr) # revealed: def chr(i: SupportsIndex, /) -> str
reveal_type(str) # revealed: Literal[str]
```
## Builtin symbol from custom typeshed
If we specify a custom typeshed, we can use the builtin symbol from it, and no longer access the
builtins from the "actual" vendored typeshed:
```toml
[environment]
typeshed = "/typeshed"
```
`/typeshed/stdlib/builtins.pyi`:
```pyi
class Custom: ...
custom_builtin: Custom
```
`/typeshed/stdlib/typing_extensions.pyi`:
```pyi
def reveal_type(obj, /): ...
```
```py
reveal_type(custom_builtin) # revealed: Custom
# error: [unresolved-reference]
reveal_type(str) # revealed: Unknown
```
## Unknown builtin (later defined)
`foo` has a type of `Unknown` in this example, as it relies on `bar` which has not been defined at
that point:
```toml
[environment]
typeshed = "/typeshed"
```
`/typeshed/stdlib/builtins.pyi`:
```pyi
foo = bar
bar = 1
```
`/typeshed/stdlib/typing_extensions.pyi`:
```pyi
def reveal_type(obj, /): ...
```
```py
reveal_type(foo) # revealed: Unknown
```

View file

@ -0,0 +1,128 @@
# Case Sensitive Imports
```toml
system = "os"
```
Python's import system is case-sensitive even on case-insensitive file system. This means, importing
a module `a` should fail if the file in the search paths is named `A.py`. See
[PEP 235](https://peps.python.org/pep-0235/).
## Correct casing
Importing a module where the name matches the file name's casing should succeed.
`a.py`:
```py
class Foo:
x: int = 1
```
```python
from a import Foo
reveal_type(Foo().x) # revealed: int
```
## Incorrect casing
Importing a module where the name does not match the file name's casing should fail.
`A.py`:
```py
class Foo:
x: int = 1
```
```python
# error: [unresolved-import]
from a import Foo
```
## Multiple search paths with different cased modules
The resolved module is the first matching the file name's casing but Python falls back to later
search paths if the file name's casing does not match.
```toml
[environment]
extra-paths = ["/search-1", "/search-2"]
```
`/search-1/A.py`:
```py
class Foo:
x: int = 1
```
`/search-2/a.py`:
```py
class Bar:
x: str = "test"
```
```python
from A import Foo
from a import Bar
reveal_type(Foo().x) # revealed: int
reveal_type(Bar().x) # revealed: str
```
## Intermediate segments
`db/__init__.py`:
```py
```
`db/a.py`:
```py
class Foo:
x: int = 1
```
`correctly_cased.py`:
```python
from db.a import Foo
reveal_type(Foo().x) # revealed: int
```
Imports where some segments are incorrectly cased should fail.
`incorrectly_cased.py`:
```python
# error: [unresolved-import]
from DB.a import Foo
# error: [unresolved-import]
from DB.A import Foo
# error: [unresolved-import]
from db.A import Foo
```
## Incorrect extension casing
The extension of imported python modules must be `.py` or `.pyi` but not `.PY` or `Py` or any
variant where some characters are uppercase.
`a.PY`:
```py
class Foo:
x: int = 1
```
```python
# error: [unresolved-import]
from a import Foo
```

View file

@ -0,0 +1,137 @@
# Conditional imports
## Maybe unbound
`maybe_unbound.py`:
```py
def coinflip() -> bool:
return True
if coinflip():
y = 3
x = y # error: [possibly-unresolved-reference]
# revealed: Literal[3]
reveal_type(x)
# revealed: Literal[3]
# error: [possibly-unresolved-reference]
reveal_type(y)
```
```py
# error: [possibly-unbound-import] "Member `y` of module `maybe_unbound` is possibly unbound"
from maybe_unbound import x, y
reveal_type(x) # revealed: Unknown | Literal[3]
reveal_type(y) # revealed: Unknown | Literal[3]
```
## Maybe unbound annotated
`maybe_unbound_annotated.py`:
```py
def coinflip() -> bool:
return True
if coinflip():
y: int = 3
x = y # error: [possibly-unresolved-reference]
# revealed: Literal[3]
reveal_type(x)
# revealed: Literal[3]
# error: [possibly-unresolved-reference]
reveal_type(y)
```
Importing an annotated name prefers the declared type over the inferred type:
```py
# error: [possibly-unbound-import] "Member `y` of module `maybe_unbound_annotated` is possibly unbound"
from maybe_unbound_annotated import x, y
reveal_type(x) # revealed: Unknown | Literal[3]
reveal_type(y) # revealed: int
```
## Maybe undeclared
Importing a possibly undeclared name still gives us its declared type:
`maybe_undeclared.py`:
```py
def coinflip() -> bool:
return True
if coinflip():
x: int
```
```py
from maybe_undeclared import x
reveal_type(x) # revealed: int
```
## Reimport
`c.py`:
```py
def f(): ...
```
`b.py`:
```py
def coinflip() -> bool:
return True
if coinflip():
from c import f
else:
def f(): ...
```
```py
from b import f
# TODO: We should disambiguate in such cases between `b.f` and `c.f`.
reveal_type(f) # revealed: (def f() -> Unknown) | (def f() -> Unknown)
```
## Reimport with stub declaration
When we have a declared type in one path and only an inferred-from-definition type in the other, we
should still be able to unify those:
`c.pyi`:
```pyi
x: int
```
`b.py`:
```py
def coinflip() -> bool:
return True
if coinflip():
from c import x
else:
x = 1
```
```py
from b import x
reveal_type(x) # revealed: int
```

View file

@ -0,0 +1,91 @@
# Conflicting attributes and submodules
## Via import
```py
import a.b
reveal_type(a.b) # revealed: <module 'a.b'>
```
`a/__init__.py`:
```py
b: int = 42
```
`a/b.py`:
```py
```
## Via from/import
```py
from a import b
reveal_type(b) # revealed: int
```
`a/__init__.py`:
```py
b: int = 42
```
`a/b.py`:
```py
```
## Via both
```py
import a.b
from a import b
reveal_type(b) # revealed: <module 'a.b'>
reveal_type(a.b) # revealed: <module 'a.b'>
```
`a/__init__.py`:
```py
b: int = 42
```
`a/b.py`:
```py
```
## Via both (backwards)
In this test, we infer a different type for `b` than the runtime behavior of the Python interpreter.
The interpreter will not load the submodule `a.b` during the `from a import b` statement, since `a`
contains a non-module attribute named `b`. (See the [definition][from-import] of a `from...import`
statement for details.) However, because our import tracking is flow-insensitive, we will see that
`a.b` is imported somewhere in the file, and therefore assume that the `from...import` statement
sees the submodule as the value of `b` instead of the integer.
```py
from a import b
import a.b
# Python would say `int` for `b`
reveal_type(b) # revealed: <module 'a.b'>
reveal_type(a.b) # revealed: <module 'a.b'>
```
`a/__init__.py`:
```py
b: int = 42
```
`a/b.py`:
```py
```
[from-import]: https://docs.python.org/3/reference/simple_stmts.html#the-import-statement

View file

@ -0,0 +1,370 @@
# Import conventions
This document describes the conventions for importing symbols.
Reference:
- <https://typing.python.org/en/latest/spec/distributing.html#import-conventions>
## Builtins scope
When looking up for a name, ty will fallback to using the builtins scope if the name is not found in
the global scope. The `builtins.pyi` file, that will be used to resolve any symbol in the builtins
scope, contains multiple symbols from other modules (e.g., `typing`) but those are not re-exported.
```py
# These symbols are being imported in `builtins.pyi` but shouldn't be considered as being
# available in the builtins scope.
# error: "Name `Literal` used when not defined"
reveal_type(Literal) # revealed: Unknown
# error: "Name `sys` used when not defined"
reveal_type(sys) # revealed: Unknown
```
## Builtins import
Similarly, trying to import the symbols from the builtins module which aren't re-exported should
also raise an error.
```py
# error: "Module `builtins` has no member `Literal`"
# error: "Module `builtins` has no member `sys`"
from builtins import Literal, sys
reveal_type(Literal) # revealed: Unknown
reveal_type(sys) # revealed: Unknown
# error: "Module `math` has no member `Iterable`"
from math import Iterable
reveal_type(Iterable) # revealed: Unknown
```
## Re-exported symbols in stub files
When a symbol is re-exported, importing it should not raise an error. This tests both `import ...`
and `from ... import ...` forms.
Note: Submodule imports in `import ...` form doesn't work because it's a syntax error. For example,
in `import os.path as os.path` the `os.path` is not a valid identifier.
```py
from b import Any, Literal, foo
reveal_type(Any) # revealed: typing.Any
reveal_type(Literal) # revealed: typing.Literal
reveal_type(foo) # revealed: <module 'foo'>
```
`b.pyi`:
```pyi
import foo as foo
from typing import Any as Any, Literal as Literal
```
`foo.py`:
```py
```
## Non-exported symbols in stub files
Here, none of the symbols are being re-exported in the stub file.
```py
# error: 15 [unresolved-import] "Module `b` has no member `foo`"
# error: 20 [unresolved-import] "Module `b` has no member `Any`"
# error: 25 [unresolved-import] "Module `b` has no member `Literal`"
from b import foo, Any, Literal
reveal_type(Any) # revealed: Unknown
reveal_type(Literal) # revealed: Unknown
reveal_type(foo) # revealed: Unknown
```
`b.pyi`:
```pyi
import foo
from typing import Any, Literal
```
`foo.pyi`:
```pyi
```
## Nested non-exports
Here, a chain of modules all don't re-export an import.
```py
# error: "Module `a` has no member `Any`"
from a import Any
reveal_type(Any) # revealed: Unknown
```
`a.pyi`:
```pyi
# error: "Module `b` has no member `Any`"
from b import Any
reveal_type(Any) # revealed: Unknown
```
`b.pyi`:
```pyi
# error: "Module `c` has no member `Any`"
from c import Any
reveal_type(Any) # revealed: Unknown
```
`c.pyi`:
```pyi
from typing import Any
reveal_type(Any) # revealed: typing.Any
```
## Nested mixed re-export and not
But, if the symbol is being re-exported explicitly in one of the modules in the chain, it should not
raise an error at that step in the chain.
```py
# error: "Module `a` has no member `Any`"
from a import Any
reveal_type(Any) # revealed: Unknown
```
`a.pyi`:
```pyi
from b import Any
reveal_type(Any) # revealed: Unknown
```
`b.pyi`:
```pyi
# error: "Module `c` has no member `Any`"
from c import Any as Any
reveal_type(Any) # revealed: Unknown
```
`c.pyi`:
```pyi
from typing import Any
reveal_type(Any) # revealed: typing.Any
```
## Exported as different name
The re-export convention only works when the aliased name is exactly the same as the original name.
```py
# error: "Module `a` has no member `Foo`"
from a import Foo
reveal_type(Foo) # revealed: Unknown
```
`a.pyi`:
```pyi
from b import AnyFoo as Foo
reveal_type(Foo) # revealed: Literal[AnyFoo]
```
`b.pyi`:
```pyi
class AnyFoo: ...
```
## Exported using `__all__`
Here, the symbol is re-exported using the `__all__` variable.
```py
# TODO: This should *not* be an error but we don't understand `__all__` yet.
# error: "Module `a` has no member `Foo`"
from a import Foo
```
`a.pyi`:
```pyi
from b import Foo
__all__ = ['Foo']
```
`b.pyi`:
```pyi
class Foo: ...
```
## Re-exports in `__init__.pyi`
Similarly, for an `__init__.pyi` (stub) file, importing a non-exported name should raise an error
but the inference would be `Unknown`.
```py
# error: 15 "Module `a` has no member `Foo`"
# error: 20 "Module `a` has no member `c`"
from a import Foo, c, foo
reveal_type(Foo) # revealed: Unknown
reveal_type(c) # revealed: Unknown
reveal_type(foo) # revealed: <module 'a.foo'>
```
`a/__init__.pyi`:
```pyi
from .b import c
from .foo import Foo
```
`a/foo.pyi`:
```pyi
class Foo: ...
```
`a/b/__init__.pyi`:
```pyi
```
`a/b/c.pyi`:
```pyi
```
## Conditional re-export in stub file
The following scenarios are when a re-export happens conditionally in a stub file.
### Global import
```py
# error: "Member `Foo` of module `a` is possibly unbound"
from a import Foo
reveal_type(Foo) # revealed: str
```
`a.pyi`:
```pyi
from b import Foo
def coinflip() -> bool: ...
if coinflip():
Foo: str = ...
reveal_type(Foo) # revealed: Literal[Foo] | str
```
`b.pyi`:
```pyi
class Foo: ...
```
### Both branch is an import
Here, both the branches of the condition are import statements where one of them re-exports while
the other does not.
```py
# error: "Member `Foo` of module `a` is possibly unbound"
from a import Foo
reveal_type(Foo) # revealed: Literal[Foo]
```
`a.pyi`:
```pyi
def coinflip() -> bool: ...
if coinflip():
from b import Foo
else:
from b import Foo as Foo
reveal_type(Foo) # revealed: Literal[Foo]
```
`b.pyi`:
```pyi
class Foo: ...
```
### Re-export in one branch
```py
# error: "Member `Foo` of module `a` is possibly unbound"
from a import Foo
reveal_type(Foo) # revealed: Literal[Foo]
```
`a.pyi`:
```pyi
def coinflip() -> bool: ...
if coinflip():
from b import Foo as Foo
```
`b.pyi`:
```pyi
class Foo: ...
```
### Non-export in one branch
```py
# error: "Module `a` has no member `Foo`"
from a import Foo
reveal_type(Foo) # revealed: Unknown
```
`a.pyi`:
```pyi
def coinflip() -> bool: ...
if coinflip():
from b import Foo
```
`b.pyi`:
```pyi
class Foo: ...
```

View file

@ -0,0 +1,88 @@
# Unresolved Imports
## Unresolved import statement
```py
import bar # error: "Cannot resolve import `bar`"
reveal_type(bar) # revealed: Unknown
```
## Unresolved import from statement
```py
from bar import baz # error: "Cannot resolve import `bar`"
reveal_type(baz) # revealed: Unknown
```
## Unresolved import from resolved module
`a.py`:
```py
```
```py
from a import thing # error: "Module `a` has no member `thing`"
reveal_type(thing) # revealed: Unknown
```
## Resolved import of symbol from unresolved import
`a.py`:
```py
import foo as foo # error: "Cannot resolve import `foo`"
reveal_type(foo) # revealed: Unknown
```
Importing the unresolved import into a second file should not trigger an additional "unresolved
import" violation:
```py
from a import foo
reveal_type(foo) # revealed: Unknown
```
## No implicit shadowing
`b.py`:
```py
x: int
```
```py
from b import x
x = "foo" # error: [invalid-assignment] "Object of type `Literal["foo"]"
```
## Import cycle
`a.py`:
```py
class A: ...
reveal_type(A.__mro__) # revealed: tuple[Literal[A], Literal[object]]
import b
class C(b.B): ...
reveal_type(C.__mro__) # revealed: tuple[Literal[C], Literal[B], Literal[A], Literal[object]]
```
`b.py`:
```py
from a import A
class B(A): ...
reveal_type(B.__mro__) # revealed: tuple[Literal[B], Literal[A], Literal[object]]
```

View file

@ -0,0 +1,35 @@
# Invalid syntax
## Missing module name
```py
from import bar # error: [invalid-syntax]
reveal_type(bar) # revealed: Unknown
```
## Invalid nested module import
TODO: This is correctly flagged as an error, but we could clean up the diagnostics that we report.
```py
# TODO: No second diagnostic
# error: [invalid-syntax] "Expected ',', found '.'"
# error: [unresolved-import] "Module `a` has no member `c`"
from a import b.c
# TODO: Should these be inferred as Unknown?
reveal_type(b) # revealed: <module 'a.b'>
reveal_type(b.c) # revealed: int
```
`a/__init__.py`:
```py
```
`a/b.py`:
```py
c: int = 1
```

View file

@ -0,0 +1,271 @@
# Relative
## Non-existent
`package/__init__.py`:
```py
```
`package/bar.py`:
```py
from .foo import X # error: [unresolved-import]
reveal_type(X) # revealed: Unknown
```
## Simple
`package/__init__.py`:
```py
```
`package/foo.py`:
```py
X: int = 42
```
`package/bar.py`:
```py
from .foo import X
reveal_type(X) # revealed: int
```
## Dotted
`package/__init__.py`:
```py
```
`package/foo/bar/baz.py`:
```py
X: int = 42
```
`package/bar.py`:
```py
from .foo.bar.baz import X
reveal_type(X) # revealed: int
```
## Bare to package
`package/__init__.py`:
```py
X: int = 42
```
`package/bar.py`:
```py
from . import X
reveal_type(X) # revealed: int
```
## Non-existent + bare to package
`package/bar.py`:
```py
from . import X # error: [unresolved-import]
reveal_type(X) # revealed: Unknown
```
## Dunder init
`package/__init__.py`:
```py
from .foo import X
reveal_type(X) # revealed: int
```
`package/foo.py`:
```py
X: int = 42
```
## Non-existent + dunder init
`package/__init__.py`:
```py
from .foo import X # error: [unresolved-import]
reveal_type(X) # revealed: Unknown
```
## Long relative import
`package/__init__.py`:
```py
```
`package/foo.py`:
```py
X: int = 42
```
`package/subpackage/subsubpackage/bar.py`:
```py
from ...foo import X
reveal_type(X) # revealed: int
```
## Unbound symbol
`package/__init__.py`:
```py
```
`package/foo.py`:
```py
x # error: [unresolved-reference]
```
`package/bar.py`:
```py
from .foo import x # error: [unresolved-import]
reveal_type(x) # revealed: Unknown
```
## Bare to module
`package/__init__.py`:
```py
```
`package/foo.py`:
```py
X: int = 42
```
`package/bar.py`:
```py
from . import foo
reveal_type(foo.X) # revealed: int
```
## Non-existent + bare to module
This test verifies that we emit an error when we try to import a symbol that is neither a submodule
nor an attribute of `package`.
`package/__init__.py`:
```py
```
`package/bar.py`:
```py
from . import foo # error: [unresolved-import]
reveal_type(foo) # revealed: Unknown
```
## Import submodule from self
We don't currently consider `from...import` statements when building up the `imported_modules` set
in the semantic index. When accessing an attribute of a module, we only consider it a potential
submodule when that submodule name appears in the `imported_modules` set. That means that submodules
that are imported via `from...import` are not visible to our type inference if you also access that
submodule via the attribute on its parent package.
`package/__init__.py`:
```py
```
`package/foo.py`:
```py
X: int = 42
```
`package/bar.py`:
```py
from . import foo
import package
# error: [unresolved-attribute] "Type `<module 'package'>` has no attribute `foo`"
reveal_type(package.foo.X) # revealed: Unknown
```
## Relative imports at the top of a search path
Relative imports at the top of a search path result in a runtime error:
`ImportError: attempted relative import with no known parent package`. That's why ty should disallow
them.
`parser.py`:
```py
X: int = 42
```
`__main__.py`:
```py
from .parser import X # error: [unresolved-import]
```
## Relative imports in `site-packages`
Relative imports in `site-packages` are correctly resolved even when the `site-packages` search path
is a subdirectory of the first-party search path. Note that mdtest sets the first-party search path
to `/src/`, which is why the virtual environment in this test is a subdirectory of `/src/`, even
though this is not how a typical Python project would be structured:
```toml
[environment]
python = "/src/.venv"
python-version = "3.13"
```
`/src/bar.py`:
```py
from foo import A
reveal_type(A) # revealed: Literal[A]
```
`/src/.venv/<path-to-site-packages>/foo/__init__.py`:
```py
from .a import A as A
```
`/src/.venv/<path-to-site-packages>/foo/a.py`:
```py
class A: ...
```

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,286 @@
# Stub packages
Stub packages are packages named `<package>-stubs` that provide typing stubs for `<package>`. See
[specification](https://typing.python.org/en/latest/spec/distributing.html#stub-only-packages).
## Simple stub
```toml
[environment]
extra-paths = ["/packages"]
```
`/packages/foo-stubs/__init__.pyi`:
```pyi
class Foo:
name: str
age: int
```
`/packages/foo/__init__.py`:
```py
class Foo: ...
```
`main.py`:
```py
from foo import Foo
reveal_type(Foo().name) # revealed: str
```
## Stubs only
The regular package isn't required for type checking.
```toml
[environment]
extra-paths = ["/packages"]
```
`/packages/foo-stubs/__init__.pyi`:
```pyi
class Foo:
name: str
age: int
```
`main.py`:
```py
from foo import Foo
reveal_type(Foo().name) # revealed: str
```
## `-stubs` named module
A module named `<module>-stubs` isn't a stub package.
```toml
[environment]
extra-paths = ["/packages"]
```
`/packages/foo-stubs.pyi`:
```pyi
class Foo:
name: str
age: int
```
`main.py`:
```py
from foo import Foo # error: [unresolved-import]
reveal_type(Foo().name) # revealed: Unknown
```
## Namespace package in different search paths
A namespace package with multiple stub packages spread over multiple search paths.
```toml
[environment]
extra-paths = ["/stubs1", "/stubs2", "/packages"]
```
`/stubs1/shapes-stubs/polygons/pentagon.pyi`:
```pyi
class Pentagon:
sides: int
area: float
```
`/stubs2/shapes-stubs/polygons/hexagon.pyi`:
```pyi
class Hexagon:
sides: int
area: float
```
`/packages/shapes/polygons/pentagon.py`:
```py
class Pentagon: ...
```
`/packages/shapes/polygons/hexagon.py`:
```py
class Hexagon: ...
```
`main.py`:
```py
from shapes.polygons.hexagon import Hexagon
from shapes.polygons.pentagon import Pentagon
reveal_type(Pentagon().sides) # revealed: int
reveal_type(Hexagon().area) # revealed: int | float
```
## Inconsistent stub packages
Stub packages where one is a namespace package and the other is a regular package. Module resolution
should stop after the first non-namespace stub package. This matches Pyright's behavior.
```toml
[environment]
extra-paths = ["/stubs1", "/stubs2", "/packages"]
```
`/stubs1/shapes-stubs/__init__.pyi`:
```pyi
```
`/stubs1/shapes-stubs/polygons/__init__.pyi`:
```pyi
```
`/stubs1/shapes-stubs/polygons/pentagon.pyi`:
```pyi
class Pentagon:
sides: int
area: float
```
`/stubs2/shapes-stubs/polygons/hexagon.pyi`:
```pyi
class Hexagon:
sides: int
area: float
```
`/packages/shapes/polygons/pentagon.py`:
```py
class Pentagon: ...
```
`/packages/shapes/polygons/hexagon.py`:
```py
class Hexagon: ...
```
`main.py`:
```py
from shapes.polygons.pentagon import Pentagon
from shapes.polygons.hexagon import Hexagon # error: [unresolved-import]
reveal_type(Pentagon().sides) # revealed: int
reveal_type(Hexagon().area) # revealed: Unknown
```
## Namespace stubs for non-namespace package
The runtime package is a regular package but the stubs are namespace packages. Pyright skips the
stub package if the "regular" package isn't a namespace package. I'm not aware that the behavior
here is specified, and using the stubs without probing the runtime package first requires slightly
fewer lookups.
```toml
[environment]
extra-paths = ["/packages"]
```
`/packages/shapes-stubs/polygons/pentagon.pyi`:
```pyi
class Pentagon:
sides: int
area: float
```
`/packages/shapes-stubs/polygons/hexagon.pyi`:
```pyi
class Hexagon:
sides: int
area: float
```
`/packages/shapes/__init__.py`:
```py
```
`/packages/shapes/polygons/__init__.py`:
```py
```
`/packages/shapes/polygons/pentagon.py`:
```py
class Pentagon: ...
```
`/packages/shapes/polygons/hexagon.py`:
```py
class Hexagon: ...
```
`main.py`:
```py
from shapes.polygons.pentagon import Pentagon
from shapes.polygons.hexagon import Hexagon
reveal_type(Pentagon().sides) # revealed: int
reveal_type(Hexagon().area) # revealed: int | float
```
## Stub package using `__init__.py` over `.pyi`
It's recommended that stub packages use `__init__.pyi` files over `__init__.py` but it doesn't seem
to be an enforced convention. At least, Pyright is fine with the following.
```toml
[environment]
extra-paths = ["/packages"]
```
`/packages/shapes-stubs/__init__.py`:
```py
class Pentagon:
sides: int
area: float
class Hexagon:
sides: int
area: float
```
`/packages/shapes/__init__.py`:
```py
class Pentagon: ...
class Hexagon: ...
```
`main.py`:
```py
from shapes import Hexagon, Pentagon
reveal_type(Pentagon().sides) # revealed: int
reveal_type(Hexagon().area) # revealed: int | float
```

View file

@ -0,0 +1,31 @@
# Stubs
## Import from stub declaration
```py
from b import x
y = x
reveal_type(y) # revealed: int
```
`b.pyi`:
```pyi
x: int
```
## Import from non-stub with declaration and definition
```py
from b import x
y = x
reveal_type(y) # revealed: int
```
`b.py`:
```py
x: int = 1
```

View file

@ -0,0 +1,118 @@
# Tracking imported modules
These tests depend on how we track which modules have been imported. There are currently two
characteristics of our module tracking that can lead to inaccuracies:
- Imports are tracked on a per-file basis. At runtime, importing a submodule in one file makes that
submodule globally available via any reference to the containing package. We will flag an error
if a file tries to access a submodule without there being an import of that submodule _in that
same file_.
This is a purposeful decision, and not one we plan to change. If a module wants to re-export some
other module that it imports, there are ways to do that (tested below) that are blessed by the
typing spec and that are visible to our file-scoped import tracking.
- Imports are tracked flow-insensitively: submodule accesses are allowed and resolved if that
submodule is imported _anywhere in the file_. This handles the common case where all imports are
grouped at the top of the file, and is easiest to implement. We might revisit this decision and
track submodule imports flow-sensitively, in which case we will have to update the assertions in
some of these tests.
## Import submodule later in file
This test highlights our flow-insensitive analysis, since we access the `a.b` submodule before it
has been imported.
```py
import a
# Would be an error with flow-sensitive tracking
reveal_type(a.b.C) # revealed: Literal[C]
import a.b
```
`a/__init__.py`:
```py
```
`a/b.py`:
```py
class C: ...
```
## Rename a re-export
This test highlights how import tracking is local to each file, but specifically to the file where a
containing module is first referenced. This allows the main module to see that `q.a` contains a
submodule `b`, even though `a.b` is never imported in the main module.
```py
from q import a, b
reveal_type(b) # revealed: <module 'a.b'>
reveal_type(b.C) # revealed: Literal[C]
reveal_type(a.b) # revealed: <module 'a.b'>
reveal_type(a.b.C) # revealed: Literal[C]
```
`a/__init__.py`:
```py
```
`a/b.py`:
```py
class C: ...
```
`q.py`:
```py
import a as a
import a.b as b
```
## Attribute overrides submodule
Technically, either a submodule or a non-module attribute could shadow the other, depending on the
ordering of when the submodule is loaded relative to the parent module's `__init__.py` file being
evaluated. We have chosen to always have the submodule take priority. (This matches pyright's
current behavior, and opposite of mypy's current behavior.)
```py
import sub.b
import attr.b
# In the Python interpreter, `attr.b` is Literal[1]
reveal_type(sub.b) # revealed: <module 'sub.b'>
reveal_type(attr.b) # revealed: <module 'attr.b'>
```
`sub/__init__.py`:
```py
b = 1
```
`sub/b.py`:
```py
```
`attr/__init__.py`:
```py
from . import b as _
b = 1
```
`attr/b.py`:
```py
```