mirror of https://github.com/astral-sh/ruff
[red-knot] Add more tests for `*` imports (#16955)
## Summary This PR separates out the entirely new tests from https://github.com/astral-sh/ruff/pull/16923 into a standalone PR. I'll rebase https://github.com/astral-sh/ruff/pull/16923 on top of this branch. The reasons for separating it out are: - It should make it clearer to see in <https://github.com/astral-sh/ruff/pull/16923> exactly how the functionality is changing (we can see the assertions in the tests _change_, which isn't so obvious if the tests are entirely new) - The diff on <https://github.com/astral-sh/ruff/pull/16923> is getting pretty big; this should reduce the diff on that PR somewhat - These tests seem useful in and of themselves, so even if we need to do a wholesale revert of <https://github.com/astral-sh/ruff/pull/16923> for whatever reason, it'll be nice to keep the tests ## Test Plan `cargo test -p red_knot_python_semantic`
This commit is contained in:
parent
85b7f808e1
commit
66d0cf2a72
|
|
@ -148,6 +148,201 @@ reveal_type(X) # revealed: Unknown
|
||||||
reveal_type(Y) # revealed: Unknown
|
reveal_type(Y) # revealed: Unknown
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Global-scope symbols defined in many other ways
|
||||||
|
|
||||||
|
`a.py`:
|
||||||
|
|
||||||
|
```py
|
||||||
|
import typing
|
||||||
|
from collections import OrderedDict
|
||||||
|
from collections import OrderedDict as Foo
|
||||||
|
|
||||||
|
A, B = 1, (C := 2)
|
||||||
|
D: (E := 4) = (F := 5) # error: [invalid-type-form]
|
||||||
|
|
||||||
|
for G in [1]:
|
||||||
|
...
|
||||||
|
|
||||||
|
for (H := 4).whatever in [2]: # error: [unresolved-attribute]
|
||||||
|
...
|
||||||
|
|
||||||
|
class I: ...
|
||||||
|
|
||||||
|
def J(): ...
|
||||||
|
|
||||||
|
type K = int
|
||||||
|
|
||||||
|
with () as L: # error: [invalid-context-manager]
|
||||||
|
...
|
||||||
|
|
||||||
|
match 42:
|
||||||
|
case {"something": M}:
|
||||||
|
...
|
||||||
|
case [*N]:
|
||||||
|
...
|
||||||
|
case [O]:
|
||||||
|
...
|
||||||
|
case P | Q:
|
||||||
|
...
|
||||||
|
case object(foo=R):
|
||||||
|
...
|
||||||
|
case object(S):
|
||||||
|
...
|
||||||
|
case T:
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
`b.py`:
|
||||||
|
|
||||||
|
```py
|
||||||
|
from a import *
|
||||||
|
|
||||||
|
# fmt: off
|
||||||
|
|
||||||
|
print((
|
||||||
|
# TODO: false positive
|
||||||
|
A, # error: [unresolved-reference]
|
||||||
|
# TODO: false positive
|
||||||
|
B, # error: [unresolved-reference]
|
||||||
|
# TODO: false positive
|
||||||
|
C, # error: [unresolved-reference]
|
||||||
|
# TODO: false positive
|
||||||
|
D, # error: [unresolved-reference]
|
||||||
|
# TODO: false positive
|
||||||
|
E, # error: [unresolved-reference]
|
||||||
|
# TODO: false positive
|
||||||
|
F, # error: [unresolved-reference]
|
||||||
|
# TODO: could emit diagnostic about being possibly unbound, but this is a false positive
|
||||||
|
G, # error: [unresolved-reference] "Name `G` used when not defined"
|
||||||
|
# TODO: false positive
|
||||||
|
H, # error: [unresolved-reference]
|
||||||
|
# TODO: false positive
|
||||||
|
I, # error: [unresolved-reference]
|
||||||
|
# TODO: false positive
|
||||||
|
J, # error: [unresolved-reference]
|
||||||
|
# TODO: false positive
|
||||||
|
K, # error: [unresolved-reference]
|
||||||
|
# TODO: false positive
|
||||||
|
L, # error: [unresolved-reference]
|
||||||
|
# TODO: could emit diagnostic about being possibly unbound, but this is a false positive
|
||||||
|
M, # error: [unresolved-reference] "Name `M` used when not defined"
|
||||||
|
# TODO: could emit diagnostic about being possibly unbound, but this is a false positive
|
||||||
|
N, # error: [unresolved-reference] "Name `N` used when not defined"
|
||||||
|
# TODO: could emit diagnostic about being possibly unbound, but this is a false positive
|
||||||
|
O, # error: [unresolved-reference] "Name `O` used when not defined"
|
||||||
|
# TODO: could emit diagnostic about being possibly unbound, but this is a false positive
|
||||||
|
P, # error: [unresolved-reference] "Name `P` used when not defined"
|
||||||
|
# TODO: could emit diagnostic about being possibly unbound, but this is a false positive
|
||||||
|
Q, # error: [unresolved-reference] "Name `Q` used when not defined"
|
||||||
|
# TODO: could emit diagnostic about being possibly unbound, but this is a false positive
|
||||||
|
R, # error: [unresolved-reference] "Name `R` used when not defined"
|
||||||
|
# TODO: could emit diagnostic about being possibly unbound, but this is a false positive
|
||||||
|
S, # error: [unresolved-reference] "Name `S` used when not defined"
|
||||||
|
# TODO: could emit diagnostic about being possibly unbound, but this is a false positive
|
||||||
|
T, # error: [unresolved-reference] "Name `T` used when not defined"
|
||||||
|
# TODO: false positive
|
||||||
|
typing, # error: [unresolved-reference]
|
||||||
|
# TODO: false positive
|
||||||
|
OrderedDict, # error: [unresolved-reference]
|
||||||
|
# TODO: false positive
|
||||||
|
Foo, # error: [unresolved-reference]
|
||||||
|
))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Definitions in function-like scopes are not global definitions
|
||||||
|
|
||||||
|
Except for some cases involving walrus expressions inside comprehension scopes.
|
||||||
|
|
||||||
|
`a.py`:
|
||||||
|
|
||||||
|
```py
|
||||||
|
class Iterator:
|
||||||
|
def __next__(self) -> int:
|
||||||
|
return 42
|
||||||
|
|
||||||
|
class Iterable:
|
||||||
|
def __iter__(self) -> Iterator:
|
||||||
|
return Iterator()
|
||||||
|
|
||||||
|
[a for a in Iterable()]
|
||||||
|
{b for b in Iterable()}
|
||||||
|
{c: c for c in Iterable()}
|
||||||
|
(d for d in Iterable())
|
||||||
|
lambda e: (f := 42)
|
||||||
|
|
||||||
|
# Definitions created by walruses in a comprehension scope are unique;
|
||||||
|
# they "leak out" of the scope and are stored in the surrounding scope
|
||||||
|
[(g := h * 2) for h in Iterable()]
|
||||||
|
[i for j in Iterable() if (i := j - 10) > 0]
|
||||||
|
{(k := l * 2): (m := l * 3) for l in Iterable()}
|
||||||
|
```
|
||||||
|
|
||||||
|
`b.py`:
|
||||||
|
|
||||||
|
```py
|
||||||
|
from a import *
|
||||||
|
|
||||||
|
# error: [unresolved-reference]
|
||||||
|
reveal_type(a) # revealed: Unknown
|
||||||
|
# error: [unresolved-reference]
|
||||||
|
reveal_type(b) # revealed: Unknown
|
||||||
|
# error: [unresolved-reference]
|
||||||
|
reveal_type(c) # revealed: Unknown
|
||||||
|
# error: [unresolved-reference]
|
||||||
|
reveal_type(d) # revealed: Unknown
|
||||||
|
# error: [unresolved-reference]
|
||||||
|
reveal_type(e) # revealed: Unknown
|
||||||
|
# error: [unresolved-reference]
|
||||||
|
reveal_type(f) # revealed: Unknown
|
||||||
|
# error: [unresolved-reference]
|
||||||
|
reveal_type(h) # revealed: Unknown
|
||||||
|
# error: [unresolved-reference]
|
||||||
|
reveal_type(j) # revealed: Unknown
|
||||||
|
|
||||||
|
# TODO: these should all reveal `Unknown | int` and should not have diagnostics.
|
||||||
|
# (We don't generally model elsewhere in red-knot that bindings from walruses
|
||||||
|
# "leak" from comprehension scopes into outer scopes, but we should.)
|
||||||
|
# See https://github.com/astral-sh/ruff/issues/16954
|
||||||
|
#
|
||||||
|
# error: [unresolved-reference]
|
||||||
|
reveal_type(g) # revealed: Unknown
|
||||||
|
# error: [unresolved-reference]
|
||||||
|
reveal_type(i) # revealed: Unknown
|
||||||
|
# error: [unresolved-reference]
|
||||||
|
reveal_type(k) # revealed: Unknown
|
||||||
|
# error: [unresolved-reference]
|
||||||
|
reveal_type(m) # revealed: Unknown
|
||||||
|
```
|
||||||
|
|
||||||
|
### An annotation without a value is a definition in a stub but not a `.py` file
|
||||||
|
|
||||||
|
`a.pyi`:
|
||||||
|
|
||||||
|
```pyi
|
||||||
|
X: bool
|
||||||
|
```
|
||||||
|
|
||||||
|
`b.py`:
|
||||||
|
|
||||||
|
```py
|
||||||
|
Y: bool
|
||||||
|
```
|
||||||
|
|
||||||
|
`c.py`:
|
||||||
|
|
||||||
|
```py
|
||||||
|
from a import *
|
||||||
|
from b import *
|
||||||
|
|
||||||
|
# TODO: this is a false positive, should reveal `bool`
|
||||||
|
# error: [unresolved-reference]
|
||||||
|
reveal_type(X) # revealed: Unknown
|
||||||
|
|
||||||
|
# but this diagnostic is accurate!
|
||||||
|
# error: [unresolved-reference]
|
||||||
|
reveal_type(Y) # revealed: Unknown
|
||||||
|
```
|
||||||
|
|
||||||
### Global-scope names starting with underscores
|
### Global-scope names starting with underscores
|
||||||
|
|
||||||
Global-scope names starting with underscores are not imported from a `*` import (unless the module
|
Global-scope names starting with underscores are not imported from a `*` import (unless the module
|
||||||
|
|
@ -263,11 +458,14 @@ if sys.version_info >= (3, 11):
|
||||||
X: bool = True
|
X: bool = True
|
||||||
else:
|
else:
|
||||||
Y: bool = False
|
Y: bool = False
|
||||||
|
Z: int = 42
|
||||||
```
|
```
|
||||||
|
|
||||||
`b.py`:
|
`b.py`:
|
||||||
|
|
||||||
```py
|
```py
|
||||||
|
Z: bool = True
|
||||||
|
|
||||||
from a import *
|
from a import *
|
||||||
|
|
||||||
# TODO should not error, should reveal `bool`
|
# TODO should not error, should reveal `bool`
|
||||||
|
|
@ -276,6 +474,12 @@ reveal_type(X) # revealed: Unknown
|
||||||
|
|
||||||
# error: [unresolved-reference]
|
# error: [unresolved-reference]
|
||||||
reveal_type(Y) # revealed: Unknown
|
reveal_type(Y) # revealed: Unknown
|
||||||
|
|
||||||
|
# The `*` import should not be considered a redefinition
|
||||||
|
# of the global variable in this module, as the symbol in
|
||||||
|
# the `a` module is in a branch that is statically known
|
||||||
|
# to be dead code given the `python-version` configuration.
|
||||||
|
reveal_type(Z) # revealed: Literal[True]
|
||||||
```
|
```
|
||||||
|
|
||||||
### Relative `*` imports
|
### Relative `*` imports
|
||||||
|
|
@ -662,6 +866,40 @@ reveal_type(X) # revealed: Unknown
|
||||||
reveal_type(Y) # revealed: Unknown
|
reveal_type(Y) # revealed: Unknown
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## `global` statements in non-global scopes
|
||||||
|
|
||||||
|
A `global` statement in a nested function scope, combined with a definition in the same function
|
||||||
|
scope of the name that was declared `global`, can add a symbol to the global namespace.
|
||||||
|
|
||||||
|
`a.py`:
|
||||||
|
|
||||||
|
```py
|
||||||
|
def f():
|
||||||
|
global g, h
|
||||||
|
|
||||||
|
g: bool = True
|
||||||
|
|
||||||
|
f()
|
||||||
|
```
|
||||||
|
|
||||||
|
`b.py`:
|
||||||
|
|
||||||
|
```py
|
||||||
|
from a import *
|
||||||
|
|
||||||
|
# TODO: false positive, should be `Literal[f]` with no diagnostic
|
||||||
|
# error: [unresolved-reference]
|
||||||
|
reveal_type(f) # revealed: Unknown
|
||||||
|
|
||||||
|
# TODO: false positive, should be `bool` with no diagnostic
|
||||||
|
# error: [unresolved-reference]
|
||||||
|
reveal_type(g) # revealed: Unknown
|
||||||
|
|
||||||
|
# this diagnostic is accurate, though!
|
||||||
|
# error: [unresolved-reference]
|
||||||
|
reveal_type(h) # revealed: Unknown
|
||||||
|
```
|
||||||
|
|
||||||
## Integration test: `collections.abc`
|
## Integration test: `collections.abc`
|
||||||
|
|
||||||
The `collections.abc` standard-library module provides a good integration test, as all its symbols
|
The `collections.abc` standard-library module provides a good integration test, as all its symbols
|
||||||
|
|
@ -711,5 +949,46 @@ def f():
|
||||||
reveal_type(X) # revealed: Unknown
|
reveal_type(X) # revealed: Unknown
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### `*` combined with other aliases in the list
|
||||||
|
|
||||||
|
`a.py`:
|
||||||
|
|
||||||
|
```py
|
||||||
|
X: bool = True
|
||||||
|
_Y: bool = False
|
||||||
|
_Z: bool = True
|
||||||
|
```
|
||||||
|
|
||||||
|
`b.py`:
|
||||||
|
|
||||||
|
<!-- blacken-docs:off -->
|
||||||
|
|
||||||
|
```py
|
||||||
|
from a import *, _Y # error: [invalid-syntax]
|
||||||
|
|
||||||
|
# The import statement above is invalid syntax,
|
||||||
|
# but it's pretty obvious that the user wanted to do a `*` import,
|
||||||
|
# so we should import all public names from `a` anyway, to minimize cascading errors
|
||||||
|
#
|
||||||
|
# TODO: get rid of this error, reveal `bool`
|
||||||
|
# error: [unresolved-reference]
|
||||||
|
reveal_type(X) # revealed: Unknown
|
||||||
|
reveal_type(_Y) # revealed: bool
|
||||||
|
```
|
||||||
|
|
||||||
|
These tests are more to assert that we don't panic on these various kinds of invalid syntax than
|
||||||
|
anything else:
|
||||||
|
|
||||||
|
`c.py`:
|
||||||
|
|
||||||
|
```py
|
||||||
|
from a import *, _Y # error: [invalid-syntax]
|
||||||
|
from a import _Y, *, _Z # error: [invalid-syntax]
|
||||||
|
from a import *, _Y as fooo # error: [invalid-syntax]
|
||||||
|
from a import *, *, _Y # error: [invalid-syntax]
|
||||||
|
```
|
||||||
|
|
||||||
|
<!-- blacken-docs:on -->
|
||||||
|
|
||||||
[python language reference for import statements]: https://docs.python.org/3/reference/simple_stmts.html#the-import-statement
|
[python language reference for import statements]: https://docs.python.org/3/reference/simple_stmts.html#the-import-statement
|
||||||
[typing spec]: https://typing.python.org/en/latest/spec/distributing.html#library-interface-public-and-private-symbols
|
[typing spec]: https://typing.python.org/en/latest/spec/distributing.html#library-interface-public-and-private-symbols
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue