Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Inspect class doc output #17685

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
54 changes: 38 additions & 16 deletions mypy/stubgenc.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@
import inspect
import keyword
import os.path
import sys
from types import FunctionType, ModuleType
from typing import Any, Callable, Mapping

import mypy.util
from mypy.fastparse import parse_type_comment
from mypy.moduleinspect import is_c_module
from mypy.stubdoc import (
Expand Down Expand Up @@ -651,21 +653,35 @@ def generate_function_stub(

def _indent_docstring(self, docstring: str) -> str:
"""Fix indentation of docstring extracted from pybind11 or other binding generators."""
lines = docstring.splitlines(keepends=True)
indent = self._indent + " "
if len(lines) > 1:
if not all(line.startswith(indent) or not line.strip() for line in lines):
# if the docstring is not indented, then indent all but the first line
for i, line in enumerate(lines[1:]):
if line.strip():
lines[i + 1] = indent + line
# if there's a trailing newline, add a final line to visually indent the quoted docstring
if lines[-1].endswith("\n"):
if len(lines) > 1:
lines.append(indent)
else:
lines[-1] = lines[-1][:-1]
return "".join(lines)
# this follows inspect.cleandoc except it only changes the margins.
# it won't remove empty lines at the start or end.
# nor remove whitespace at the start of the first line.
# essentially it should do as little to the docstring as possible.

lines = docstring.expandtabs().split("\n")

# Find minimum indentation of any non-blank lines after first line.
margin = sys.maxsize
for line in lines[1:]:
content = len(line.lstrip(" "))
if content:
indent = len(line) - content
margin = min(margin, indent)

doc_indent = self._indent + " "
# Remove margin and set it to indent.
if margin < sys.maxsize:
for i in range(1, len(lines)):
# dedent the line
line = lines[i][margin:]
# if the line after dedent was not empty, prepend our indent
if line:
line = doc_indent + line
lines[i] = line
if lines[-1] == "":
# if the last line was empty, indent it so the triple end quote is in a good spot.
lines[-1] = doc_indent + lines[-1]
return "\n".join(lines)

def _fix_iter(
self, ctx: FunctionContext, inferred: list[FunctionSig], output: list[str]
Expand Down Expand Up @@ -809,6 +825,7 @@ def generate_class_stub(self, class_name: str, cls: type, output: list[str]) ->
self.indent()

class_info = ClassInfo(class_name, "", getattr(cls, "__doc__", None), cls)
docstring = class_info.docstring if self._include_docstrings else None

for attr, value in items:
# use unevaluated descriptors when dealing with property inspection
Expand Down Expand Up @@ -857,13 +874,18 @@ def generate_class_stub(self, class_name: str, cls: type, output: list[str]) ->

self.dedent()

if docstring:
docstring = self._indent_docstring(docstring)

bases = self.get_base_types(cls)
if bases:
bases_str = "(%s)" % ", ".join(bases)
else:
bases_str = ""
if types or static_properties or rw_properties or methods or ro_properties:
if types or static_properties or rw_properties or methods or ro_properties or docstring:
output.append(f"{self._indent}class {class_name}{bases_str}:")
if docstring:
output.append(f"{self._indent} {mypy.util.quote_docstring(docstring)}")
for line in types:
if (
output
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,17 @@ class TestStruct:
def field_readonly(self) -> int: ...

def func_incomplete_signature(*args, **kwargs):
"""func_incomplete_signature() -> dummy_sub_namespace::HasNoBinding"""
"""func_incomplete_signature() -> dummy_sub_namespace::HasNoBinding
"""
def func_returning_optional() -> int | None:
"""func_returning_optional() -> Optional[int]"""
"""func_returning_optional() -> Optional[int]
"""
def func_returning_pair() -> tuple[int, float]:
"""func_returning_pair() -> Tuple[int, float]"""
"""func_returning_pair() -> Tuple[int, float]
"""
def func_returning_path() -> os.PathLike:
"""func_returning_path() -> os.PathLike"""
"""func_returning_path() -> os.PathLike
"""
def func_returning_vector() -> list[float]:
"""func_returning_vector() -> List[float]"""
"""func_returning_vector() -> List[float]
"""
Original file line number Diff line number Diff line change
Expand Up @@ -5,45 +5,73 @@ __version__: str

class Point:
class AngleUnit:
"""Describes the angle measurement units.

Members:

radian

degree"""
__members__: ClassVar[dict] = ... # read-only
__entries: ClassVar[dict] = ...
degree: ClassVar[Point.AngleUnit] = ...
radian: ClassVar[Point.AngleUnit] = ...
def __init__(self, value: int) -> None:
"""__init__(self: pybind11_fixtures.demo.Point.AngleUnit, value: int) -> None"""
"""__init__(self: pybind11_fixtures.demo.Point.AngleUnit, value: int) -> None
"""
def __eq__(self, other: object) -> bool:
"""__eq__(self: object, other: object) -> bool"""
"""__eq__(self: object, other: object) -> bool
"""
def __hash__(self) -> int:
"""__hash__(self: object) -> int"""
"""__hash__(self: object) -> int
"""
def __index__(self) -> int:
"""__index__(self: pybind11_fixtures.demo.Point.AngleUnit) -> int"""
"""__index__(self: pybind11_fixtures.demo.Point.AngleUnit) -> int
"""
def __int__(self) -> int:
"""__int__(self: pybind11_fixtures.demo.Point.AngleUnit) -> int"""
"""__int__(self: pybind11_fixtures.demo.Point.AngleUnit) -> int
"""
def __ne__(self, other: object) -> bool:
"""__ne__(self: object, other: object) -> bool"""
"""__ne__(self: object, other: object) -> bool
"""
@property
def name(self) -> str: ...
@property
def value(self) -> int: ...

class LengthUnit:
"""Describes the length measurement units.

Members:

mm

pixel

inch"""
__members__: ClassVar[dict] = ... # read-only
__entries: ClassVar[dict] = ...
inch: ClassVar[Point.LengthUnit] = ...
mm: ClassVar[Point.LengthUnit] = ...
pixel: ClassVar[Point.LengthUnit] = ...
def __init__(self, value: int) -> None:
"""__init__(self: pybind11_fixtures.demo.Point.LengthUnit, value: int) -> None"""
"""__init__(self: pybind11_fixtures.demo.Point.LengthUnit, value: int) -> None
"""
def __eq__(self, other: object) -> bool:
"""__eq__(self: object, other: object) -> bool"""
"""__eq__(self: object, other: object) -> bool
"""
def __hash__(self) -> int:
"""__hash__(self: object) -> int"""
"""__hash__(self: object) -> int
"""
def __index__(self) -> int:
"""__index__(self: pybind11_fixtures.demo.Point.LengthUnit) -> int"""
"""__index__(self: pybind11_fixtures.demo.Point.LengthUnit) -> int
"""
def __int__(self) -> int:
"""__int__(self: pybind11_fixtures.demo.Point.LengthUnit) -> int"""
"""__int__(self: pybind11_fixtures.demo.Point.LengthUnit) -> int
"""
def __ne__(self, other: object) -> bool:
"""__ne__(self: object, other: object) -> bool"""
"""__ne__(self: object, other: object) -> bool
"""
@property
def name(self) -> str: ...
@property
Expand Down Expand Up @@ -74,7 +102,8 @@ class Point:
2. __init__(self: pybind11_fixtures.demo.Point, x: float, y: float) -> None
"""
def as_list(self) -> list[float]:
"""as_list(self: pybind11_fixtures.demo.Point) -> List[float]"""
"""as_list(self: pybind11_fixtures.demo.Point) -> List[float]
"""
@overload
def distance_to(self, x: float, y: float) -> float:
"""distance_to(*args, **kwargs)
Expand Down Expand Up @@ -102,11 +131,13 @@ def answer() -> int:
answer docstring, with end quote"
'''
def midpoint(left: float, right: float) -> float:
"""midpoint(left: float, right: float) -> float"""
"""midpoint(left: float, right: float) -> float
"""
def sum(arg0: int, arg1: int) -> int:
'''sum(arg0: int, arg1: int) -> int

multiline docstring test, edge case quotes """\'\'\'
'''
def weighted_midpoint(left: float, right: float, alpha: float = ...) -> float:
"""weighted_midpoint(left: float, right: float, alpha: float = 0.5) -> float"""
"""weighted_midpoint(left: float, right: float, alpha: float = 0.5) -> float
"""
4 changes: 2 additions & 2 deletions test-data/pybind11_fixtures/src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -228,8 +228,8 @@ void bind_demo(py::module& m) {

// Classes
py::class_<Point> pyPoint(m, "Point");
py::enum_<Point::LengthUnit> pyLengthUnit(pyPoint, "LengthUnit");
py::enum_<Point::AngleUnit> pyAngleUnit(pyPoint, "AngleUnit");
py::enum_<Point::LengthUnit> pyLengthUnit(pyPoint, "LengthUnit", "Describes the length measurement units.");
py::enum_<Point::AngleUnit> pyAngleUnit(pyPoint, "AngleUnit", "Describes the angle measurement units.");

pyPoint
.def(py::init<>())
Expand Down
55 changes: 55 additions & 0 deletions test-data/unit/stubgen.test
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,7 @@ class A: ...
[out]
class A: ...


[case testSkipPrivateFunction]
def _f(): ...
def g(): ...
Expand Down Expand Up @@ -3640,6 +3641,60 @@ class B:
def quoteD() -> None:
'''raw with quotes\\"'''

[case testIncludeDocstringsInspectMode-xfail]
# flags: --include-docstrings --inspect-mode

# TODO: --inspect mode doesn't explicitly match the functionality when writing the function call signatures.
class A:
"""class docstring

a multiline 😊 docstring"""
def func():
"""func docstring
don't forget to indent"""
...
def nodoc():
...
class B:
def quoteA():
'''func docstring with quotes"""\\n
and an end quote\''''
...
def quoteB():
'''func docstring with quotes"""
\'\'\'
and an end quote\\"'''
...
def quoteC():
"""func docstring with end quote\\\""""
...
def quoteD():
r'''raw with quotes\"'''
...
[out]
class A:
"""class docstring

a multiline 😊 docstring"""
def func() -> None:
"""func docstring
don't forget to indent"""
def nodoc() -> None: ...

class B:
def quoteA() -> None:
'''func docstring with quotes"""\\n
and an end quote\''''
def quoteB() -> None:
'''func docstring with quotes"""
\'\'\'
and an end quote\\"'''
def quoteC() -> None:
'''func docstring with end quote\\"'''
def quoteD() -> None:
'''raw with quotes\\"'''


[case testIgnoreDocstrings]
class A:
"""class docstring
Expand Down
Loading