Skip to content

Commit 9882092

Browse files
committed
Add option to include docstrings with stubgen
Add a --include-docstrings flag to stubgen This was suggested in python#11965. When using this flag, the .pyi files will include docstrings for Python classes and functions and for C extension functions.
1 parent 0ec789d commit 9882092

File tree

5 files changed

+74
-14
lines changed

5 files changed

+74
-14
lines changed

mypy/fastparse.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1028,6 +1028,8 @@ def do_func_def(
10281028
# FuncDef overrides set_line -- can't use self.set_line
10291029
func_def.set_line(lineno, n.col_offset, end_line, end_column)
10301030
retval = func_def
1031+
if ast3.get_docstring(n):
1032+
func_def.docstring = ast3.get_docstring(n, clean=False)
10311033
self.class_and_function_stack.pop()
10321034
return retval
10331035

@@ -1137,6 +1139,8 @@ def visit_ClassDef(self, n: ast3.ClassDef) -> ClassDef:
11371139
else:
11381140
cdef.line = n.lineno
11391141
cdef.deco_line = n.decorator_list[0].lineno if n.decorator_list else None
1142+
if ast3.get_docstring(n):
1143+
cdef.docstring = ast3.get_docstring(n, clean=False)
11401144
cdef.column = n.col_offset
11411145
cdef.end_line = getattr(n, "end_lineno", None)
11421146
cdef.end_column = getattr(n, "end_col_offset", None)

mypy/nodes.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -774,6 +774,7 @@ class FuncDef(FuncItem, SymbolNode, Statement):
774774
"is_abstract",
775775
"original_def",
776776
"deco_line",
777+
"docstring",
777778
)
778779

779780
# Note that all __init__ args must have default values
@@ -794,6 +795,7 @@ def __init__(
794795
self.original_def: Union[None, FuncDef, Var, Decorator] = None
795796
# Used for error reporting (to keep backwad compatibility with pre-3.8)
796797
self.deco_line: Optional[int] = None
798+
self.docstring = None
797799

798800
@property
799801
def name(self) -> str:
@@ -1070,6 +1072,7 @@ class ClassDef(Statement):
10701072
"analyzed",
10711073
"has_incompatible_baseclass",
10721074
"deco_line",
1075+
"docstring",
10731076
)
10741077

