Skip to content

Commit 827361f

Browse files
committed
Fix stubgen regressions
Fix pybind11 stubgen tests so that they report failures
1 parent 0699dde commit 827361f

File tree

7 files changed

+54
-25
lines changed

7 files changed

+54
-25
lines changed

misc/test-stubgenc.sh

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ function stubgenc_test() {
2424
# Compare generated stubs to expected ones
2525
if ! git diff --exit-code "$STUBGEN_OUTPUT_FOLDER";
2626
then
27-
EXIT=$?
27+
echo "exit status $?"
28+
EXIT=1
2829
fi
2930
}
3031

mypy/stubdoc.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -374,7 +374,8 @@ def infer_ret_type_sig_from_docstring(docstr: str, name: str) -> str | None:
374374

375375
def infer_ret_type_sig_from_anon_docstring(docstr: str) -> str | None:
376376
"""Convert signature in form of "(self: TestClass, arg0) -> int" to their return type."""
377-
return infer_ret_type_sig_from_docstring("stub" + docstr.strip(), "stub")
377+
lines = ["stub" + line.strip() for line in docstr.splitlines() if line.strip().startswith("(")]
378+
return infer_ret_type_sig_from_docstring("".join(lines), "stub")
378379

379380

380381
def parse_signature(sig: str) -> tuple[str, list[str], list[str]] | None:

mypy/stubgen.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1629,6 +1629,7 @@ def generate_stubs(options: Options) -> None:
16291629
doc_dir=options.doc_dir,
16301630
include_private=options.include_private,
16311631
export_less=options.export_less,
1632+
include_docstrings=options.include_docstrings,
16321633
)
16331634
num_modules = len(all_modules)
16341635
if not options.quiet and num_modules > 0:

