mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-04 10:48:32 +00:00
[ty] Add support for __all__
(#17856)
## Summary This PR adds support for the `__all__` module variable. Reference spec: https://typing.python.org/en/latest/spec/distributing.html#library-interface-public-and-private-symbols This PR adds a new `dunder_all_names` query that returns a set of `Name`s defined in the `__all__` variable of the given `File`. The query works by implementing the `StatementVisitor` and collects all the names by recognizing the supported idioms as mentioned in the spec. Any idiom that's not recognized are ignored. The current implementation is minimum to what's required for us to remove all the false positives that this is causing. Refer to the "Follow-ups" section below to see what we can do next. I'll a open separate issue to keep track of them. Closes: astral-sh/ty#106 Closes: astral-sh/ty#199 ### Follow-ups * Diagnostics: * Add warning diagnostics for unrecognized `__all__` idioms, `__all__` containing non-string element * Add an error diagnostic for elements that are present in `__all__` but not defined in the module. This could lead to runtime error * Maybe we should return `<type>` instead of `Unknown | <type>` for `module.__all__`. For example: https://playknot.ruff.rs/2a6fe5d7-4e16-45b1-8ec3-d79f2d4ca894 * Mark a symbol that's mentioned in `__all__` as used otherwise it could raise (possibly in the future) "unused-name" diagnostic Supporting diagnostics will require that we update the return type of the query to be something other than `Option<FxHashSet<Name>>`, something that behaves like a result and provides a way to check whether a name exists in `__all__`, loop over elements in `__all__`, loop over the invalid elements, etc. ## Ecosystem analysis The following are the maximum amount of diagnostics **removed** in the ecosystem: * "Type <module '...'> has no attribute ..." * `collections.abc` - 14 * `numpy` - 35534 * `numpy.ma` - 296 * `numpy.char` - 37 * `numpy.testing` - 175 * `hashlib` - 311 * `scipy.fft` - 2 * `scipy.stats` - 38 * "Module '...' has no member ..." * `collections.abc` - 85 * `numpy` - 508 * `numpy.testing` - 741 * `hashlib` - 36 * `scipy.stats` - 68 * `scipy.interpolate` - 7 * `scipy.signal` - 5 The following modules have dynamic `__all__` definition, so `ty` assumes that `__all__` doesn't exists in that module: * `scipy.stats` (95a5d6ea8b/scipy/stats/__init__.py (L665)
) * `scipy.interpolate` (95a5d6ea8b/scipy/interpolate/__init__.py (L221)
) * `scipy.signal` (indirectly via95a5d6ea8b/scipy/signal/_signal_api.py (L30)
) * `numpy.testing` (de784cd6ee/numpy/testing/__init__.py (L16-L18)
) ~There's this one category of **false positives** that have been added:~ Fixed the false positives by also ignoring `__all__` from a module that uses unrecognized idioms. <details><summary>Details about the false postivie:</summary> <p> The `scipy.stats` module has dynamic `__all__` and it imports a bunch of symbols via star imports. Some of those modules have a mix of valid and invalid `__all__` idioms. For example, in95a5d6ea8b/scipy/stats/distributions.py (L18-L24)
, 2 out of 4 `__all__` idioms are invalid but currently `ty` recognizes two of them and says that the module has a `__all__` with 5 values. This leads to around **2055** newly added false positives of the form: ``` Type <module 'scipy.stats'> has no attribute ... ``` I think the fix here is to completely ignore `__all__`, not only if there are invalid elements in it, but also if there are unrecognized idioms used in the module. </p> </details> ## Test Plan Add a bunch of test cases using the new `ty_extensions.dunder_all_names` function to extract a module's `__all__` names. Update various test cases to remove false positives around `*` imports and re-export convention. Add new test cases for named import behavior as `*` imports covers all of it already (thanks Alex!).
This commit is contained in:
parent
c6f4929cdc
commit
78054824c0
11 changed files with 1456 additions and 69 deletions
|
@ -102,8 +102,8 @@ reveal_type(SetSubclass.__mro__)
|
|||
|
||||
class FrozenSetSubclass(typing.FrozenSet): ...
|
||||
|
||||
# TODO: should have `Generic`, should not have `Unknown`
|
||||
# revealed: tuple[<class 'FrozenSetSubclass'>, <class 'frozenset'>, Unknown, <class 'object'>]
|
||||
# TODO: generic protocols
|
||||
# revealed: tuple[<class 'FrozenSetSubclass'>, <class 'frozenset'>, <class 'AbstractSet'>, <class 'Collection'>, <class 'Iterable'>, <class 'Container'>, @Todo(`Protocol[]` subscript), typing.Generic, <class 'object'>]
|
||||
reveal_type(FrozenSetSubclass.__mro__)
|
||||
|
||||
####################
|
||||
|
|
|
@ -202,9 +202,9 @@ class AnyFoo: ...
|
|||
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
|
||||
|
||||
reveal_type(Foo) # revealed: <class 'Foo'>
|
||||
```
|
||||
|
||||
`a.pyi`:
|
||||
|
@ -221,6 +221,44 @@ __all__ = ['Foo']
|
|||
class Foo: ...
|
||||
```
|
||||
|
||||
## Re-exports with `__all__`
|
||||
|
||||
If a symbol is re-exported via redundant alias but is not included in `__all__`, it shouldn't raise
|
||||
an error when using named import.
|
||||
|
||||
`named_import.py`:
|
||||
|
||||
```py
|
||||
from a import Foo
|
||||
|
||||
reveal_type(Foo) # revealed: <class 'Foo'>
|
||||
```
|
||||
|
||||
`a.pyi`:
|
||||
|
||||
```pyi
|
||||
from b import Foo as Foo
|
||||
|
||||
__all__ = []
|
||||
```
|
||||
|
||||
`b.pyi`:
|
||||
|
||||
```pyi
|
||||
class Foo: ...
|
||||
```
|
||||
|
||||
However, a star import _would_ raise an error.
|
||||
|
||||
`star_import.py`:
|
||||
|
||||
```py
|
||||
from a import *
|
||||
|
||||
# error: [unresolved-reference] "Name `Foo` used when not defined"
|
||||
reveal_type(Foo) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Re-exports in `__init__.pyi`
|
||||
|
||||
Similarly, for an `__init__.pyi` (stub) file, importing a non-exported name should raise an error
|
||||
|
|
796
crates/ty_python_semantic/resources/mdtest/import/dunder_all.md
Normal file
796
crates/ty_python_semantic/resources/mdtest/import/dunder_all.md
Normal file
|
@ -0,0 +1,796 @@
|
|||
# `__all__`
|
||||
|
||||
Reference:
|
||||
<https://typing.python.org/en/latest/spec/distributing.html#library-interface-public-and-private-symbols>
|
||||
|
||||
NOTE: This file only includes the usage of `__all__` for named-imports i.e.,
|
||||
`from module import symbol`. For the usage of `__all__` in wildcard imports, refer to
|
||||
[star.md](star.md).
|
||||
|
||||
## Undefined
|
||||
|
||||
`exporter.py`:
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
class B: ...
|
||||
```
|
||||
|
||||
`importer.py`:
|
||||
|
||||
```py
|
||||
import exporter
|
||||
from ty_extensions import dunder_all_names
|
||||
|
||||
# revealed: None
|
||||
reveal_type(dunder_all_names(exporter))
|
||||
```
|
||||
|
||||
## Global scope
|
||||
|
||||
The `__all__` variable is only recognized from the global scope of the module. It is not recognized
|
||||
from the local scope of a function or class.
|
||||
|
||||
`exporter.py`:
|
||||
|
||||
```py
|
||||
__all__ = ["A"]
|
||||
|
||||
def foo():
|
||||
__all__.append("B")
|
||||
|
||||
class Foo:
|
||||
__all__ += ["C"]
|
||||
|
||||
class A: ...
|
||||
class B: ...
|
||||
class C: ...
|
||||
|
||||
foo()
|
||||
```
|
||||
|
||||
`importer.py`:
|
||||
|
||||
```py
|
||||
import exporter
|
||||
from ty_extensions import dunder_all_names
|
||||
|
||||
# revealed: tuple[Literal["A"]]
|
||||
reveal_type(dunder_all_names(exporter))
|
||||
```
|
||||
|
||||
## Supported idioms
|
||||
|
||||
According to the [specification], the following idioms are supported:
|
||||
|
||||
### List assignment
|
||||
|
||||
`exporter.py`:
|
||||
|
||||
```py
|
||||
__all__ = ["A", "B"]
|
||||
|
||||
class A: ...
|
||||
class B: ...
|
||||
```
|
||||
|
||||
`exporter_annotated.py`:
|
||||
|
||||
```py
|
||||
__all__: list[str] = ["C", "D"]
|
||||
|
||||
class C: ...
|
||||
class D: ...
|
||||
```
|
||||
|
||||
`importer.py`:
|
||||
|
||||
```py
|
||||
import exporter
|
||||
import exporter_annotated
|
||||
from ty_extensions import dunder_all_names
|
||||
|
||||
# revealed: tuple[Literal["A"], Literal["B"]]
|
||||
reveal_type(dunder_all_names(exporter))
|
||||
|
||||
# revealed: tuple[Literal["C"], Literal["D"]]
|
||||
reveal_type(dunder_all_names(exporter_annotated))
|
||||
```
|
||||
|
||||
### List assignment (shadowed)
|
||||
|
||||
`exporter.py`:
|
||||
|
||||
```py
|
||||
__all__ = ["A", "B"]
|
||||
|
||||
class A: ...
|
||||
class B: ...
|
||||
|
||||
__all__ = ["C", "D"]
|
||||
|
||||
class C: ...
|
||||
class D: ...
|
||||
```
|
||||
|
||||
`exporter_annotated.py`:
|
||||
|
||||
```py
|
||||
__all__ = ["X"]
|
||||
|
||||
class X: ...
|
||||
|
||||
__all__: list[str] = ["Y", "Z"]
|
||||
|
||||
class Y: ...
|
||||
class Z: ...
|
||||
```
|
||||
|
||||
`importer.py`:
|
||||
|
||||
```py
|
||||
import exporter
|
||||
import exporter_annotated
|
||||
from ty_extensions import dunder_all_names
|
||||
|
||||
# revealed: tuple[Literal["C"], Literal["D"]]
|
||||
reveal_type(dunder_all_names(exporter))
|
||||
|
||||
# revealed: tuple[Literal["Y"], Literal["Z"]]
|
||||
reveal_type(dunder_all_names(exporter_annotated))
|
||||
```
|
||||
|
||||
### Tuple assignment
|
||||
|
||||
`exporter.py`:
|
||||
|
||||
```py
|
||||
__all__ = ("A", "B")
|
||||
|
||||
class A: ...
|
||||
class B: ...
|
||||
```
|
||||
|
||||
`exporter_annotated.py`:
|
||||
|
||||
```py
|
||||
__all__: tuple[str, ...] = ("C", "D")
|
||||
|
||||
class C: ...
|
||||
class D: ...
|
||||
```
|
||||
|
||||
`importer.py`:
|
||||
|
||||
```py
|
||||
import exporter
|
||||
import exporter_annotated
|
||||
from ty_extensions import dunder_all_names
|
||||
|
||||
# revealed: tuple[Literal["A"], Literal["B"]]
|
||||
reveal_type(dunder_all_names(exporter))
|
||||
|
||||
# revealed: tuple[Literal["C"], Literal["D"]]
|
||||
reveal_type(dunder_all_names(exporter_annotated))
|
||||
```
|
||||
|
||||
### Tuple assignment (shadowed)
|
||||
|
||||
`exporter.py`:
|
||||
|
||||
```py
|
||||
__all__ = ("A", "B")
|
||||
|
||||
class A: ...
|
||||
class B: ...
|
||||
|
||||
__all__ = ("C", "D")
|
||||
|
||||
class C: ...
|
||||
class D: ...
|
||||
```
|
||||
|
||||
`exporter_annotated.py`:
|
||||
|
||||
```py
|
||||
__all__ = ("X",)
|
||||
|
||||
class X: ...
|
||||
|
||||
__all__: tuple[str, ...] = ("Y", "Z")
|
||||
|
||||
class Y: ...
|
||||
class Z: ...
|
||||
```
|
||||
|
||||
`importer.py`:
|
||||
|
||||
```py
|
||||
import exporter
|
||||
import exporter_annotated
|
||||
from ty_extensions import dunder_all_names
|
||||
|
||||
# revealed: tuple[Literal["C"], Literal["D"]]
|
||||
reveal_type(dunder_all_names(exporter))
|
||||
|
||||
# revealed: tuple[Literal["Y"], Literal["Z"]]
|
||||
reveal_type(dunder_all_names(exporter_annotated))
|
||||
```
|
||||
|
||||
### Augmenting list with a list or submodule `__all__`
|
||||
|
||||
`subexporter.py`:
|
||||
|
||||
```py
|
||||
__all__ = ["A", "B"]
|
||||
|
||||
class A: ...
|
||||
class B: ...
|
||||
```
|
||||
|
||||
`exporter.py`:
|
||||
|
||||
```py
|
||||
import subexporter
|
||||
|
||||
__all__ = []
|
||||
__all__ += ["C", "D"]
|
||||
__all__ += subexporter.__all__
|
||||
|
||||
class C: ...
|
||||
class D: ...
|
||||
```
|
||||
|
||||
`importer.py`:
|
||||
|
||||
```py
|
||||
import exporter
|
||||
from ty_extensions import dunder_all_names
|
||||
|
||||
# revealed: tuple[Literal["A"], Literal["B"], Literal["C"], Literal["D"]]
|
||||
reveal_type(dunder_all_names(exporter))
|
||||
```
|
||||
|
||||
### Extending with a list or submodule `__all__`
|
||||
|
||||
`subexporter.py`:
|
||||
|
||||
```py
|
||||
__all__ = ["A", "B"]
|
||||
|
||||
class A: ...
|
||||
class B: ...
|
||||
```
|
||||
|
||||
`exporter.py`:
|
||||
|
||||
```py
|
||||
import subexporter
|
||||
|
||||
__all__ = []
|
||||
__all__.extend(["C", "D"])
|
||||
__all__.extend(subexporter.__all__)
|
||||
|
||||
class C: ...
|
||||
class D: ...
|
||||
```
|
||||
|
||||
`importer.py`:
|
||||
|
||||
```py
|
||||
import exporter
|
||||
from ty_extensions import dunder_all_names
|
||||
|
||||
# revealed: tuple[Literal["A"], Literal["B"], Literal["C"], Literal["D"]]
|
||||
reveal_type(dunder_all_names(exporter))
|
||||
```
|
||||
|
||||
### Appending a single symbol
|
||||
|
||||
`exporter.py`:
|
||||
|
||||
```py
|
||||
__all__ = ["A"]
|
||||
__all__.append("B")
|
||||
|
||||
class A: ...
|
||||
class B: ...
|
||||
```
|
||||
|
||||
`importer.py`:
|
||||
|
||||
```py
|
||||
import exporter
|
||||
from ty_extensions import dunder_all_names
|
||||
|
||||
# revealed: tuple[Literal["A"], Literal["B"]]
|
||||
reveal_type(dunder_all_names(exporter))
|
||||
```
|
||||
|
||||
### Removing a single symbol
|
||||
|
||||
`exporter.py`:
|
||||
|
||||
```py
|
||||
__all__ = ["A", "B"]
|
||||
__all__.remove("A")
|
||||
|
||||
# Non-existant symbol in `__all__` at this point
|
||||
# TODO: This raises `ValueError` at runtime, maybe we should raise a diagnostic as well?
|
||||
__all__.remove("C")
|
||||
|
||||
class A: ...
|
||||
class B: ...
|
||||
```
|
||||
|
||||
`importer.py`:
|
||||
|
||||
```py
|
||||
import exporter
|
||||
from ty_extensions import dunder_all_names
|
||||
|
||||
# revealed: tuple[Literal["B"]]
|
||||
reveal_type(dunder_all_names(exporter))
|
||||
```
|
||||
|
||||
### Mixed
|
||||
|
||||
`subexporter.py`:
|
||||
|
||||
```py
|
||||
__all__ = []
|
||||
|
||||
__all__ = ["A"]
|
||||
__all__.append("B")
|
||||
__all__.extend(["C"])
|
||||
__all__.remove("B")
|
||||
|
||||
class A: ...
|
||||
class B: ...
|
||||
class C: ...
|
||||
```
|
||||
|
||||
`exporter.py`:
|
||||
|
||||
```py
|
||||
import subexporter
|
||||
|
||||
__all__ = []
|
||||
__all__ += ["D"]
|
||||
__all__ += subexporter.__all__
|
||||
|
||||
class D: ...
|
||||
```
|
||||
|
||||
`importer.py`:
|
||||
|
||||
```py
|
||||
import exporter
|
||||
from ty_extensions import dunder_all_names
|
||||
|
||||
# revealed: tuple[Literal["A"], Literal["C"], Literal["D"]]
|
||||
reveal_type(dunder_all_names(exporter))
|
||||
```
|
||||
|
||||
## Invalid
|
||||
|
||||
### Unsupported idioms
|
||||
|
||||
Idioms that are not mentioned in the [specification] are not recognized by `ty` and if they're used,
|
||||
`__all__` is considered to be undefined for that module. This is to avoid false positives.
|
||||
|
||||
`bar.py`:
|
||||
|
||||
```py
|
||||
__all__ = ["A", "B"]
|
||||
|
||||
class A: ...
|
||||
class B: ...
|
||||
```
|
||||
|
||||
`foo.py`:
|
||||
|
||||
```py
|
||||
import bar as bar
|
||||
```
|
||||
|
||||
`exporter.py`:
|
||||
|
||||
```py
|
||||
import foo
|
||||
from ty_extensions import dunder_all_names
|
||||
|
||||
__all__ = []
|
||||
|
||||
# revealed: tuple[Literal["A"], Literal["B"]]
|
||||
reveal_type(dunder_all_names(foo.bar))
|
||||
|
||||
# Only direct attribute access of modules are recognized
|
||||
# TODO: warning diagnostic
|
||||
__all__.extend(foo.bar.__all__)
|
||||
# TODO: warning diagnostic
|
||||
__all__ += foo.bar.__all__
|
||||
|
||||
# Augmented assignment is only allowed when the value is a list expression
|
||||
# TODO: warning diagnostic
|
||||
__all__ += ("C",)
|
||||
|
||||
# Other methods on `list` are not recognized
|
||||
# TODO: warning diagnostic
|
||||
__all__.insert(0, "C")
|
||||
# TODO: warning diagnostic
|
||||
__all__.clear()
|
||||
|
||||
__all__.append("C")
|
||||
# `pop` is not valid; use `remove` instead
|
||||
# TODO: warning diagnostic
|
||||
__all__.pop()
|
||||
|
||||
# Sets are not recognized
|
||||
# TODO: warning diagnostic
|
||||
__all__ = {"C", "D"}
|
||||
|
||||
class C: ...
|
||||
class D: ...
|
||||
```
|
||||
|
||||
`importer.py`:
|
||||
|
||||
```py
|
||||
import exporter
|
||||
from ty_extensions import dunder_all_names
|
||||
|
||||
# revealed: None
|
||||
reveal_type(dunder_all_names(exporter))
|
||||
```
|
||||
|
||||
### Non-string elements
|
||||
|
||||
Similarly, if `__all__` contains any non-string elements, we will consider `__all__` to not be
|
||||
defined for that module. This is also to avoid false positives.
|
||||
|
||||
`subexporter.py`:
|
||||
|
||||
```py
|
||||
__all__ = ("A", "B")
|
||||
|
||||
class A: ...
|
||||
class B: ...
|
||||
```
|
||||
|
||||
`exporter1.py`:
|
||||
|
||||
```py
|
||||
import subexporter
|
||||
from ty_extensions import dunder_all_names
|
||||
|
||||
# revealed: tuple[Literal["A"], Literal["B"]]
|
||||
reveal_type(dunder_all_names(subexporter))
|
||||
|
||||
# TODO: warning diagnostic
|
||||
__all__ = ("C", *subexporter.__all__)
|
||||
|
||||
class C: ...
|
||||
```
|
||||
|
||||
`importer.py`:
|
||||
|
||||
```py
|
||||
import exporter1
|
||||
from ty_extensions import dunder_all_names
|
||||
|
||||
# revealed: None
|
||||
reveal_type(dunder_all_names(exporter1))
|
||||
```
|
||||
|
||||
## Statically known branches
|
||||
|
||||
### Python 3.10
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.10"
|
||||
```
|
||||
|
||||
`exporter.py`:
|
||||
|
||||
```py
|
||||
import sys
|
||||
|
||||
__all__ = ["AllVersion"]
|
||||
|
||||
if sys.version_info >= (3, 12):
|
||||
__all__ += ["Python312"]
|
||||
elif sys.version_info >= (3, 11):
|
||||
__all__ += ["Python311"]
|
||||
else:
|
||||
__all__ += ["Python310"]
|
||||
|
||||
class AllVersion: ...
|
||||
|
||||
if sys.version_info >= (3, 12):
|
||||
class Python312: ...
|
||||
|
||||
elif sys.version_info >= (3, 11):
|
||||
class Python311: ...
|
||||
|
||||
else:
|
||||
class Python310: ...
|
||||
```
|
||||
|
||||
`importer.py`:
|
||||
|
||||
```py
|
||||
import exporter
|
||||
from ty_extensions import dunder_all_names
|
||||
|
||||
# revealed: tuple[Literal["AllVersion"], Literal["Python310"]]
|
||||
reveal_type(dunder_all_names(exporter))
|
||||
```
|
||||
|
||||
### Python 3.11
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.11"
|
||||
```
|
||||
|
||||
`exporter.py`:
|
||||
|
||||
```py
|
||||
import sys
|
||||
|
||||
__all__ = ["AllVersion"]
|
||||
|
||||
if sys.version_info >= (3, 12):
|
||||
__all__ += ["Python312"]
|
||||
elif sys.version_info >= (3, 11):
|
||||
__all__ += ["Python311"]
|
||||
else:
|
||||
__all__ += ["Python310"]
|
||||
|
||||
class AllVersion: ...
|
||||
|
||||
if sys.version_info >= (3, 12):
|
||||
class Python312: ...
|
||||
|
||||
elif sys.version_info >= (3, 11):
|
||||
class Python311: ...
|
||||
|
||||
else:
|
||||
class Python310: ...
|
||||
```
|
||||
|
||||
`importer.py`:
|
||||
|
||||
```py
|
||||
import exporter
|
||||
from ty_extensions import dunder_all_names
|
||||
|
||||
# revealed: tuple[Literal["AllVersion"], Literal["Python311"]]
|
||||
reveal_type(dunder_all_names(exporter))
|
||||
```
|
||||
|
||||
### Python 3.12
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
`exporter.py`:
|
||||
|
||||
```py
|
||||
import sys
|
||||
|
||||
__all__ = ["AllVersion"]
|
||||
|
||||
if sys.version_info >= (3, 12):
|
||||
__all__ += ["Python312"]
|
||||
elif sys.version_info >= (3, 11):
|
||||
__all__ += ["Python311"]
|
||||
else:
|
||||
__all__ += ["Python310"]
|
||||
|
||||
class AllVersion: ...
|
||||
|
||||
if sys.version_info >= (3, 12):
|
||||
class Python312: ...
|
||||
|
||||
elif sys.version_info >= (3, 11):
|
||||
class Python311: ...
|
||||
|
||||
else:
|
||||
class Python310: ...
|
||||
```
|
||||
|
||||
`importer.py`:
|
||||
|
||||
```py
|
||||
import exporter
|
||||
from ty_extensions import dunder_all_names
|
||||
|
||||
# revealed: tuple[Literal["AllVersion"], Literal["Python312"]]
|
||||
reveal_type(dunder_all_names(exporter))
|
||||
```
|
||||
|
||||
### Multiple `if` statements
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.11"
|
||||
```
|
||||
|
||||
`exporter.py`:
|
||||
|
||||
```py
|
||||
import sys
|
||||
|
||||
__all__ = ["AllVersion"]
|
||||
|
||||
if sys.version_info >= (3, 12):
|
||||
__all__ += ["Python312"]
|
||||
|
||||
if sys.version_info >= (3, 11):
|
||||
__all__ += ["Python311"]
|
||||
|
||||
if sys.version_info >= (3, 10):
|
||||
__all__ += ["Python310"]
|
||||
|
||||
class AllVersion: ...
|
||||
|
||||
if sys.version_info >= (3, 12):
|
||||
class Python312: ...
|
||||
|
||||
if sys.version_info >= (3, 11):
|
||||
class Python311: ...
|
||||
|
||||
if sys.version_info >= (3, 10):
|
||||
class Python310: ...
|
||||
```
|
||||
|
||||
`importer.py`:
|
||||
|
||||
```py
|
||||
import exporter
|
||||
from ty_extensions import dunder_all_names
|
||||
|
||||
# revealed: tuple[Literal["AllVersion"], Literal["Python310"], Literal["Python311"]]
|
||||
reveal_type(dunder_all_names(exporter))
|
||||
```
|
||||
|
||||
## Origin
|
||||
|
||||
`__all__` can be defined in a module mainly in the following three ways:
|
||||
|
||||
### Directly in the module
|
||||
|
||||
`exporter.py`:
|
||||
|
||||
```py
|
||||
__all__ = ["A"]
|
||||
|
||||
class A: ...
|
||||
```
|
||||
|
||||
`importer.py`:
|
||||
|
||||
```py
|
||||
import exporter
|
||||
from ty_extensions import dunder_all_names
|
||||
|
||||
# revealed: tuple[Literal["A"]]
|
||||
reveal_type(dunder_all_names(exporter))
|
||||
```
|
||||
|
||||
### Using named import
|
||||
|
||||
`subexporter.py`:
|
||||
|
||||
```py
|
||||
__all__ = ["A"]
|
||||
|
||||
class A: ...
|
||||
```
|
||||
|
||||
`exporter.py`:
|
||||
|
||||
```py
|
||||
from subexporter import __all__
|
||||
|
||||
__all__.append("B")
|
||||
|
||||
class B: ...
|
||||
```
|
||||
|
||||
`importer.py`:
|
||||
|
||||
```py
|
||||
import exporter
|
||||
import subexporter
|
||||
from ty_extensions import dunder_all_names
|
||||
|
||||
# revealed: tuple[Literal["A"]]
|
||||
reveal_type(dunder_all_names(subexporter))
|
||||
# revealed: tuple[Literal["A"], Literal["B"]]
|
||||
reveal_type(dunder_all_names(exporter))
|
||||
```
|
||||
|
||||
### Using wildcard import (1)
|
||||
|
||||
Wildcard import doesn't export `__all__` unless it is explicitly included in the `__all__` of the
|
||||
module.
|
||||
|
||||
`subexporter.py`:
|
||||
|
||||
```py
|
||||
__all__ = ["A", "__all__"]
|
||||
|
||||
class A: ...
|
||||
```
|
||||
|
||||
`exporter.py`:
|
||||
|
||||
```py
|
||||
from subexporter import *
|
||||
|
||||
# TODO: Should be `list[str]`
|
||||
# TODO: Should we avoid including `Unknown` for this case?
|
||||
reveal_type(__all__) # revealed: Unknown | list
|
||||
|
||||
__all__.append("B")
|
||||
|
||||
class B: ...
|
||||
```
|
||||
|
||||
`importer.py`:
|
||||
|
||||
```py
|
||||
import exporter
|
||||
import subexporter
|
||||
from ty_extensions import dunder_all_names
|
||||
|
||||
# revealed: tuple[Literal["A"], Literal["__all__"]]
|
||||
reveal_type(dunder_all_names(subexporter))
|
||||
# revealed: tuple[Literal["A"], Literal["B"], Literal["__all__"]]
|
||||
reveal_type(dunder_all_names(exporter))
|
||||
```
|
||||
|
||||
### Using wildcard import (2)
|
||||
|
||||
`subexporter.py`:
|
||||
|
||||
```py
|
||||
__all__ = ["A"]
|
||||
|
||||
class A: ...
|
||||
```
|
||||
|
||||
`exporter.py`:
|
||||
|
||||
```py
|
||||
from subexporter import *
|
||||
|
||||
# error: [unresolved-reference]
|
||||
reveal_type(__all__) # revealed: Unknown
|
||||
|
||||
# error: [unresolved-reference]
|
||||
__all__.append("B")
|
||||
|
||||
class B: ...
|
||||
```
|
||||
|
||||
`importer.py`:
|
||||
|
||||
```py
|
||||
import exporter
|
||||
import subexporter
|
||||
from ty_extensions import dunder_all_names
|
||||
|
||||
# revealed: tuple[Literal["A"]]
|
||||
reveal_type(dunder_all_names(subexporter))
|
||||
# revealed: None
|
||||
reveal_type(dunder_all_names(exporter))
|
||||
```
|
||||
|
||||
[specification]: https://typing.python.org/en/latest/spec/distributing.html#library-interface-public-and-private-symbols
|
|
@ -899,8 +899,8 @@ reveal_type(__protected) # revealed: bool
|
|||
reveal_type(__dunder__) # revealed: bool
|
||||
reveal_type(___thunder___) # revealed: bool
|
||||
|
||||
# TODO: should emit [unresolved-reference] diagnostic & reveal `Unknown`
|
||||
reveal_type(Y) # revealed: bool
|
||||
# error: [unresolved-reference]
|
||||
reveal_type(Y) # revealed: Unknown
|
||||
```
|
||||
|
||||
### Simple list `__all__`
|
||||
|
@ -921,8 +921,8 @@ from exporter import *
|
|||
|
||||
reveal_type(X) # revealed: bool
|
||||
|
||||
# TODO: should emit [unresolved-reference] diagnostic & reveal `Unknown`
|
||||
reveal_type(Y) # revealed: bool
|
||||
# error: [unresolved-reference]
|
||||
reveal_type(Y) # revealed: Unknown
|
||||
```
|
||||
|
||||
### `__all__` with additions later on in the global scope
|
||||
|
@ -949,15 +949,13 @@ __all__ = ["A"]
|
|||
__all__ += ["B"]
|
||||
__all__.append("C")
|
||||
__all__.extend(["D"])
|
||||
__all__.extend(("E",))
|
||||
__all__.extend(a.__all__)
|
||||
|
||||
A: bool = True
|
||||
B: bool = True
|
||||
C: bool = True
|
||||
D: bool = True
|
||||
E: bool = True
|
||||
F: bool = False
|
||||
E: bool = False
|
||||
```
|
||||
|
||||
`c.py`:
|
||||
|
@ -969,11 +967,10 @@ reveal_type(A) # revealed: bool
|
|||
reveal_type(B) # revealed: bool
|
||||
reveal_type(C) # revealed: bool
|
||||
reveal_type(D) # revealed: bool
|
||||
reveal_type(E) # revealed: bool
|
||||
reveal_type(FOO) # revealed: bool
|
||||
|
||||
# TODO should error with [unresolved-reference] & reveal `Unknown`
|
||||
reveal_type(F) # revealed: bool
|
||||
# error: [unresolved-reference]
|
||||
reveal_type(E) # revealed: Unknown
|
||||
```
|
||||
|
||||
### `__all__` with subtractions later on in the global scope
|
||||
|
@ -985,7 +982,7 @@ one way of subtracting from `__all__` that type checkers are required to support
|
|||
|
||||
```py
|
||||
__all__ = ["A", "B"]
|
||||
__all__.remove("A")
|
||||
__all__.remove("B")
|
||||
|
||||
A: bool = True
|
||||
B: bool = True
|
||||
|
@ -998,8 +995,8 @@ from exporter import *
|
|||
|
||||
reveal_type(A) # revealed: bool
|
||||
|
||||
# TODO should emit an [unresolved-reference] diagnostic & reveal `Unknown`
|
||||
reveal_type(B) # revealed: bool
|
||||
# error: [unresolved-reference]
|
||||
reveal_type(B) # revealed: Unknown
|
||||
```
|
||||
|
||||
### Invalid `__all__`
|
||||
|
@ -1125,8 +1122,8 @@ else:
|
|||
```py
|
||||
from exporter import *
|
||||
|
||||
# TODO: should reveal `Unknown` and emit `[unresolved-reference]`
|
||||
reveal_type(X) # revealed: bool
|
||||
# error: [unresolved-reference]
|
||||
reveal_type(X) # revealed: Unknown
|
||||
|
||||
# error: [unresolved-reference]
|
||||
reveal_type(Y) # revealed: Unknown
|
||||
|
@ -1199,8 +1196,8 @@ else:
|
|||
```py
|
||||
from exporter import *
|
||||
|
||||
# TODO: should reveal `Unknown` & emit `[unresolved-reference]
|
||||
reveal_type(X) # revealed: bool
|
||||
# error: [unresolved-reference]
|
||||
reveal_type(X) # revealed: Unknown
|
||||
|
||||
# error: [unresolved-reference]
|
||||
reveal_type(Y) # revealed: Unknown
|
||||
|
@ -1235,9 +1232,11 @@ __all__ = []
|
|||
from a import *
|
||||
from b import *
|
||||
|
||||
# TODO: both of these should have [unresolved-reference] diagnostics and reveal `Unknown`
|
||||
reveal_type(X) # revealed: bool
|
||||
reveal_type(Y) # revealed: bool
|
||||
# error: [unresolved-reference]
|
||||
reveal_type(X) # revealed: Unknown
|
||||
|
||||
# error: [unresolved-reference]
|
||||
reveal_type(Y) # revealed: Unknown
|
||||
```
|
||||
|
||||
### `__all__` in a stub file
|
||||
|
@ -1257,7 +1256,11 @@ Y: bool = True
|
|||
```pyi
|
||||
from a import X, Y
|
||||
|
||||
__all__ = ["X"]
|
||||
__all__ = ["X", "Z"]
|
||||
|
||||
Z: bool = True
|
||||
|
||||
Nope: bool = True
|
||||
```
|
||||
|
||||
`c.py`:
|
||||
|
@ -1265,18 +1268,21 @@ __all__ = ["X"]
|
|||
```py
|
||||
from b import *
|
||||
|
||||
# TODO: should not error, should reveal `bool`
|
||||
# (`X` is re-exported from `b.pyi` due to presence in `__all__`)
|
||||
# See https://github.com/astral-sh/ruff/issues/16159
|
||||
#
|
||||
# error: [unresolved-reference]
|
||||
reveal_type(X) # revealed: Unknown
|
||||
# `X` is re-exported from `b.pyi` due to presence in `__all__`
|
||||
reveal_type(X) # revealed: bool
|
||||
|
||||
# This diagnostic is accurate: `Y` does not use the "redundant alias" convention in `b.pyi`,
|
||||
# nor is it included in `b.__all__`, so it is not exported from `b.pyi`
|
||||
# nor is it included in `b.__all__`, so it is not exported from `b.pyi`. It would still be
|
||||
# an error if it used the "redundant alias" convention as `__all__` would take precedence.
|
||||
#
|
||||
# error: [unresolved-reference]
|
||||
reveal_type(Y) # revealed: Unknown
|
||||
|
||||
# `Z` is defined in `b.pyi` and included in `__all__`
|
||||
reveal_type(Z) # revealed: bool
|
||||
|
||||
# error: [unresolved-reference]
|
||||
reveal_type(Nope) # revealed: Unknown
|
||||
```
|
||||
|
||||
## `global` statements in non-global scopes
|
||||
|
@ -1355,10 +1361,7 @@ import collections.abc
|
|||
|
||||
reveal_type(collections.abc.Sequence) # revealed: <class 'Sequence'>
|
||||
reveal_type(collections.abc.Callable) # revealed: typing.Callable
|
||||
|
||||
# TODO: false positive as it's only re-exported from `_collections.abc` due to presence in `__all__`
|
||||
# error: [unresolved-attribute]
|
||||
reveal_type(collections.abc.Set) # revealed: Unknown
|
||||
reveal_type(collections.abc.Set) # revealed: <class 'AbstractSet'>
|
||||
```
|
||||
|
||||
## Invalid `*` imports
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue