Skip to content

Commit 66d0cf2

Browse files
authored
[red-knot] Add more tests for * imports (#16955)
## Summary This PR separates out the entirely new tests from #16923 into a standalone PR. I'll rebase #16923 on top of this branch. The reasons for separating it out are: - It should make it clearer to see in <#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 <#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 <#16923> for whatever reason, it'll be nice to keep the tests ## Test Plan `cargo test -p red_knot_python_semantic`
1 parent 85b7f80 commit 66d0cf2

File tree

1 file changed

+279
-0
lines changed
  • crates/red_knot_python_semantic/resources/mdtest/import

1 file changed

+279
-0
lines changed

crates/red_knot_python_semantic/resources/mdtest/import/star.md

Lines changed: 279 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,201 @@ reveal_type(X) # revealed: Unknown
148148
reveal_type(Y) # revealed: Unknown
149149
```
150150

151+
### Global-scope symbols defined in many other ways
152+
153+
`a.py`:
154+
155+
```py
156+
import typing
157+
from collections import OrderedDict
158+
from collections import OrderedDict as Foo
159+
160+
A, B = 1, (C := 2)
161+
D: (E := 4) = (F := 5) # error: [invalid-type-form]
162+
163+
for G in [1]:
164+
...
165+
166+
for (H := 4).whatever in [2]: # error: [unresolved-attribute]
167+
...
168+
169+
class I: ...
170+
171+
def J(): ...
172+
173+
type K = int
174+
175+
with () as L: # error: [invalid-context-manager]
176+
...
177+
178+
match 42:
179+
case {"something": M}:
180+
...
181+
case [*N]:
182+
...
183+
case [O]:
184+
...
185+
case P | Q:
186+
...
187+
case object(foo=R):
188+
...
189+
case object(S):
190+
...
191+
case T:
192+
...
193+
```
194+
195+
`b.py`:
196+
197+
```py
198+
from a import *
199+
200+
# fmt: off
201+
202+
print((
203+
# TODO: false positive
204+
A, # error: [unresolved-reference]
205+
# TODO: false positive
206+
B, # error: [unresolved-reference]
207+
# TODO: false positive
208+
C, # error: [unresolved-reference]
209+
# TODO: false positive
210+
D, # error: [unresolved-reference]
211+
# TODO: false positive
212+
E, # error: [unresolved-reference]
213+
# TODO: false positive
214+
F, # error: [unresolved-reference]
215+
# TODO: could emit diagnostic about being possibly unbound, but this is a false positive
216+
G, # error: [unresolved-reference] "Name `G` used when not defined"
217+
# TODO: false positive
218+
H, # error: [unresolved-reference]
219+
# TODO: false positive
220+
I, # error: [unresolved-reference]
221+
# TODO: false positive
222+
J, # error: [unresolved-reference]
223+
# TODO: false positive
224+
K, # error: [unresolved-reference]
225+
# TODO: false positive
226+
L, # error: [unresolved-reference]
227+
# TODO: could emit diagnostic about being possibly unbound, but this is a false positive
228+
M, # error: [unresolved-reference] "Name `M` used when not defined"
229+
# TODO: could emit diagnostic about being possibly unbound, but this is a false positive
230+
N, # error: [unresolved-reference] "Name `N` used when not defined"
231+
# TODO: could emit diagnostic about being possibly unbound, but this is a false positive
232+
O, # error: [unresolved-reference] "Name `O` used when not defined"
233+
# TODO: could emit diagnostic about being possibly unbound, but this is a false positive
234+
P, # error: [unresolved-reference] "Name `P` used when not defined"
235+
# TODO: could emit diagnostic about being possibly unbound, but this is a false positive
236+
Q, # error: [unresolved-reference] "Name `Q` used when not defined"
237+
# TODO: could emit diagnostic about being possibly unbound, but this is a false positive
238+
R, # error: [unresolved-reference] "Name `R` used when not defined"
239+
# TODO: could emit diagnostic about being possibly unbound, but this is a false positive
240+
S, # error: [unresolved-reference] "Name `S` used when not defined"
241+
# TODO: could emit diagnostic about being possibly unbound, but this is a false positive
242+
T, # error: [unresolved-reference] "Name `T` used when not defined"
243+
# TODO: false positive
244+
typing, # error: [unresolved-reference]
245+
# TODO: false positive
246+
OrderedDict, # error: [unresolved-reference]
247+
# TODO: false positive
248+
Foo, # error: [unresolved-reference]
249+
))
250+
```
251+
252+
### Definitions in function-like scopes are not global definitions
253+
254+
Except for some cases involving walrus expressions inside comprehension scopes.
255+
256+
`a.py`:
257+
258+
```py
259+
class Iterator:
260+
def __next__(self) -> int:
261+
return 42
262+
263+
class Iterable:
264+
def __iter__(self) -> Iterator:
265+
return Iterator()
266+
267+
[a for a in Iterable()]
268+
{b for b in Iterable()}
269+
{c: c for c in Iterable()}
270+
(d for d in Iterable())
271+
lambda e: (f := 42)
272+
273+
# Definitions created by walruses in a comprehension scope are unique;
274+
# they "leak out" of the scope and are stored in the surrounding scope
275+
[(g := h * 2) for h in Iterable()]
276+
[i for j in Iterable() if (i := j - 10) > 0]
277+
{(k := l * 2): (m := l * 3) for l in Iterable()}
278+
```
279+
280+
`b.py`:
281+
282+
```py
283+
from a import *
284+
285+
# error: [unresolved-reference]
286+
reveal_type(a) # revealed: Unknown
287+
# error: [unresolved-reference]
288+
reveal_type(b) # revealed: Unknown
289+
# error: [unresolved-reference]
290+
reveal_type(c) # revealed: Unknown
291+
# error: [unresolved-reference]
292+
reveal_type(d) # revealed: Unknown
293+
# error: [unresolved-reference]
294+
reveal_type(e) # revealed: Unknown
295+
# error: [unresolved-reference]
296+
reveal_type(f) # revealed: Unknown
297+
# error: [unresolved-reference]
298+
reveal_type(h) # revealed: Unknown
299+
# error: [unresolved-reference]
300+
reveal_type(j) # revealed: Unknown
301+
302+
# TODO: these should all reveal `Unknown | int` and should not have diagnostics.
303+
# (We don't generally model elsewhere in red-knot that bindings from walruses
304+
# "leak" from comprehension scopes into outer scopes, but we should.)
305+
# See https://github.com/astral-sh/ruff/issues/16954
306+
#
307+
# error: [unresolved-reference]
308+
reveal_type(g) # revealed: Unknown
309+
# error: [unresolved-reference]
310+
reveal_type(i) # revealed: Unknown
311+
# error: [unresolved-reference]
312+
reveal_type(k) # revealed: Unknown
313+
# error: [unresolved-reference]
314+
reveal_type(m) # revealed: Unknown
315+
```
316+
317+
### An annotation without a value is a definition in a stub but not a `.py` file
318+
319+
`a.pyi`:
320+
321+
```pyi
322+
X: bool
323+
```
324+
325+
`b.py`:
326+
327+
```py
328+
Y: bool
329+
```
330+
331+
`c.py`:
332+
333+
```py
334+
from a import *
335+
from b import *
336+
337+
# TODO: this is a false positive, should reveal `bool`
338+
# error: [unresolved-reference]
339+
reveal_type(X) # revealed: Unknown
340+
341+
# but this diagnostic is accurate!
342+
# error: [unresolved-reference]
343+
reveal_type(Y) # revealed: Unknown
344+
```
345+
151346
### Global-scope names starting with underscores
152347

153348
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):
263458
X: bool = True
264459
else:
265460
Y: bool = False
461+
Z: int = 42
266462
```
267463

268464
`b.py`:
269465

270466
```py
467+
Z: bool = True
468+
271469
from a import *
272470

273471
# TODO should not error, should reveal `bool`
@@ -276,6 +474,12 @@ reveal_type(X) # revealed: Unknown
276474

277475
# error: [unresolved-reference]
278476
reveal_type(Y) # revealed: Unknown
477+
478+
# The `*` import should not be considered a redefinition
479+
# of the global variable in this module, as the symbol in
480+
# the `a` module is in a branch that is statically known
481+
# to be dead code given the `python-version` configuration.
482+
reveal_type(Z) # revealed: Literal[True]
279483
```
280484

281485
### Relative `*` imports
@@ -662,6 +866,40 @@ reveal_type(X) # revealed: Unknown
662866
reveal_type(Y) # revealed: Unknown
663867
```
664868

869+
## `global` statements in non-global scopes
870+
871+
A `global` statement in a nested function scope, combined with a definition in the same function
872+
scope of the name that was declared `global`, can add a symbol to the global namespace.
873+
874+
`a.py`:
875+
876+
```py
877+
def f():
878+
global g, h
879+
880+
g: bool = True
881+
882+
f()
883+
```
884+
885+
`b.py`:
886+
887+
```py
888+
from a import *
889+
890+
# TODO: false positive, should be `Literal[f]` with no diagnostic
891+
# error: [unresolved-reference]
892+
reveal_type(f) # revealed: Unknown
893+
894+
# TODO: false positive, should be `bool` with no diagnostic
895+
# error: [unresolved-reference]
896+
reveal_type(g) # revealed: Unknown
897+
898+
# this diagnostic is accurate, though!
899+
# error: [unresolved-reference]
900+
reveal_type(h) # revealed: Unknown
901+
```
902+
665903
## Integration test: `collections.abc`
666904

667905
The `collections.abc` standard-library module provides a good integration test, as all its symbols
@@ -711,5 +949,46 @@ def f():
711949
reveal_type(X) # revealed: Unknown
712950
```
713951

952+
### `*` combined with other aliases in the list
953+
954+
`a.py`:
955+
956+
```py
957+
X: bool = True
958+
_Y: bool = False
959+
_Z: bool = True
960+
```
961+
962+
`b.py`:
963+
964+
<!-- blacken-docs:off -->
965+
966+
```py
967+
from a import *, _Y # error: [invalid-syntax]
968+
969+
# The import statement above is invalid syntax,
970+
# but it's pretty obvious that the user wanted to do a `*` import,
971+
# so we should import all public names from `a` anyway, to minimize cascading errors
972+
#
973+
# TODO: get rid of this error, reveal `bool`
974+
# error: [unresolved-reference]
975+
reveal_type(X) # revealed: Unknown
976+
reveal_type(_Y) # revealed: bool
977+
```
978+
979+
These tests are more to assert that we don't panic on these various kinds of invalid syntax than
980+
anything else:
981+
982+
`c.py`:
983+
984+
```py
985+
from a import *, _Y # error: [invalid-syntax]
986+
from a import _Y, *, _Z # error: [invalid-syntax]
987+
from a import *, _Y as fooo # error: [invalid-syntax]
988+
from a import *, *, _Y # error: [invalid-syntax]
989+
```
990+
991+
<!-- blacken-docs:on -->
992+
714993
[python language reference for import statements]: https://docs.python.org/3/reference/simple_stmts.html#the-import-statement
715994
[typing spec]: https://typing.python.org/en/latest/spec/distributing.html#library-interface-public-and-private-symbols

0 commit comments

Comments
 (0)