mypy/stubgenc.py

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -126,10 +126,12 @@ def get_property_type(self, default_type: str | None, ctx: FunctionContext) -> s
126126
"""Infer property type from docstring or docstring signature."""
127127
if ctx.docstring is not None:
128128
inferred = infer_ret_type_sig_from_anon_docstring(ctx.docstring)
129-
if not inferred:
130-
inferred = infer_ret_type_sig_from_docstring(ctx.docstring, ctx.name)
131-
if not inferred:
132-
inferred = infer_prop_type_from_docstring(ctx.docstring)
129+
if inferred:
130+
return inferred
131+
inferred = infer_ret_type_sig_from_docstring(ctx.docstring, ctx.name)
132+
if inferred:
133+
return inferred
134+
inferred = infer_prop_type_from_docstring(ctx.docstring)
133135
return inferred
134136
else:
135137
return None
@@ -590,9 +592,31 @@ def generate_function_stub(
590592
if inferred[0].args and inferred[0].args[0].name == "cls":
591593
decorators.append("@classmethod")
592594

593-
output.extend(self.format_func_def(inferred, decorators=decorators, docstring=docstring))
595+
output.extend(
596+
self.format_func_def(
597+
inferred, decorators=decorators, docstring=self._indent_docstring(docstring)
598+
)
599+
)
594600
self._fix_iter(ctx, inferred, output)
595601

602+
def _indent_docstring(self, docstring: str) -> str:
603+
"""Fix indentation of docstring extracted from pybind11 or other binding generators."""
604+
lines = docstring.splitlines(keepends=True)
605+
indent = self._indent + " "
606+
if len(lines) > 1:
607+
if not all(line.startswith(indent) or not line.strip() for line in lines):
608+
# if the docstring is not indented, then indent all but the first line
609+
for i, line in enumerate(lines[1:]):
610+
if line.strip():
611+
lines[i + 1] = indent + line
612+
# if there's a trailing newline, add a final line to visually indent the quoted docstring
613+
if lines[-1].endswith("\n"):
614+
if len(lines) > 1:
615+
lines.append(indent)
616+
else:
617+
lines[-1] = lines[-1][:-1]
618+
return "".join(lines)
619+
596620
def _fix_iter(
597621
self, ctx: FunctionContext, inferred: list[FunctionSig], output: list[str]
598622
) -> None:

test-data/pybind11_mypy_demo/src/main.cpp

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,14 +134,17 @@ void bind_basics(py::module& basics) {
134134
.def(py::init<double, double>(), py::arg("x"), py::arg("y"))
135135
.def("distance_to", py::overload_cast<double, double>(&Point::distance_to, py::const_), py::arg("x"), py::arg("y"))
136136
.def("distance_to", py::overload_cast<const Point&>(&Point::distance_to, py::const_), py::arg("other"))
137-
.def_readwrite("x", &Point::x)
137+
// Note that the trailing newline is required because the generated docstring
138+
// is concatenated to the signature, e.g.:
139+
// 'some docstring\n(self: pybind11_mypy_demo.basics.Point) -> float\n'
140+
.def_readwrite("x", &Point::x, "some docstring\n")
138141
.def_property("y",
139142
[](Point& self){ return self.y; },
140143
[](Point& self, double value){ self.y = value; }
141144
)
142145
.def_property_readonly("length", &Point::length)
143146
.def_property_readonly_static("x_axis", [](py::object cls){return Point::x_axis;})
144-
.def_property_readonly_static("y_axis", [](py::object cls){return Point::y_axis;})
147+
.def_property_readonly_static("y_axis", [](py::object cls){return Point::y_axis;}, "another docstring")
145148
.def_readwrite_static("length_unit", &Point::length_unit)
146149
.def_property_static("angle_unit",
147150
[](py::object& /*cls*/){ return Point::angle_unit; },
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from . import basics as basics

test-data/pybind11_mypy_demo/stubgen-include-docs/pybind11_mypy_demo/basics.pyi

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
from typing import ClassVar
1+
from typing import ClassVar, overload
22

3-
from typing import overload
43
PI: float
4+
__version__: str
55

66
class Point:
77
class AngleUnit:
@@ -13,8 +13,6 @@ class Point:
1313
"""__init__(self: pybind11_mypy_demo.basics.Point.AngleUnit, value: int) -> None"""
1414
def __eq__(self, other: object) -> bool:
1515
"""__eq__(self: object, other: object) -> bool"""
16-
def __getstate__(self) -> int:
17-
"""__getstate__(self: object) -> int"""
1816
def __hash__(self) -> int:
1917
"""__hash__(self: object) -> int"""
2018
def __index__(self) -> int:
@@ -23,8 +21,6 @@ class Point:
2321
"""__int__(self: pybind11_mypy_demo.basics.Point.AngleUnit) -> int"""
2422
def __ne__(self, other: object) -> bool:
2523
"""__ne__(self: object, other: object) -> bool"""
26-
def __setstate__(self, state: int) -> None:
27-
"""__setstate__(self: pybind11_mypy_demo.basics.Point.AngleUnit, state: int) -> None"""
2824
@property
2925
def name(self) -> str: ...
3026
@property
@@ -40,8 +36,6 @@ class Point:
4036
"""__init__(self: pybind11_mypy_demo.basics.Point.LengthUnit, value: int) -> None"""
4137
def __eq__(self, other: object) -> bool:
4238
"""__eq__(self: object, other: object) -> bool"""
43-
def __getstate__(self) -> int:
44-
"""__getstate__(self: object) -> int"""
4539
def __hash__(self) -> int:
4640
"""__hash__(self: object) -> int"""
4741
def __index__(self) -> int:
@@ -50,8 +44,6 @@ class Point:
5044
"""__int__(self: pybind11_mypy_demo.basics.Point.LengthUnit) -> int"""
5145
def __ne__(self, other: object) -> bool:
5246
"""__ne__(self: object, other: object) -> bool"""
53-
def __setstate__(self, state: int) -> None:
54-
"""__setstate__(self: pybind11_mypy_demo.basics.Point.LengthUnit, state: int) -> None"""
5547
@property
5648
def name(self) -> str: ...
5749
@property
@@ -70,43 +62,49 @@ class Point:
7062
7163
1. __init__(self: pybind11_mypy_demo.basics.Point) -> None
7264
73-
2. __init__(self: pybind11_mypy_demo.basics.Point, x: float, y: float) -> None"""
65+
2. __init__(self: pybind11_mypy_demo.basics.Point, x: float, y: float) -> None
66+
"""
7467
@overload
7568
def __init__(self, x: float, y: float) -> None:
7669
"""__init__(*args, **kwargs)
7770
Overloaded function.
7871
7972
1. __init__(self: pybind11_mypy_demo.basics.Point) -> None
8073
81-
2. __init__(self: pybind11_mypy_demo.basics.Point, x: float, y: float) -> None"""
74+
2. __init__(self: pybind11_mypy_demo.basics.Point, x: float, y: float) -> None
75+
"""
8276
@overload
8377
def distance_to(self, x: float, y: float) -> float:
8478
"""distance_to(*args, **kwargs)
8579
Overloaded function.
8680
8781
1. distance_to(self: pybind11_mypy_demo.basics.Point, x: float, y: float) -> float
8882
89-
2. distance_to(self: pybind11_mypy_demo.basics.Point, other: pybind11_mypy_demo.basics.Point) -> float"""
83+
2. distance_to(self: pybind11_mypy_demo.basics.Point, other: pybind11_mypy_demo.basics.Point) -> float
84+
"""
9085
@overload
9186
def distance_to(self, other: Point) -> float:
9287
"""distance_to(*args, **kwargs)
9388
Overloaded function.
9489
9590
1. distance_to(self: pybind11_mypy_demo.basics.Point, x: float, y: float) -> float
9691
97-
2. distance_to(self: pybind11_mypy_demo.basics.Point, other: pybind11_mypy_demo.basics.Point) -> float"""
92+
2. distance_to(self: pybind11_mypy_demo.basics.Point, other: pybind11_mypy_demo.basics.Point) -> float
93+
"""
9894
@property
9995
def length(self) -> float: ...
10096

10197
def answer() -> int:
10298
'''answer() -> int
10399
104-
answer docstring, with end quote"'''
100+
answer docstring, with end quote"
101+
'''
105102
def midpoint(left: float, right: float) -> float:
106103
"""midpoint(left: float, right: float) -> float"""
107104
def sum(arg0: int, arg1: int) -> int:
108105
'''sum(arg0: int, arg1: int) -> int
109106
110-
multiline docstring test, edge case quotes """\'\'\''''
107+
multiline docstring test, edge case quotes """\'\'\'
108+
'''
111109
def weighted_midpoint(left: float, right: float, alpha: float = ...) -> float:
112110
"""weighted_midpoint(left: float, right: float, alpha: float = 0.5) -> float"""

0 commit comments

Comments
 (0)