Skip to content

Commit ef92542

Browse files
authored
Improve CLI, refactor and document stubgen (#6256)
This PR does several "infrastructure" changes to `stubgen` tool: * Update CLI and source discovery/collection to match the `mypy` CLI * Refactor the logic in `stubgen.main()` into independent functions * Use semantically analyzed Python ASTs * Separate inference of signatures from docs to a separate module `stubdoc.py` * Move some functions from `stubgen.py` to `stubutil.py` and reorganize the latter * Clean-up the test runner to make more use of `DataSuite` * Add documentation for the `stubgen` script This also does few smaller things: * Minimize number of repeated hard-coded constants * Removed duplicate TODOs * Added dosctrings to `stubgenc.py` and (new) `stubdoc.py` * Avoid mutable defaults This is not a pure refactoring, turning the semantic analysis on required some (although relatively small) changes in logic (because the sources should be semantically analyzed as a whole). It also required couple minor changes in `semanal.py` and `build.py`.
1 parent d6aef70 commit ef92542

File tree

12 files changed

+1458
-779
lines changed

12 files changed

+1458
-779
lines changed

docs/source/index.rst

+1
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ Mypy is a static type checker for Python 3 and Python 2.7.
5353
mypy_daemon
5454
installed_packages
5555
extending_mypy
56+
stubgen
5657

5758
.. toctree::
5859
:maxdepth: 2

docs/source/stubgen.rst

+143
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
.. _stugen:
2+
3+
Automatic stub generation
4+
=========================
5+
6+
Stub files (see `PEP 484 <https://www.python.org/dev/peps/pep-0484/#stub-files>`_)
7+
are files containing only type hints not the actual runtime implementation.
8+
They can be useful for C extension modules, third-party modules whose authors
9+
have not yet added type hints, etc.
10+
11+
Mypy comes with a ``stubgen`` tool for automatic generation of
12+
stub files (``.pyi`` files) from Python source files. For example,
13+
this source file:
14+
15+
.. code-block:: python
16+
17+
from other_module import dynamic
18+
19+
BORDER_WIDTH = 15
20+
21+
class Window:
22+
parent = dynamic()
23+
def __init__(self, width, hight):
24+
self.width = width
25+
self.hight = hight
26+
27+
def create_empty() -> Window:
28+
return Window(0, 0)
29+
30+
will be transformed into the following stub file:
31+
32+
.. code-block:: python
33+
34+
from typing import Any
35+
36+
BORDER_WIDTH: int = ...
37+
38+
class Window:
39+
parent: Any = ...
40+
width: Any = ...
41+
height: Any: ...
42+
def __init__(self, width, height) -> None: ...
43+
44+
def create_empty() -> Window: ...
45+
46+
In most cases, the auto-generated stub files require manual check for
47+
completeness. This section documents stubgen's command line interface.
48+
You can view a quick summary of the available flags by running
49+
``stubgen --help``.
50+
51+
.. note::
52+
53+
Stubgen tool is still experimental and will evolve. Command line flags
54+
are liable to change between releases.
55+
56+
Specifying what to stub
57+
***********************
58+
59+
By default, you can specify for what code you want to generate
60+
stub files by passing in the paths to the sources::
61+
62+
$ stubgen foo.py bar.py some_directory
63+
64+
Note that directories are checked recursively.
65+
66+
Stubgen also lets you specify modules for stub generation in two
67+
other ways. The relevant flags are:
68+
69+
``-m MODULE``, ``--module MODULE``
70+
Asks stubgen to generate stub file for the provided module. This flag
71+
may be repeated multiple times.
72+
73+
Stubgen *will not* recursively generate stubs for any submodules of
74+
the provided module.
75+
76+
``-p PACKAGE``, ``--package PACKAGE``
77+
Asks stubgen to generate stubs for the provided package. This flag may
78+
be repeated multiple times.
79+
80+
Stubgen *will* recursively generate stubs for all submodules of
81+
the provided package. This flag is identical to ``--module`` apart from
82+
this behavior.
83+
84+
.. note::
85+
86+
You can use either module/package mode or source code mode, these two
87+
can't be mixed together in the same stubgen invocation.
88+
89+
Specifying how to generate stubs
90+
********************************
91+
92+
By default stubgen will try to import the modules and packages given.
93+
This has an advantage of possibility to discover and stub also C modules.
94+
By default stubgen will use mypy to semantically analyze the Python
95+
sources found. To alter this behavior, you can use following flags:
96+
97+
``--no-import``
98+
Don't try to import modules, instead use mypy's normal mechanisms to find
99+
sources. This will not find any C extension modules. Stubgen also uses
100+
runtime introspection to find actual value of ``__all__``, so with this flag
101+
the set of re-exported names may be incomplete. This flag will be useful if
102+
importing the module causes an error.
103+
104+
``--parse-only``
105+
Don't perform mypy semantic analysis of source files. This may generate
106+
worse stubs: in particular some module, class, and function aliases may
107+
be typed as variables with ``Any`` type. This can be useful if semantic
108+
analysis causes a critical mypy error.
109+
110+
``--doc-dir PATH``
111+
Try to infer function and class signatures by parsing .rst documentation
112+
in ``PATH``. This may result in better stubs, but currently only works for
113+
C modules.
114+
115+
Additional flags
116+
****************
117+
118+
``--py2``
119+
Run stubgen in Python 2 mode (the default is Python 3 mode).
120+
121+
``--ignore-errors``
122+
Ignore any errors when trying to generate stubs for modules and packages.
123+
This may be useful for C modules where runtime introspection is used
124+
intensively.
125+
126+
``--include-private``
127+
Generate stubs for objects and members considered private (with single
128+
leading underscore and no trailing underscores).
129+
130+
``--search-path PATH``
131+
Specify module search directories, separated by colons (currently only
132+
used if ``--no-import`` is given).
133+
134+
``--python-executable PATH``
135+
Use Python interpreter at ``PATH`` for module finding and runtime
136+
introspection (has no effect with ``--no-import``). Currently only works
137+
for Python 2. In Python 3 mode only the default interpreter will be used.
138+
139+
``-o PATH``, ``--output PATH``
140+
Change the output directory. By default the stubs are written in
141+
``./out`` directory. The output directory will be created if it didn't
142+
exist. Existing stubs in the output directory will be overwritten without
143+
warning.

mypy/build.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -1463,7 +1463,8 @@ def __init__(self,
14631463
# as error reporting should be avoided.
14641464
temporary: bool = False,
14651465
) -> None:
1466-
assert id or path or source is not None, "Neither id, path nor source given"
1466+
if not temporary:
1467+
assert id or path or source is not None, "Neither id, path nor source given"
14671468
self.manager = manager
14681469
State.order_counter += 1
14691470
self.order = State.order_counter

mypy/newsemanal/semanal.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,9 @@ def __init__(self,
245245
self.incomplete_namespaces = incomplete_namespaces
246246
self.postpone_nested_functions_stack = [FUNCTION_BOTH_PHASES]
247247
self.postponed_functions_stack = []
248-
self.all_exports = set() # type: Set[str]
248+
self.all_exports = [] # type: List[str]
249+
# Map from module id to list of explicitly exported names (i.e. names in __all__).
250+
self.export_map = {} # type: Dict[str, List[str]]
249251
self.plugin = plugin
250252
# If True, process function definitions. If False, don't. This is used
251253
# for processing module top levels in fine-grained incremental mode.
@@ -317,6 +319,8 @@ def visit_file(self, file_node: MypyFile, fnam: str, options: Options,
317319
if name not in self.all_exports:
318320
g.module_public = False
319321

322+
self.export_map[self.cur_mod_id] = self.all_exports
323+
self.all_exports = []
320324
del self.options
321325
del self.patches
322326
del self.cur_mod_node
@@ -3899,7 +3903,7 @@ def add_exports(self, exp_or_exps: Union[Iterable[Expression], Expression]) -> N
38993903
exps = [exp_or_exps] if isinstance(exp_or_exps, Expression) else exp_or_exps
39003904
for exp in exps:
39013905
if isinstance(exp, StrExpr):
3902-
self.all_exports.add(exp.value)
3906+
self.all_exports.append(exp.value)
39033907

39043908
def check_no_global(self, n: str, ctx: Context,
39053909
is_overloaded_func: bool = False) -> None:

mypy/nodes.py

+3
Original file line numberDiff line numberDiff line change
@@ -652,6 +652,8 @@ class Decorator(SymbolNode, Statement):
652652

653653
func = None # type: FuncDef # Decorated function
654654
decorators = None # type: List[Expression] # Decorators (may be empty)
655+
# Some decorators are removed by semanal, keep the original here.
656+
original_decorators = None # type: List[Expression]
655657
# TODO: This is mostly used for the type; consider replacing with a 'type' attribute
656658
var = None # type: Var # Represents the decorated function obj
657659
is_overload = False
@@ -661,6 +663,7 @@ def __init__(self, func: FuncDef, decorators: List[Expression],
661663
super().__init__()
662664
self.func = func
663665
self.decorators = decorators
666+
self.original_decorators = decorators.copy()
664667
self.var = var
665668
self.is_overload = False
666669

mypy/semanal.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,9 @@ def __init__(self,
242242
self.missing_modules = missing_modules
243243
self.postpone_nested_functions_stack = [FUNCTION_BOTH_PHASES]
244244
self.postponed_functions_stack = []
245-
self.all_exports = set() # type: Set[str]
245+
self.all_exports = [] # type: List[str]
246+
# Map from module id to list of explicitly exported names (i.e. names in __all__).
247+
self.export_map = {} # type: Dict[str, List[str]]
246248
self.plugin = plugin
247249
# If True, process function definitions. If False, don't. This is used
248250
# for processing module top levels in fine-grained incremental mode.
@@ -314,6 +316,8 @@ def visit_file(self, file_node: MypyFile, fnam: str, options: Options,
314316
if name not in self.all_exports:
315317
g.module_public = False
316318

319+
self.export_map[self.cur_mod_id] = self.all_exports
320+
self.all_exports = []
317321
del self.options
318322
del self.patches
319323
del self.cur_mod_node
@@ -3707,7 +3711,7 @@ def add_exports(self, exp_or_exps: Union[Iterable[Expression], Expression]) -> N
37073711
exps = [exp_or_exps] if isinstance(exp_or_exps, Expression) else exp_or_exps
37083712
for exp in exps:
37093713
if isinstance(exp, StrExpr):
3710-
self.all_exports.add(exp.value)
3714+
self.all_exports.append(exp.value)
37113715

37123716
def check_no_global(self, n: str, ctx: Context,
37133717
is_overloaded_func: bool = False) -> None:

0 commit comments

Comments
 (0)