Skip to content

Commit 7cf8eab

Browse files
committed
add more edge cases
1 parent 760030e commit 7cf8eab

File tree

1 file changed

+293
-10
lines changed
  • crates/red_knot_python_semantic/resources/mdtest/import

1 file changed

+293
-10
lines changed

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

Lines changed: 293 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# Wildcard (`*`) imports
22

3+
See the [Python language reference for import statements].
4+
35
## Basic functionality
46

57
### A simple `*` import
@@ -130,6 +132,130 @@ from c import X # error: [unresolved-import]
130132
from c import Y # error: [unresolved-import]
131133
```
132134

135+
### Global-scope symbols defined using walrus expressions
136+
137+
`a.py`:
138+
139+
```py
140+
X = (Y := 3) + 4
141+
```
142+
143+
`b.py`:
144+
145+
```py
146+
# TODO: should not error
147+
from a import * # error: [unresolved-import]
148+
149+
# TODO should not error, should reveal `Literal[7] | Unknown`
150+
# error: [unresolved-reference]
151+
reveal_type(X) # revealed: Unknown
152+
# TODO should not error, should reveal `Literal[3] | Unknown`
153+
# error: [unresolved-reference]
154+
reveal_type(Y) # revealed: Unknown
155+
```
156+
157+
### Global-scope names starting with underscores
158+
159+
Global-scope names starting with underscores are not imported from a `*` import (unless the module
160+
has `__all__` and they are included in `__all__`):
161+
162+
`a.py`:
163+
164+
```py
165+
_private: bool = False
166+
__protected: bool = False
167+
__dunder__: bool = False
168+
___thunder___: bool = False
169+
170+
Y: bool = True
171+
```
172+
173+
`b.py`:
174+
175+
```py
176+
# TODO: should not error
177+
from a import * # error: [unresolved-import]
178+
179+
# These errors are correct:
180+
#
181+
# error: [unresolved-reference]
182+
reveal_type(_private) # revealed: Unknown
183+
# error: [unresolved-reference]
184+
reveal_type(__protected) # revealed: Unknown
185+
# error: [unresolved-reference]
186+
reveal_type(__dunder__) # revealed: Unknown
187+
# error: [unresolved-reference]
188+
reveal_type(___thunder___) # revealed: Unknown
189+
190+
# TODO: this error is incorrect (should reveal `bool`):
191+
#
192+
# error: [unresolved-reference]
193+
reveal_type(Y) # revealed: Unknown
194+
```
195+
196+
### All public symbols are considered re-exported from `.py` files
197+
198+
For `.py` files, we should consider all public symbols in the global namespace exported by that
199+
module when considering which symbols are made available by a `*` import. Here, `b.py` does not use
200+
the explicit `from a import X as X` syntax to explicitly mark it as publicly re-exported, and `X` is
201+
not included in `__all__`; whether it should be considered a "public name" in module `b` is
202+
ambiguous. We could consider an opt-in rule to warn the user when they use `X` in `c.py` that it was
203+
not included in `__all__` and was not marked as an explicit re-export.
204+
205+
`a.py`:
206+
207+
```py
208+
X: bool = True
209+
```
210+
211+
`b.py`:
212+
213+
```py
214+
from a import X
215+
```
216+
217+
`c.py`:
218+
219+
```py
220+
# TODO: should not error
221+
from b import * # error: [unresolved-import]
222+
223+
# TODO: this is a false positive, but we could consider a different opt-in diagnostic
224+
# (see prose commentary above)
225+
#
226+
# error: [unresolved-reference]
227+
reveal_type(X) # revealed: Unknown
228+
```
229+
230+
### Only explicit re-exports are considered re-exported from `.pyi` files
231+
232+
For `.pyi` files, we should consider all imports private to the stub unless they are included in
233+
`__all__` or use the explict `from foo import X as X` syntax.
234+
235+
`a.pyi`:
236+
237+
```pyi
238+
X: bool = True
239+
```
240+
241+
`b.pyi`:
242+
243+
```pyi
244+
from a import X
245+
```
246+
247+
`c.py`:
248+
249+
```py
250+
# TODO: should not error
251+
from b import * # error: [unresolved-import]
252+
253+
# This error is correct, as `X` is not considered re-exported from module `b`:
254+
#
255+
# error: [unresolved-reference]
256+
reveal_type(X) # revealed: Unknown
257+
```
258+
133259
### Symbols in statically known branches
134260

135261
```toml
@@ -190,17 +316,22 @@ reveal_type(X) # revealed: Unknown
190316