10751078
name: str # Name of the class without module prefix
@@ -1111,6 +1114,7 @@ def __init__(
11111114
self.has_incompatible_baseclass = False
11121115
# Used for error reporting (to keep backwad compatibility with pre-3.8)
11131116
self.deco_line: Optional[int] = None
1117+
self.docstring: str = None
11141118

11151119
def accept(self, visitor: StatementVisitor[T]) -> T:
11161120
return visitor.visit_class_def(self)

mypy/stubgen.py

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,7 @@ def __init__(
206206
verbose: bool,
207207
quiet: bool,
208208
export_less: bool,
209+
include_docstrings: bool,
209210
) -> None:
210211
# See parse_options for descriptions of the flags.
211212
self.pyversion = pyversion
@@ -224,6 +225,7 @@ def __init__(
224225
self.verbose = verbose
225226
self.quiet = quiet
226227
self.export_less = export_less
228+
self.include_docstrings = include_docstrings
227229

228230

229231
class StubSource:
@@ -572,6 +574,7 @@ def __init__(
572574
include_private: bool = False,
573575
analyzed: bool = False,
574576
export_less: bool = False,
577+
include_docstrings: bool = False,
575578
) -> None:
576579
# Best known value of __all__.
577580
self._all_ = _all_
@@ -587,6 +590,7 @@ def __init__(
587590
self._toplevel_names: List[str] = []
588591
self._pyversion = pyversion
589592
self._include_private = include_private
593+
self._include_docstrings = include_docstrings
590594
self.import_tracker = ImportTracker()
591595
# Was the tree semantically analysed before?
592596
self.analyzed = analyzed
@@ -754,7 +758,11 @@ def visit_func_def(
754758
retfield = " -> " + retname
755759

756760
self.add(", ".join(args))
757-
self.add(f"){retfield}: ...\n")
761+
self.add(f"){retfield}:")
762+
if self._include_docstrings and o.docstring:
763+
self.add(f'\n{self._indent} """{o.docstring}"""\n{self._indent} ')
764+
765+
self.add(" ...\n")
758766
self._state = FUNC
759767

760768
def is_none_expr(self, expr: Expression) -> bool:
@@ -926,8 +934,10 @@ def visit_class_def(self, o: ClassDef) -> None:
926934
if base_types:
927935
self.add(f"({', '.join(base_types)})")
928936
self.add(":\n")
929-
n = len(self._output)
930937
self._indent += " "
938+
if o.docstring:
939+
self.add(f'{self._indent}"""{o.docstring}"""\n')
940+
n = len(self._output)
931941
self._vars.append([])
932942
super().visit_class_def(o)
933943
self._indent = self._indent[:-4]
@@ -1605,6 +1615,7 @@ def generate_stub_from_ast(
16051615
pyversion: Tuple[int, int] = defaults.PYTHON3_VERSION,
16061616
include_private: bool = False,
16071617
export_less: bool = False,
1618+
include_docstrings: bool = False,
16081619
) -> None:
16091620
"""Use analysed (or just parsed) AST to generate type stub for single file.
16101621
@@ -1617,6 +1628,7 @@ def generate_stub_from_ast(
16171628
include_private=include_private,
16181629
analyzed=not parse_only,
16191630
export_less=export_less,
1631+
include_docstrings=include_docstrings,
16201632
)
16211633
assert mod.ast is not None, "This function must be used only with analyzed modules"
16221634
mod.ast.accept(gen)
@@ -1677,6 +1689,7 @@ def generate_stubs(options: Options) -> None:
16771689
options.pyversion,
16781690
options.include_private,
16791691
options.export_less,
1692+
options.include_docstrings,
16801693
)
16811694

16821695
# Separately analyse C modules using different logic.
@@ -1688,7 +1701,7 @@ def generate_stubs(options: Options) -> None:
16881701
target = os.path.join(options.output_dir, target)
16891702
files.append(target)
16901703
with generate_guarded(mod.module, target, options.ignore_errors, options.verbose):
1691-
generate_stub_for_c_module(mod.module, target, sigs=sigs, class_sigs=class_sigs)
1704+
generate_stub_for_c_module(mod.module, target, sigs=sigs, class_sigs=class_sigs, include_docstrings=options.include_docstrings)
16921705
num_modules = len(py_modules) + len(c_modules)
16931706
if not options.quiet and num_modules > 0:
16941707
print("Processed %d modules" % num_modules)
@@ -1743,6 +1756,13 @@ def parse_options(args: List[str]) -> Options:
17431756
"don't implicitly export all names imported from other modules " "in the same package"
17441757
),
17451758
)
1759+
parser.add_argument(
1760+
"--include-docstrings",
1761+
action="store_true",
1762+
help=(
1763+
"include existing docstrings with the stubs"
1764+
),
1765+
)
17461766
parser.add_argument("-v", "--verbose", action="store_true", help="show more verbose messages")
17471767
parser.add_argument("-q", "--quiet", action="store_true", help="show fewer messages")
17481768
parser.add_argument(
@@ -1823,6 +1843,7 @@ def parse_options(args: List[str]) -> Options:
18231843
verbose=ns.verbose,
18241844
quiet=ns.quiet,
18251845
export_less=ns.export_less,
1846+
include_docstrings=ns.include_docstrings,
18261847
)
18271848

18281849

mypy/stubgenc.py

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ def generate_stub_for_c_module(
4444
target: str,
4545
sigs: Optional[Dict[str, str]] = None,
4646
class_sigs: Optional[Dict[str, str]] = None,
47+
include_docstrings: bool = False,
4748
) -> None:
4849
"""Generate stub for C module.
4950
@@ -64,15 +65,15 @@ def generate_stub_for_c_module(
6465
items = sorted(module.__dict__.items(), key=lambda x: x[0])
6566
for name, obj in items:
6667
if is_c_function(obj):
67-
generate_c_function_stub(module, name, obj, functions, imports=imports, sigs=sigs)
68+
generate_c_function_stub(module, name, obj, functions, imports=imports, sigs=sigs, include_docstrings=include_docstrings)
6869
done.add(name)
6970
types: List[str] = []
7071
for name, obj in items:
7172
if name.startswith("__") and name.endswith("__"):
7273
continue
7374
if is_c_type(obj):
7475
generate_c_type_stub(
75-
module, name, obj, types, imports=imports, sigs=sigs, class_sigs=class_sigs
76+
module, name, obj, types, imports=imports, sigs=sigs, class_sigs=class_sigs, include_docstrings=include_docstrings
7677
)
7778
done.add(name)
7879
variables = []
@@ -156,10 +157,11 @@ def generate_c_function_stub(
156157
sigs: Optional[Dict[str, str]] = None,
157158
class_name: Optional[str] = None,
158159
class_sigs: Optional[Dict[str, str]] = None,
160+
include_docstrings: bool = False
159161
) -> None:
160162
"""Generate stub for a single function or method.
161163
162-
The result (always a single line) will be appended to 'output'.
164+
The result will be appended to 'output'.
163165
If necessary, any required names will be added to 'imports'.
164166
The 'class_name' is used to find signature of __init__ or __new__ in
165167
'class_sigs'.
@@ -170,7 +172,7 @@ def generate_c_function_stub(
170172
class_sigs = {}
171173

172174
ret_type = "None" if name == "__init__" and class_name else "Any"
173-
175+
docstr = None
174176
if (
175177
name in ("__new__", "__init__")
176178
and name not in sigs
@@ -236,13 +238,23 @@ def generate_c_function_stub(
236238

237239
if is_overloaded:
238240
output.append("@overload")
239-
output.append(
240-
"def {function}({args}) -> {ret}: ...".format(
241-
function=name,
242-
args=", ".join(sig),
243-
ret=strip_or_import(signature.ret_type, module, imports),
241+
if include_docstrings and docstr:
242+
output.append(
243+
"def {function}({args}) -> {ret}:\n\"\"\"{docstr}\"\"\"\n...".format(
244+
function=name,
245+
args=", ".join(sig),
246+
ret=strip_or_import(signature.ret_type, module, imports),
247+
docstr=docstr,
248+
)
249+
)
250+
else:
251+
output.append(
252+
"def {function}({args}) -> {ret}: ...".format(
253+
function=name,
254+
args=", ".join(sig),
255+
ret=strip_or_import(signature.ret_type, module, imports),
256+
)
244257
)
245-
)
246258

247259

248260
def strip_or_import(typ: str, module: ModuleType, imports: List[str]) -> str:
@@ -339,6 +351,7 @@ def generate_c_type_stub(
339351
imports: List[str],
340352
sigs: Optional[Dict[str, str]] = None,
341353
class_sigs: Optional[Dict[str, str]] = None,
354+
include_docstrings: bool = False
342355
) -> None:
343356
"""Generate stub for a single class using runtime introspection.
344357
@@ -382,6 +395,7 @@ def generate_c_type_stub(
382395
sigs=sigs,
383396
class_name=class_name,
384397
class_sigs=class_sigs,
398+
include_docstrings=include_docstrings,
385399
)
386400
elif is_c_property(value):
387401
done.add(attr)
@@ -397,7 +411,7 @@ def generate_c_type_stub(
397411
)
398412
elif is_c_type(value):
399413
generate_c_type_stub(
400-
module, attr, value, types, imports=imports, sigs=sigs, class_sigs=class_sigs
414+
module, attr, value, types, imports=imports, sigs=sigs, class_sigs=class_sigs, include_docstrings=include_docstrings
401415
)
402416
done.add(attr)
403417

test-data/unit/stubgen.test

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2688,3 +2688,20 @@ def f():
26882688
return 0
26892689
[out]
26902690
def f(): ...
2691+
2692+
[case testIncludeDocstrings]
2693+
# flags: --include-docstrings
2694+
class A:
2695+
"""class docstring"""
2696+
def func():
2697+
"""func docstring"""
2698+
...
2699+
def nodoc():
2700+
...
2701+
[out]
2702+
class A:
2703+
"""class docstring"""
2704+
def func() -> None:
2705+
"""func docstring"""
2706+
...
2707+
def nodoc() -> None: ...

0 commit comments

Comments
 (0)