Repository: python/mypy
Pull request: stubgen: include __all__ in output
Fixes #10314

Problem statement:
Maybe __all__ should be added explicitly in stubgen
**Feature**

(A clear and concise description of your feature proposal.)

Maybe `__all__` should be added explicitly in stubgen.

**Pitch**

(Please explain why this feature should be implemented and how it would be used. Add examples, if applicable.)

Adding `__all__` to type stubs explicitly can avoid some error. For example:

├── test
│   └── __init__.py


\_\_init\_\_.py:
```python
class Test:
    pass

__all__ = []
```

The type stub file generated by stubgen is
\_\_init\_\_.pyi:
```python
class Test: ...
```

When using `from .test import *` to import test as a submodule, `Test` will be imported according to the type stub, while in fact `Test` should not be used.

**A Possible Implement**

Replace `output(self)` in stubgen.py by following implement:
```python
    def output(self) -> str:
        """Return the text for the stub."""
        imports = ''
        if self._import_lines:
            imports += ''.join(self._import_lines)
        imports += ''.join(self.import_tracker.import_lines())
        if imports and self._output:
            imports += '\n'
        imports += ''.join(self._output)
        """export __all__ when it's defined"""
        if imports and self._all_ != None:
            imports += '\n'
        if self._all_ != None:
            imports += '__all__ = ['
            for name in self._all_:
                imports += ''.join(('\"', name, '\", '))
            imports += ']'
        return imports
```

Hints from issue discussion:

Commit message: stubgen: include __all__ in output

Fixes #10314

Patch to review:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d8237795112b..74f7c676c279 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,7 +2,7 @@
 
 ## Unreleased
 
