Skip to content

Commit 8e7b7cd

Browse files
committed
Generate introspection signatures for classes too
1 parent f8689e9 commit 8e7b7cd

File tree

3 files changed

+134
-77
lines changed

3 files changed

+134
-77
lines changed

mypyc/codegen/emitclass.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from collections.abc import Mapping
66
from typing import Callable
77

8+
from mypyc.codegen.cstring import c_string_initializer
89
from mypyc.codegen.emit import Emitter, HeaderDeclaration, ReturnHandler
910
from mypyc.codegen.emitfunc import native_function_doc_initializer, native_function_header
1011
from mypyc.codegen.emitwrapper import (
@@ -21,7 +22,13 @@
2122
)
2223
from mypyc.common import BITMAP_BITS, BITMAP_TYPE, NATIVE_PREFIX, PREFIX, REG_PREFIX
2324
from mypyc.ir.class_ir import ClassIR, VTableEntries
24-
from mypyc.ir.func_ir import FUNC_CLASSMETHOD, FUNC_STATICMETHOD, FuncDecl, FuncIR
25+
from mypyc.ir.func_ir import (
26+
FUNC_CLASSMETHOD,
27+
FUNC_STATICMETHOD,
28+
FuncDecl,
29+
FuncIR,
30+
get_text_signature,
31+
)
2532
from mypyc.ir.rtypes import RTuple, RType, object_rprimitive
2633
from mypyc.namegen import NameGenerator
2734
from mypyc.sametype import is_same_type
@@ -345,6 +352,8 @@ def emit_line() -> None:
345352
flags.append("Py_TPFLAGS_MANAGED_DICT")
346353
fields["tp_flags"] = " | ".join(flags)
347354

355+
fields["tp_doc"] = native_class_doc_initializer(cl)
356+
348357
emitter.emit_line(f"static PyTypeObject {emitter.type_struct_name(cl)}_template_ = {{")
349358
emitter.emit_line("PyVarObject_HEAD_INIT(NULL, 0)")
350359
for field, value in fields.items():
@@ -1100,3 +1109,16 @@ def has_managed_dict(cl: ClassIR, emitter: Emitter) -> bool:
11001109
and cl.has_dict
11011110
and cl.builtin_base != "PyBaseExceptionObject"
11021111
)
1112+
1113+
1114+
def native_class_doc_initializer(cl: ClassIR) -> str:
1115+
init_fn = cl.get_method("__init__")
1116+
if init_fn is not None:
1117+
text_sig = get_text_signature(init_fn, bound=True)
1118+
if text_sig is None:
1119+
return "NULL"
1120+
text_sig = text_sig.replace("__init__", cl.name, 1)
1121+
else:
1122+
text_sig = f"{cl.name}()"
1123+
docstring = f"{text_sig}\n--\n\n"
1124+
return c_string_initializer(docstring.encode("ascii", errors="backslashreplace"))

