-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Description
Describe the bug
The reportUnsupportedDunderAll rule triggers on __all__ definitions in a package's __init__.py that refer to the name of a submodule even when the submodule exists. The rule should only report names that are neither present in __init__.py nor are the name of a submodule.
This is the same issue reported in #3304, but the docs have recently been updated with more information on the semantics of __all__.
Code or Screenshots
In a package with the structure:
example/
__init__.py
submodule.py
# example/__init__.py
__all__ = [
"submodule" # <-- "submodule" is specified in __all__ but is not present in module
]VS Code extension or command-line
Command line pyright 1.1.333
Details from the Python docs
Refer to the docs for import. It defines how __all__ works for modules:
The public names defined by a module are determined by checking the module’s namespace for a variable named
__all__; if defined, it must be a sequence of strings which are names defined or imported by that module.
This lines up with everything so far. But there is additional behavior for packages which is detailed in an entirely different section of the documentation for some reason:
The import statement uses the following convention: if a package’s
__init__.pycode defines a list named__all__, it is taken to be the list of module names that should be imported when from package import * is encountered.
An example for a package sound/effects/__init__.py is given:
__all__ = ["echo", "surround", "reverse"]This would mean that from sound.effects import * would import the three named submodules of the sound.effects package.
It also goes on to clarify that __all__ can contain both names defined in __init__.py and names of submodules:
Be aware that submodules might become shadowed by locally defined names. For example, if you added a reverse function to the sound/effects/init.py file, the from sound.effects import * would only import the two submodules echo and surround, but not the reverse submodule, because it is shadowed by the locally defined reverse function:
__all__ = [ "echo", # refers to the 'echo.py' file "surround", # refers to the 'surround.py' file "reverse", # !!! refers to the 'reverse' function now !!! ] def reverse(msg: str): # <-- this name shadows the 'reverse.py' submodule return msg[::-1] # in the case of a 'from sound.effects import *'
Summary:
__all__should contain a list of names defined in the module/package- The names are what is imported by a
*import - The list defines the package/module's public API
- If the file is a module:
__all__should contain a list of names defined in the module.
- If the file is a package's
__init__.py:- If a name in
__all__refers to a name defined in the__init__.py, that object is what will be imported, both byfrom package import *andfrom package import name. - If the name is not defined in
__init__.py, it should refer to a submodule with that name, and that submodule will be imported byfrom package import *andfrom package import name
- If a name in
I think the rule needs to be enhanced to check for submodule names, as this appears to be correct behavior according to the docs, and as seen in the standard library packages defining __all__.