-...
+Stubgen will now include `__all__` in its output if it is in the input file (PR [16356](https://github.com/python/mypy/pull/16356)).
 
 #### Other Notable Changes and Fixes
 ...
diff --git a/mypy/stubutil.py b/mypy/stubutil.py
index cc3b63098fd2..e8c293d91939 100644
--- a/mypy/stubutil.py
+++ b/mypy/stubutil.py
@@ -614,10 +614,20 @@ def get_imports(self) -> str:
 
     def output(self) -> str:
         """Return the text for the stub."""
-        imports = self.get_imports()
-        if imports and self._output:
-            imports += "\n"
-        return imports + "".join(self._output)
+        pieces: list[str] = []
+        if imports := self.get_imports():
+            pieces.append(imports)
+        if dunder_all := self.get_dunder_all():
+            pieces.append(dunder_all)
+        if self._output:
+            pieces.append("".join(self._output))
+        return "\n".join(pieces)
+
+    def get_dunder_all(self) -> str:
+        """Return the __all__ list for the stub."""
+        if self._all_:
+            return f"__all__ = {self._all_!r}\n"
+        return ""
 
     def add(self, string: str) -> None:
         """Add text to generated stub."""
diff --git a/test-data/unit/stubgen.test b/test-data/unit/stubgen.test
index 895500c1ba57..1defa05bd80d 100644
--- a/test-data/unit/stubgen.test
+++ b/test-data/unit/stubgen.test
@@ -587,6 +587,8 @@ __all__ = [] + ['f']
 def f(): ...
 def g(): ...
 [out]
+__all__ = ['f']
+
 def f() -> None: ...
 
 [case testOmitDefsNotInAll_semanal]
@@ -594,6 +596,8 @@ __all__ = ['f']
 def f(): ...
 def g(): ...
 [out]
+__all__ = ['f']
+
 def f() -> None: ...
 
 [case testOmitDefsNotInAll_inspect]
@@ -601,6 +605,8 @@ __all__ = [] + ['f']
 def f(): ...
 def g(): ...
 [out]
+__all__ = ['f']
+
 def f(): ...
 
 [case testVarDefsNotInAll_import]
@@ -610,6 +616,8 @@ x = 1
 y = 1
 def g(): ...
 [out]
+__all__ = ['f', 'g']
+
 def f() -> None: ...
 def g() -> None: ...
 
@@ -620,6 +628,8 @@ x = 1
 y = 1
 def g(): ...
 [out]
+__all__ = ['f', 'g']
+
 def f(): ...
 def g(): ...
 
@@ -628,6 +638,8 @@ __all__ = [] + ['f']
 def f(): ...
 class A: ...
 [out]
+__all__ = ['f']
+
 def f() -> None: ...
 
 class A: ...
@@ -637,6 +649,8 @@ __all__ = [] + ['f']
 def f(): ...
 class A: ...
 [out]
+__all__ = ['f']
+
 def f(): ...
 
 class A: ...
@@ -647,6 +661,8 @@ class A:
     x = 1
     def f(self): ...
 [out]
+__all__ = ['A']
+
 class A:
     x: int
     def f(self) -> None: ...
@@ -684,6 +700,8 @@ x = 1
 [out]
 from re import match as match, sub as sub
 
+__all__ = ['match', 'sub', 'x']
+
 x: int
 
 [case testExportModule_import]
@@ -694,6 +712,8 @@ y = 2
 [out]
 import re as re
 
+__all__ = ['re', 'x']
+
 x: int
 
 [case testExportModule2_import]
@@ -704,6 +724,8 @@ y = 2
 [out]
 import re as re
 
+__all__ = ['re', 'x']
+
 x: int
 
 [case testExportModuleAs_import]
@@ -714,6 +736,8 @@ y = 2
 [out]
 import re as rex
 
+__all__ = ['rex', 'x']
+
 x: int
 
 [case testExportModuleInPackage_import]
@@ -722,6 +746,8 @@ __all__ = ['p']
 [out]
 import urllib.parse as p
 
+__all__ = ['p']
+
 [case testExportPackageOfAModule_import]
 import urllib.parse
 __all__ = ['urllib']
@@ -729,6 +755,8 @@ __all__ = ['urllib']
 [out]
 import urllib as urllib
 
+__all__ = ['urllib']
+
 [case testRelativeImportAll]
 from .x import *
 [out]
@@ -741,6 +769,8 @@ x = 1
 class C:
     def g(self): ...
 [out]
+__all__ = ['f', 'x', 'C', 'g']
+
 def f() -> None: ...
 
 x: int
@@ -758,6 +788,8 @@ x = 1
 class C:
     def g(self): ...
 [out]
+__all__ = ['f', 'x', 'C', 'g']
+
 def f(): ...
 
 x: int
@@ -2343,6 +2375,8 @@ else:
 [out]
 import cookielib as cookielib
 
+__all__ = ['cookielib']
+
 [case testCannotCalculateMRO_semanal]
 class X: pass
 
@@ -2788,6 +2822,8 @@ class A: pass
 # p/__init__.pyi
 from p.a import A
 
+__all__ = ['a']
+
 a: A
 # p/a.pyi
 class A: ...
@@ -2963,6 +2999,8 @@ __version__ = ''
 [out]
 from m import __version__ as __version__
 
+__all__ = ['__about__', '__author__', '__version__']
+
 [case testAttrsClass_semanal]
 import attrs
 


Code review comments:
diff_hunk: @@ -2963,6 +2999,8 @@ __version__ = ''
 [out]
 from m import __version__ as __version__
path: test-data/unit/stubgen.test
text: @user1:
This was a problem before your PR, but I'm not sure what the justification is for not importing `__about__` and `__author__` in the generated stub here, given that they're included in `__all__` at runtime

@author:
Yes, they're in IGNORED_DUNDERS in stubutil.py. It generally makes sense to exclude these names as they're not useful in stubs, but we shouldn't ignore them if they're in `__all__`.

@author:
Pushed a change so that IGNORED_DUNDERS aren't ignored if they're in `__all__`.