Skip to content

Commit a9bb737

Browse files
authored
stubgenc: add support for including class and property docstrings (#17964)
<!-- If this pull request fixes an issue, add "Fixes #NNN" with the issue number. --> Prior to this change passing `--include-docstrings` did not generate docstrings for classes or properties, only functions. This PR brings c-extensions up to parity with pure-python modules. I used this feature to generate stubs for this project: https://github.com/LumaPictures/cg-stubs/blob/master/usd/stubs/pxr/Usd/__init__.pyi <!-- Checklist: - Read the [Contributing Guidelines](https://github.com/python/mypy/blob/master/CONTRIBUTING.md) - Add tests for all changed behaviour. - If you can't add a test, please explain why and how you verified your changes work. - Make sure CI passes. - Please do not force push to the PR once it has been reviewed. -->
1 parent 301c3b6 commit a9bb737

File tree

5 files changed

+64
-22
lines changed

5 files changed

+64
-22
lines changed

mypy/stubdoc.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ class FunctionSig(NamedTuple):
7878
args: list[ArgSig]
7979
ret_type: str | None
8080
type_args: str = "" # TODO implement in stubgenc and remove the default
81+
docstring: str | None = None
8182

8283
def is_special_method(self) -> bool:
8384
return bool(
@@ -110,6 +111,7 @@ def format_sig(
110111
is_async: bool = False,
111112
any_val: str | None = None,
112113
docstring: str | None = None,
114+
include_docstrings: bool = False,
113115
) -> str:
114116
args: list[str] = []
115117
for arg in self.args:
@@ -144,8 +146,11 @@ def format_sig(
144146

145147
prefix = "async " if is_async else ""
146148
sig = f"{indent}{prefix}def {self.name}{self.type_args}({', '.join(args)}){retfield}:"
147-
if docstring:
148-
suffix = f"\n{indent} {mypy.util.quote_docstring(docstring)}"
149+
# if this object has a docstring it's probably produced by a SignatureGenerator, so it
150+
# takes precedence over the passed docstring, which acts as a fallback.
151+
doc = (self.docstring or docstring) if include_docstrings else None
152+
if doc:
153+
suffix = f"\n{indent} {mypy.util.quote_docstring(doc)}"
149154
else:
150155
suffix = " ..."
151156
return f"{sig}{suffix}"

mypy/stubgenc.py

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
infer_method_arg_types,
3939
infer_method_ret_type,
4040
)
41+
from mypy.util import quote_docstring
4142

4243

4344
class ExternalSignatureGenerator(SignatureGenerator):
@@ -649,8 +650,7 @@ def generate_function_stub(
649650
if inferred[0].args and inferred[0].args[0].name == "cls":
650651
decorators.append("@classmethod")
651652

652-
if docstring:
653-
docstring = self._indent_docstring(docstring)
653+
docstring = self._indent_docstring(ctx.docstring) if ctx.docstring else None
654654
output.extend(self.format_func_def(inferred, decorators=decorators, docstring=docstring))
655655
self._fix_iter(ctx, inferred, output)
656656

@@ -754,9 +754,14 @@ def generate_property_stub(
754754
)
755755
else: # regular property
756756
if readonly:
757+
docstring = self._indent_docstring(ctx.docstring) if ctx.docstring else None
757758
ro_properties.append(f"{self._indent}@property")
758-
sig = FunctionSig(name, [ArgSig("self")], inferred_type)
759-
ro_properties.append(sig.format_sig(indent=self._indent))
759+
sig = FunctionSig(name, [ArgSig("self")], inferred_type, docstring=docstring)
760+
ro_properties.append(
761+
sig.format_sig(
762+
indent=self._indent, include_docstrings=self._include_docstrings
763+
)
764+
)
760765
else:
761766
if inferred_type is None:
762767
inferred_type = self.add_name("_typeshed.Incomplete")
@@ -875,8 +880,17 @@ def generate_class_stub(
875880
bases_str = "(%s)" % ", ".join(bases)
876881
else:
877882
bases_str = ""
878-
if types or static_properties or rw_properties or methods or ro_properties:
883+
884+
if class_info.docstring and self._include_docstrings:
885+
doc = quote_docstring(self._indent_docstring(class_info.docstring))
886+
doc = f" {self._indent}{doc}"
887+
docstring = doc.splitlines(keepends=False)
888+
else:
889+
docstring = []
890+
891+
if docstring or types or static_properties or rw_properties or methods or ro_properties:
879892
output.append(f"{self._indent}class {class_name}{bases_str}:")
893+
output.extend(docstring)
880894
for line in types:
881895
if (
882896
output
@@ -886,14 +900,10 @@ def generate_class_stub(
886900
):
887901
output.append("")
888902
output.append(line)
889-
for line in static_properties:
890-
output.append(line)
891-
for line in rw_properties:
892-
output.append(line)
893-
for line in methods:
894-
output.append(line)
895-
for line in ro_properties:
896-
output.append(line)
903+
output.extend(static_properties)
904+
output.extend(rw_properties)
905+
output.extend(methods)
906+
output.extend(ro_properties)
897907
else:
898908
output.append(f"{self._indent}class {class_name}{bases_str}: ...")
899909

mypy/stubutil.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -803,7 +803,8 @@ def format_func_def(
803803
signature.format_sig(
804804
indent=self._indent,
805805
is_async=is_coroutine,
806-
docstring=docstring if self._include_docstrings else None,
806+
docstring=docstring,
807+
include_docstrings=self._include_docstrings,
807808
)
808809
)
809810
return lines

test-data/pybind11_fixtures/expected_stubs_with_docs/pybind11_fixtures/__init__.pyi

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,10 @@ class TestStruct:
3838
def __init__(self, *args, **kwargs) -> None:
3939
"""Initialize self. See help(type(self)) for accurate signature."""
4040
@property
41-
def field_readonly(self) -> int: ...
41+
def field_readonly(self) -> int:
42+
"""some docstring
43+
(arg0: pybind11_fixtures.TestStruct) -> int
44+
"""
4245

4346
def func_incomplete_signature(*args, **kwargs):
4447
"""func_incomplete_signature() -> dummy_sub_namespace::HasNoBinding"""

test-data/pybind11_fixtures/expected_stubs_with_docs/pybind11_fixtures/demo.pyi

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ __version__: str
55

66
class Point:
77
class AngleUnit:
8+
"""Members:
9+
10+
radian
11+
12+
degree"""
813
__members__: ClassVar[dict] = ... # read-only
914
__entries: ClassVar[dict] = ...
1015
degree: ClassVar[Point.AngleUnit] = ...
@@ -22,11 +27,23 @@ class Point:
2227
def __ne__(self, other: object) -> bool:
2328
"""__ne__(self: object, other: object) -> bool"""
2429
@property
25-
def name(self) -> str: ...
30+
def name(self) -> str:
31+
"""name(self: handle) -> str
32+
33+
name(self: handle) -> str
34+
"""
2635
@property
27-
def value(self) -> int: ...
36+
def value(self) -> int:
37+
"""(arg0: pybind11_fixtures.demo.Point.AngleUnit) -> int"""
2838

2939
class LengthUnit:
40+
"""Members:
41+
42+
mm
43+
44+
pixel
45+
46+
inch"""
3047
__members__: ClassVar[dict] = ... # read-only
3148
__entries: ClassVar[dict] = ...
3249
inch: ClassVar[Point.LengthUnit] = ...
@@ -45,9 +62,14 @@ class Point:
4562
def __ne__(self, other: object) -> bool:
4663
"""__ne__(self: object, other: object) -> bool"""
4764
@property
48-
def name(self) -> str: ...
65+
def name(self) -> str:
66+
"""name(self: handle) -> str
67+
68+
name(self: handle) -> str
69+
"""
4970
@property
50-
def value(self) -> int: ...
71+
def value(self) -> int:
72+
"""(arg0: pybind11_fixtures.demo.Point.LengthUnit) -> int"""
5173
angle_unit: ClassVar[Point.AngleUnit] = ...
5274
length_unit: ClassVar[Point.LengthUnit] = ...
5375
x_axis: ClassVar[Point] = ... # read-only
@@ -94,7 +116,8 @@ class Point:
94116
2. distance_to(self: pybind11_fixtures.demo.Point, other: pybind11_fixtures.demo.Point) -> float
95117
"""
96118
@property
97-
def length(self) -> float: ...
119+
def length(self) -> float:
120+
"""(arg0: pybind11_fixtures.demo.Point) -> float"""
98121

99122
def answer() -> int:
100123
'''answer() -> int

0 commit comments

Comments
 (0)