191317
## Star imports with `__all__`
192318

193-
If a module `x` contains `__all__`, only symbols included in `x.__all__` are imported by
194-
`from x import *`.
319+
If a module `x` contains `__all__`, all symbols included in `x.__all__` are imported by
320+
`from x import *` (but no other symbols are).
195321

196322
### Simple tuple `__all__`
197323

198324
`a.py`:
199325

200326
```py
201-
__all__ = ("X",)
327+
__all__ = ("X", "_private", "__protected", "__dunder__", "___thunder___")
202328

203329
X: bool = True
330+
_private: bool = True
331+
__protected: bool = True
332+
__dunder__: bool = True
333+
___thunder___: bool = True
334+
204335
Y: bool = False
205336
```
206337

@@ -210,10 +341,20 @@ Y: bool = False
210341
# TODO should not error
211342
from a import * # error: [unresolved-import]
212343

213-
# TODO should not error, should reveal `bool`
344+
# TODO none of these should error, should all reveal `bool`
214345
# error: [unresolved-reference]
215346
reveal_type(X) # revealed: Unknown
347+
# error: [unresolved-reference]
348+
reveal_type(_private) # revealed: Unknown
349+
# error: [unresolved-reference]
350+
reveal_type(__protected) # revealed: Unknown
351+
# error: [unresolved-reference]
352+
reveal_type(__dunder__) # revealed: Unknown
353+
# error: [unresolved-reference]
354+
reveal_type(___thunder___) # revealed: Unknown
216355

356+
# but this diagnostic is accurate!
357+
#
217358
# error: [unresolved-reference]
218359
reveal_type(Y) # revealed: Unknown
219360
```
@@ -361,16 +502,22 @@ from a import * # fails with `AttributeError: module 'foo' has no attribute 'b'
361502

362503
### Dynamic `__all__`
363504

364-
We'll need to decide what to do if `__all__` contains members that are dynamically computed. Mypy
365-
simply ignores any members that are not statically known when determining which symbols are
366-
available (which can lead to false positives).
505+
If `__all__` contains members that are dynamically computed, we should check that all members of
506+
`__all__` are assignable to `str`. For the purposes of evaluating `*` imports, however, we should
507+
treat the module as though it has no `__all__` at all: all global-scope members of the module should
508+
be considered imported by the import statement. We should probably also emit a warning telling the
509+
user that we cannot statically determine the elements of `__all__`.
367510

368511
`a.py`:
369512

370513
```py
371514
def f() -> str:
372515
return "f"
373516

517+
def g() -> int:
518+
return 42
519+
520+
# TODO we should emit a warning here for the dynamically constructed `__all__` member.
374521
__all__ = [f()]
375522
```
376523

@@ -380,14 +527,57 @@ __all__ = [f()]
380527
# TODO: should not error
381528
from a import * # error: [unresolved-import]
382529

