mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-28 04:45:01 +00:00
Rename Red Knot (#17820)
This commit is contained in:
parent
e6a798b962
commit
b51c4f82ea
1564 changed files with 1598 additions and 1578 deletions
166
crates/ty_python_semantic/resources/mdtest/import/basic.md
Normal file
166
crates/ty_python_semantic/resources/mdtest/import/basic.md
Normal 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
|
||||
```
|
|
@ -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
|
||||
```
|
|
@ -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
|
||||
```
|
137
crates/ty_python_semantic/resources/mdtest/import/conditional.md
Normal file
137
crates/ty_python_semantic/resources/mdtest/import/conditional.md
Normal 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
|
||||
```
|
|
@ -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
|
370
crates/ty_python_semantic/resources/mdtest/import/conventions.md
Normal file
370
crates/ty_python_semantic/resources/mdtest/import/conventions.md
Normal 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: ...
|
||||
```
|
88
crates/ty_python_semantic/resources/mdtest/import/errors.md
Normal file
88
crates/ty_python_semantic/resources/mdtest/import/errors.md
Normal 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]]
|
||||
```
|
|
@ -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
|
||||
```
|
271
crates/ty_python_semantic/resources/mdtest/import/relative.md
Normal file
271
crates/ty_python_semantic/resources/mdtest/import/relative.md
Normal 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: ...
|
||||
```
|
1434
crates/ty_python_semantic/resources/mdtest/import/star.md
Normal file
1434
crates/ty_python_semantic/resources/mdtest/import/star.md
Normal file
File diff suppressed because it is too large
Load diff
|
@ -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
|
||||
```
|
31
crates/ty_python_semantic/resources/mdtest/import/stubs.md
Normal file
31
crates/ty_python_semantic/resources/mdtest/import/stubs.md
Normal 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
|
||||
```
|
118
crates/ty_python_semantic/resources/mdtest/import/tracking.md
Normal file
118
crates/ty_python_semantic/resources/mdtest/import/tracking.md
Normal 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
|
||||
```
|
Loading…
Add table
Add a link
Reference in a new issue