mypyc/ir/func_ir.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -406,20 +406,21 @@ def all_values_full(args: list[Register], blocks: list[BasicBlock]) -> list[Valu
406406
_NOT_REPRESENTABLE = object()
407407

408408

409-
def get_text_signature(fn: FuncIR) -> str | None:
409+
def get_text_signature(fn: FuncIR, *, bound: bool = False) -> str | None:
410410
"""Return a text signature in CPython's internal doc format, or None
411411
if the function's signature cannot be represented.
412412
"""
413413
parameters = []
414-
mark_self = fn.class_name is not None and fn.decl.kind != FUNC_STATICMETHOD
414+
mark_self = fn.class_name is not None and fn.decl.kind != FUNC_STATICMETHOD and not bound
415415
# Pre-scan for end of positional-only parameters.
416416
# This is needed to handle signatures like 'def foo(self, __x)', where mypy
417417
# currently sees 'self' as being positional-or-keyword and '__x' as positional-only.
418418
pos_only_idx = -1
419-
for idx, arg in enumerate(fn.decl.sig.args):
419+
sig = fn.decl.bound_sig if bound and fn.decl.bound_sig is not None else fn.decl.sig
420+
for idx, arg in enumerate(sig.args):
420421
if arg.pos_only and arg.kind in (ArgKind.ARG_POS, ArgKind.ARG_OPT):
421422
pos_only_idx = idx
422-
for idx, arg in enumerate(fn.decl.sig.args):
423+
for idx, arg in enumerate(sig.args):
423424
if arg.name.startswith("__bitmap") or arg.name == "__mypyc_self__":
424425
continue
425426
kind = (
@@ -440,8 +441,7 @@ def get_text_signature(fn: FuncIR) -> str | None:
440441
# Parameter.__init__/Parameter.replace do not accept $
441442
curr_param._name = f"${arg.name}" # type: ignore[attr-defined]
442443
mark_self = False
443-
sig = inspect.Signature(parameters)
444-
return f"{fn.name}{sig}"
444+
return f"{fn.name}{inspect.Signature(parameters)}"
445445

446446

447447
def _find_default_argument(name: str, blocks: list[BasicBlock]) -> object:

mypyc/test-data/run-signatures.test

Lines changed: 105 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
[case testSignaturesBasic]
2-
import inspect
3-
42
def f1(): pass
53
def f2(x): pass
64
def f3(x, /): pass
@@ -12,21 +10,25 @@ def f8(x=None, /): pass
1210
def f9(*, x=None): pass
1311
def f10(a, /, b, c=None, *args, d=None, **h): pass
1412

15-
def test_basic() -> None:
16-
assert str(inspect.signature(f1)) == "()"
17-
assert str(inspect.signature(f2)) == "(x)"
18-
assert str(inspect.signature(f3)) == "(x, /)"
19-
assert str(inspect.signature(f4)) == "(*, x)"
20-
assert str(inspect.signature(f5)) == "(*x)"
21-
assert str(inspect.signature(f6)) == "(**x)"
22-
assert str(inspect.signature(f7)) == "(x=None)"
23-
assert str(inspect.signature(f8)) == "(x=None, /)"
24-
assert str(inspect.signature(f9)) == "(*, x=None)"
25-
assert str(inspect.signature(f10)) == "(a, /, b, c=None, *args, d=None, **h)"
26-
27-
[case testSignaturesValidDefaults]
13+
[file driver.py]
2814
import inspect
15+
from native import *
16+
17+
assert str(inspect.signature(f1)) == "()"
18+
assert str(inspect.signature(f2)) == "(x)"
19+
assert str(inspect.signature(f3)) == "(x, /)"
20+
assert str(inspect.signature(f4)) == "(*, x)"
21+
assert str(inspect.signature(f5)) == "(*x)"
22+
assert str(inspect.signature(f6)) == "(**x)"
23+
assert str(inspect.signature(f7)) == "(x=None)"
24+
assert str(inspect.signature(f8)) == "(x=None, /)"
25+
assert str(inspect.signature(f9)) == "(*, x=None)"
26+
assert str(inspect.signature(f10)) == "(a, /, b, c=None, *args, d=None, **h)"
27+
28+
for fn in [f1, f2, f3, f4, f5, f6, f7, f8, f9, f10]:
29+
assert getattr(fn, "__doc__") is None
2930

31+
[case testSignaturesValidDefaults]
3032
def default_int(x=1): pass
3133
def default_str(x="a"): pass
3234
def default_float(x=1.0): pass
@@ -37,44 +39,43 @@ def default_tuple_empty(x=()): pass
3739
def default_tuple_literals(x=(1, "a", 1.0, False, True, None, (), (1,2,(3,4)))): pass
3840
def default_tuple_singleton(x=(1,)): pass
3941

40-
def test_valid_defaults() -> None:
41-
assert str(inspect.signature(default_int)) == "(x=1)"
42-
assert str(inspect.signature(default_str)) == "(x='a')"
43-
assert str(inspect.signature(default_float)) == "(x=1.0)"
44-
assert str(inspect.signature(default_true)) == "(x=True)"
45-
assert str(inspect.signature(default_false)) == "(x=False)"
46-
assert str(inspect.signature(default_none)) == "(x=None)"
47-
assert str(inspect.signature(default_tuple_empty)) == "(x=())"
48-
assert str(inspect.signature(default_tuple_literals)) == "(x=(1, 'a', 1.0, False, True, None, (), (1, 2, (3, 4))))"
49-
50-
# Check __text_signature__ directly since inspect.signature produces
51-
# an incorrect signature for 1-tuple default arguments prior to
52-
# Python 3.12 (cpython#102379).
53-
# assert str(inspect.signature(default_tuple_singleton)) == "(x=(1,))"
54-
assert getattr(default_tuple_singleton, "__text_signature__") == "(x=(1,))"
55-
56-
[case testSignaturesStringDefaults]
42+
[file driver.py]
5743
import inspect
44+
from native import *
45+
46+
assert str(inspect.signature(default_int)) == "(x=1)"
47+
assert str(inspect.signature(default_str)) == "(x='a')"
48+
assert str(inspect.signature(default_float)) == "(x=1.0)"
49+
assert str(inspect.signature(default_true)) == "(x=True)"
50+
assert str(inspect.signature(default_false)) == "(x=False)"
51+
assert str(inspect.signature(default_none)) == "(x=None)"
52+
assert str(inspect.signature(default_tuple_empty)) == "(x=())"
53+
assert str(inspect.signature(default_tuple_literals)) == "(x=(1, 'a', 1.0, False, True, None, (), (1, 2, (3, 4))))"
54+
55+
# Check __text_signature__ directly since inspect.signature produces
56+
# an incorrect signature for 1-tuple default arguments prior to
57+
# Python 3.12 (cpython#102379).
58+
# assert str(inspect.signature(default_tuple_singleton)) == "(x=(1,))"
59+
assert getattr(default_tuple_singleton, "__text_signature__") == "(x=(1,))"
5860

61+
[case testSignaturesStringDefaults]
5962
def f1(x="'foo"): pass
6063
def f2(x='"foo'): pass
6164
def f3(x=""""Isn\'t," they said."""): pass
6265
def f4(x="\\ \a \b \f \n \r \t \v \x00"): pass
6366
def f5(x="\N{BANANA}sv"): pass
6467

65-
def test_string_defaults() -> None:
66-
assert str(inspect.signature(f1)) == """(x="'foo")"""
67-
assert str(inspect.signature(f2)) == """(x='"foo')"""
68-
assert str(inspect.signature(f3)) == r"""(x='"Isn\'t," they said.')"""
69-
assert str(inspect.signature(f4)) == r"""(x='\\ \x07 \x08 \x0c \n \r \t \x0b \x00')"""
70-
assert str(inspect.signature(f5)) == """(x='\U0001F34Csv')"""
71-
72-
[case testSignaturesIrrepresentableDefaults]
68+
[file driver.py]
7369
import inspect
74-
from typing import Any
70+
from native import *
7571

76-
from testutil import assertRaises
72+
assert str(inspect.signature(f1)) == """(x="'foo")"""
73+
assert str(inspect.signature(f2)) == """(x='"foo')"""
74+
assert str(inspect.signature(f3)) == r"""(x='"Isn\'t," they said.')"""
75+
assert str(inspect.signature(f4)) == r"""(x='\\ \x07 \x08 \x0c \n \r \t \x0b \x00')"""
76+
assert str(inspect.signature(f5)) == """(x='\N{BANANA}sv')"""
7777

78+
[case testSignaturesIrrepresentableDefaults]
7879
def bad1(x=[]): pass
7980
def bad2(x={}): pass
8081
def bad3(x=set()): pass
@@ -88,16 +89,17 @@ def bad9(x=1|2): pass
8889
def bad10(x=float("nan")): pass
8990
def bad11(x=([],)): pass
9091

91-
def test_irrepresentable_defaults() -> None:
92-
bad: Any
93-
for bad in [bad1, bad2, bad3, bad4, bad5, bad6, bad7, bad8, bad9, bad10, bad11]:
94-
assert bad.__text_signature__ is None, f"{bad.__name__} has unexpected __text_signature__"
95-
with assertRaises(ValueError, "no signature found for builtin"):
96-
inspect.signature(bad)
97-
98-
[case testSignaturesMethods]
92+
[file driver.py]
9993
import inspect
94+
from testutil import assertRaises
95+
from native import *
10096

97+
for bad in [bad1, bad2, bad3, bad4, bad5, bad6, bad7, bad8, bad9, bad10, bad11]:
98+
assert bad.__text_signature__ is None, f"{bad.__name__} has unexpected __text_signature__"
99+
with assertRaises(ValueError, "no signature found for builtin"):
100+
inspect.signature(bad)
101+
102+
[case testSignaturesMethods]
101103
class Foo:
102104
def f1(self, x): pass
103105
@classmethod
@@ -106,26 +108,59 @@ class Foo:
106108
def f3(x): pass
107109
def __eq__(self, x: object): pass
108110

109-
def test_methods() -> None:
110-
assert getattr(Foo.f1, "__text_signature__") == "($self, x)"
111-
assert getattr(Foo().f1, "__text_signature__") == "($self, x)"
112-
assert str(inspect.signature(Foo.f1)) == "(self, /, x)"
113-
assert str(inspect.signature(Foo().f1)) == "(x)"
114-
115-
assert getattr(Foo.f2, "__text_signature__") == "($cls, x)"
116-
assert getattr(Foo().f2, "__text_signature__") == "($cls, x)"
117-
assert str(inspect.signature(Foo.f2)) == "(x)"
118-
assert str(inspect.signature(Foo().f2)) == "(x)"
119-
120-
assert getattr(Foo.f3, "__text_signature__") == "(x)"
121-
assert getattr(Foo().f3, "__text_signature__") == "(x)"
122-
assert str(inspect.signature(Foo.f3)) == "(x)"
123-
assert str(inspect.signature(Foo().f3)) == "(x)"
124-
125-
assert getattr(Foo.__eq__, "__text_signature__") == "($self, value, /)"
126-
assert getattr(Foo().__eq__, "__text_signature__") == "($self, value, /)"
127-
assert str(inspect.signature(Foo.__eq__)) == "(self, value, /)"
128-
assert str(inspect.signature(Foo().__eq__)) == "(value, /)"
111+
[file driver.py]
112+
import inspect
113+
from native import *
114+
115+
assert str(inspect.signature(Foo.f1)) == "(self, /, x)"
116+
assert str(inspect.signature(Foo().f1)) == "(x)"
117+
118+
assert str(inspect.signature(Foo.f2)) == "(x)"
119+
assert str(inspect.signature(Foo().f2)) == "(x)"
120+
121+
assert str(inspect.signature(Foo.f3)) == "(x)"
122+
assert str(inspect.signature(Foo().f3)) == "(x)"
123+
124+
assert str(inspect.signature(Foo.__eq__)) == "(self, value, /)"
125+
assert str(inspect.signature(Foo().__eq__)) == "(value, /)"
126+
127+
[case testSignaturesConstructors]
128+
class Empty: pass
129+
130+
class HasInit:
131+
def __init__(self, x) -> None: pass
132+
133+
class InheritedInit(HasInit): pass
134+
135+
class HasInitBad:
136+
def __init__(self, x=[]) -> None: pass
137+
138+
[file driver.py]
139+
import inspect
140+
from testutil import assertRaises
141+
from native import *
142+
143+
assert str(inspect.signature(Empty)) == "()"
144+
assert str(inspect.signature(Empty.__init__)) == "(self, /, *args, **kwargs)"
145+
146+
assert str(inspect.signature(HasInit)) == "(x)"
147+
assert str(inspect.signature(HasInit.__init__)) == "(self, /, *args, **kwargs)"
148+
149+
assert str(inspect.signature(InheritedInit)) == "(x)"
150+
assert str(inspect.signature(InheritedInit.__init__)) == "(self, /, *args, **kwargs)"
151+
152+
assert getattr(HasInitBad, "__text_signature__") is None
153+
with assertRaises(ValueError, "no signature found for builtin"):
154+
inspect.signature(HasInitBad)
155+
156+
# CPython detail note: type objects whose tp_doc contains only a text signature behave
157+
# differently from method objects whose ml_doc contains only a test signature: type
158+
# objects will have __doc__="" whereas method objects will have __doc__=None. This
159+
# difference stems from the former using _PyType_GetDocFromInternalDoc(...) and the
160+
# latter using PyUnicode_FromString(_PyType_DocWithoutSignature(...)).
161+
for cls in [Empty, HasInit, InheritedInit]:
162+
assert getattr(cls, "__doc__") == ""
163+
assert getattr(HasInitBad, "__doc__") is None
129164

130165
[case testSignaturesHistoricalPositionalOnly]
131166
import inspect

0 commit comments

Comments
 (0)