383-
# Strictly speaking this is a false positive, since there *is* an `f` symbol imported
384-
# by the `*` import at runtime.
530+
# TODO: we should avoid both errors here.
531+
#
532+
# At runtime, `f` is imported but `g` is not; to avoid false positives, however,
533+
# we should treat `a` as though it does not have `__all__` at all,
534+
# which would imply that both symbols would be present.
385535
#
386536
# error: [unresolved-reference]
387537
reveal_type(f) # revealed: Unknown
538+
# error: [unresolved-reference]
539+
reveal_type(g) # revealed: Unknown
388540
```
389541

390-
### `__all__` combined with statically known branches
542+
### `__all__` conditionally defined in a statically known branch
543+
544+
```toml
545+
[environment]
546+
python-version = "3.11"
547+
```
548+
549+
`a.py`:
550+
551+
```py
552+
import sys
553+
554+
X: bool = True
555+
556+
if sys.version_info >= (3, 11):
557+
__all__ = ["X", "Y"]
558+
Y: bool = True
559+
else:
560+
__all__ = ("Z",)
561+
Z: bool = True
562+
```
563+
564+
`b.py`:
565+
566+
```py
567+
# TODO should not error
568+
from a import * # error: [unresolved-import]
569+
570+
# TODO neither should error, both should be `bool`
571+
# error: [unresolved-reference]
572+
reveal_type(X) # revealed: Unknown
573+
# error: [unresolved-reference]
574+
reveal_type(Y) # revealed: Unknown
575+
576+
# error: [unresolved-reference]
577+
reveal_type(Z) # revealed: Unknown
578+
```
579+
580+
### `__all__` conditionally mutated in a statically known branch
391581

392582
```toml
393583
[environment]
@@ -426,6 +616,75 @@ reveal_type(Y) # revealed: Unknown
426616
reveal_type(Z) # revealed: Unknown
427617
```
428618

619+
### Empty `__all__`
620+
621+
An empty `__all__` is valid, but a `*` import from a module with an empty `__all__` results in 0
622+
bindings being added from the import:
623+
624+
`a.py`:
625+
626+
```py
627+
X: bool = True
628+
629+
__all__ = ()
630+
```
631+
632+
`b.py`:
633+
634+
```py
635+
Y: bool = True
636+
637+
__all__ = []
638+
```
639+
640+
`c.py`:
641+
642+
```py
643+
# TODO: should not error for either import statement:
644+
from a import * # error: [unresolved-import]
645+
from b import * # error: [unresolved-import]
646+
647+
# error: [unresolved-reference]
648+
reveal_type(X) # revealed: Unknown
649+
# error: [unresolved-reference]
650+
reveal_type(Y) # revealed: Unknown
651+
```
652+
653+
### `__all__` in a stub file
654+
655+
If a name is included in `__all__` in a stub file, it is considered re-exported even if it was only
656+
defined using an import without the explicit `from foo import X as X` syntax:
657+
658+
`a.py`:
659+
660+
```py
661+
X: bool = True
662+
Y: bool = True
663+
```
664+
665+
`b.py`:
666+
667+
```py
668+
from a import X, Y
669+
670+
__all__ = ["X"]
671+
```
672+
673+
`c.py`:
674+
675+
```py
676+
# TODO: should not error
677+
from b import * # error: [unresolved-import]
678+
679+
# TODO: should not error, should reveal `bool`
680+
# error: [unresolved-reference]
681+
reveal_type(X) # revealed: Unknown
682+
683+
# this error is correct:
684+
# error: [unresolved-reference]
685+
reveal_type(Y) # revealed: Unknown
686+
```
687+
429688
## Integration test: `collections.abc`
430689

431690
The `collections.abc` standard-library module provides a good integration test, as all its symbols
@@ -452,4 +711,28 @@ If the module is unresolved, we emit a diagnostic just like for any other unreso
452711
from foo import * # error: [unresolved-import]
453712
```
454713

714+
### Nested scope
715+
716+
A `*` import in a nested scope are always a syntax error. Red-knot does not infer any bindings from
717+
them:
718+
719+
`a.py`:
720+
721+
```py
722+
X: bool = True
723+
```
724+
725+
`b.py`:
726+
727+
```py
728+
def f():
729+
# TODO: it's correct for us to raise an error here, but the error code and error message are incorrect.
730+
# It should be a syntax errror (tracked by https://github.com/astral-sh/ruff/issues/11934)
731+
from a import * # error: [unresolved-import] "Module `a` has no member `*`"
732+
733+
# error: [unresolved-reference]
734+
reveal_type(X) # revealed: Unknown
735+
```
736+
737+
[python language reference for import statements]: https://docs.python.org/3/reference/simple_stmts.html#the-import-statement
455738
[typing spec]: https://typing.python.org/en/latest/spec/distributing.html#library-interface-public-and-private-symbols

0 commit comments

Comments
 (0)