Skip to content

Commit 646115f

Browse files
committed
[GR-75636] Support text signatures for C extensions
1 parent 4179f73 commit 646115f

10 files changed

Lines changed: 212 additions & 17 deletions

File tree

graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_functions.py

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,14 @@
4141
import sys
4242
import unittest
4343

44-
from . import CPyExtType, CPyExtTestCase, CPyExtFunction, unhandled_error_compare, CPyExtHeapType
44+
from . import (
45+
CPyExtFunction,
46+
CPyExtHeapType,
47+
CPyExtTestCase,
48+
CPyExtType,
49+
compile_module_from_string,
50+
unhandled_error_compare,
51+
)
4552

4653
DIR = os.path.dirname(__file__)
4754

@@ -563,6 +570,73 @@ def _ref_hash_not_implemented(args):
563570
# test calling m_meth
564571

565572
class TestPyCFunction(unittest.TestCase):
573+
def test_docstring_text_signature(self):
574+
module = compile_module_from_string(r"""
575+
#define PY_SSIZE_T_CLEAN
576+
#include <Python.h>
577+
578+
static PyObject *with_signature(PyObject *self, PyObject *arg) {
579+
Py_RETURN_NONE;
580+
}
581+
582+
static PyObject *without_signature(PyObject *self, PyObject *arg) {
583+
Py_RETURN_NONE;
584+
}
585+
586+
static PyObject *without_doc(PyObject *self, PyObject *unused) {
587+
Py_RETURN_NONE;
588+
}
589+
590+
static PyMethodDef module_methods[] = {
591+
{"with_signature", with_signature, METH_O,
592+
"with_signature($module, value, /)\n"
593+
"--\n\n"
594+
"Return module function metadata."},
595+
{"without_signature", without_signature, METH_O, "Return a plain docstring."},
596+
{"without_doc", without_doc, METH_NOARGS, NULL},
597+
{NULL, NULL, 0, NULL}
598+
};
599+
600+
static PyModuleDef module_def = {
601+
PyModuleDef_HEAD_INIT,
602+
"textsignature",
603+
"",
604+
-1,
605+
module_methods,
606+
NULL, NULL, NULL, NULL
607+
};
608+
609+
PyMODINIT_FUNC
610+
PyInit_textsignature(void)
611+
{
612+
return PyModule_Create(&module_def);
613+
}
614+
""", "textsignature")
615+
616+
assert module.with_signature.__doc__ == "Return module function metadata."
617+
assert module.with_signature.__text_signature__ == "($module, value, /)"
618+
assert module.without_signature.__doc__ == "Return a plain docstring."
619+
assert module.without_signature.__text_signature__ is None
620+
assert module.without_doc.__doc__ is None
621+
assert module.without_doc.__text_signature__ is None
622+
623+
TypeWithTextSignature = CPyExtType(
624+
"TypeWithTextSignature",
625+
"""
626+
static PyObject *method_with_signature(PyObject *self, PyObject *arg) {
627+
Py_RETURN_NONE;
628+
}
629+
""",
630+
tp_methods="""
631+
{"method_with_signature", method_with_signature, METH_O,
632+
"method_with_signature($self, value, /)\\n"
633+
"--\\n\\n"
634+
"Return type method metadata."}
635+
""",
636+
)
637+
assert TypeWithTextSignature.method_with_signature.__doc__ == "Return type method metadata."
638+
assert TypeWithTextSignature.method_with_signature.__text_signature__ == "($self, value, /)"
639+
566640
test_PyCFunction_NewEx_non_string_module = CPyExtFunction(
567641
lambda args: 1,
568642
lambda: (
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
/*
2+
* Copyright (c) 2026, 2026, Oracle and/or its affiliates. All rights reserved.
3+
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4+
*
5+
* The Universal Permissive License (UPL), Version 1.0
6+
*
7+
* Subject to the condition set forth below, permission is hereby granted to any
8+
* person obtaining a copy of this software, associated documentation and/or
9+
* data (collectively the "Software"), free of charge and under any and all
10+
* copyright rights in the Software, and any and all patent rights owned or
11+
* freely licensable by each licensor hereunder covering either (i) the
12+
* unmodified Software as contributed to or provided by such licensor, or (ii)
13+
* the Larger Works (as defined below), to deal in both
14+
*
15+
* (a) the Software, and
16+
*
17+
* (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if
18+
* one is included with the Software each a "Larger Work" to which the Software
19+
* is contributed by such licensors),
20+
*
21+
* without restriction, including without limitation the rights to copy, create
22+
* derivative works of, display, perform, and distribute the Software and make,
23+
* use, sell, offer for sale, import, export, have made, and have sold the
24+
* Software and the Larger Work(s), and to sublicense the foregoing rights on
25+
* either these or other terms.
26+
*
27+
* This license is subject to the following condition:
28+
*
29+
* The above copyright notice and either this complete permission notice or at a
30+
* minimum a reference to the UPL must be included in all copies or substantial
31+
* portions of the Software.
32+
*
33+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
34+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
35+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
36+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
37+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
38+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
39+
* SOFTWARE.
40+
*/
41+
package com.oracle.graal.python.builtins.modules.cext;
42+
43+
import static com.oracle.graal.python.nodes.SpecialAttributeNames.T___DOC__;
44+
import static com.oracle.graal.python.nodes.SpecialAttributeNames.T___TEXT_SIGNATURE__;
45+
import static com.oracle.graal.python.util.PythonUtils.toTruffleStringUncached;
46+
47+
import com.oracle.graal.python.builtins.objects.PNone;
48+
import com.oracle.graal.python.builtins.objects.function.PBuiltinFunction;
49+
import com.oracle.graal.python.nodes.attributes.WriteAttributeToPythonObjectNode;
50+
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
51+
import com.oracle.truffle.api.strings.TruffleString;
52+
53+
public final class CFunctionDocUtils {
54+
55+
private static final String SIGNATURE_END_MARKER = ")\n--\n\n";
56+
private static final int SIGNATURE_END_MARKER_LENGTH = SIGNATURE_END_MARKER.length();
57+
58+
private CFunctionDocUtils() {
59+
}
60+
61+
@TruffleBoundary
62+
public static void writeDocAndTextSignature(PBuiltinFunction function, TruffleString name, Object docObj) {
63+
Object doc = PNone.NONE;
64+
Object textSignature = PNone.NONE;
65+
if (docObj instanceof TruffleString) {
66+
TruffleString docTruffleString = (TruffleString) docObj;
67+
String docString = docTruffleString.toJavaStringUncached();
68+
int start = findSignature(name.toJavaStringUncached(), docString);
69+
int end = start >= 0 ? skipSignature(docString, start) : -1;
70+
if (end < 0) {
71+
doc = docString.isEmpty() ? PNone.NONE : docTruffleString;
72+
} else {
73+
int textSignatureEnd = end - SIGNATURE_END_MARKER_LENGTH + 1;
74+
doc = end == docString.length() ? PNone.NONE : toTruffleStringUncached(docString.substring(end));
75+
textSignature = toTruffleStringUncached(docString.substring(start, textSignatureEnd));
76+
}
77+
}
78+
WriteAttributeToPythonObjectNode.executeUncached(function, T___DOC__, doc);
79+
WriteAttributeToPythonObjectNode.executeUncached(function, T___TEXT_SIGNATURE__, textSignature);
80+
}
81+
82+
/*
83+
* Matches CPython's find_signature: the internal doc must start with the callable name followed
84+
* by the first '(' of the signature.
85+
*/
86+
private static int findSignature(String name, String doc) {
87+
int dot = name.lastIndexOf('.');
88+
if (dot != -1) {
89+
name = name.substring(dot + 1);
90+
}
91+
int length = name.length();
92+
if (!doc.startsWith(name) || doc.length() <= length || doc.charAt(length) != '(') {
93+
return -1;
94+
}
95+
return length;
96+
}
97+
98+
/*
99+
* Matches CPython's skip_signature: a blank line before the marker invalidates the signature.
100+
*/
101+
private static int skipSignature(String doc, int start) {
102+
for (int i = start; i < doc.length(); i++) {
103+
if (doc.startsWith(SIGNATURE_END_MARKER, i)) {
104+
return i + SIGNATURE_END_MARKER_LENGTH;
105+
}
106+
if (doc.charAt(i) == '\n' && i + 1 < doc.length() && doc.charAt(i + 1) == '\n') {
107+
return -1;
108+
}
109+
}
110+
return -1;
111+
}
112+
}

graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextAbstractBuiltins.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -793,12 +793,12 @@ static int PyObject_SetDoc(long objPtr, long valuePtr) {
793793
Object obj = NativeToPythonNode.executeRawUncached(objPtr);
794794
Object value = CharPtrToPythonNode.getUncached().execute(valuePtr);
795795
if (obj instanceof PBuiltinFunction builtinFunction) {
796-
WriteAttributeToPythonObjectNode.executeUncached(builtinFunction, T___DOC__, value);
796+
CFunctionDocUtils.writeDocAndTextSignature(builtinFunction, builtinFunction.getName(), value);
797797
return 1;
798798
}
799799
if (obj instanceof PBuiltinMethod builtinMethod) {
800800
PBuiltinFunction builtinFunction = builtinMethod.getBuiltinFunction();
801-
WriteAttributeToPythonObjectNode.executeUncached(builtinFunction, T___DOC__, value);
801+
CFunctionDocUtils.writeDocAndTextSignature(builtinFunction, builtinFunction.getName(), value);
802802
return 1;
803803
}
804804
if (obj instanceof GetSetDescriptor descriptor) {

graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextDescrBuiltins.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,6 @@
4949
import static com.oracle.graal.python.builtins.objects.cext.capi.transitions.ArgDescriptor.PyObjectTransfer;
5050
import static com.oracle.graal.python.builtins.objects.cext.capi.transitions.ArgDescriptor.PyTypeObject;
5151
import static com.oracle.graal.python.nodes.HiddenAttr.METHOD_DEF_PTR;
52-
import static com.oracle.graal.python.nodes.SpecialAttributeNames.T___DOC__;
5352
import static com.oracle.graal.python.nodes.SpecialAttributeNames.T___NAME__;
5453

5554
import com.oracle.graal.python.PythonLanguage;
@@ -104,9 +103,10 @@ public static long GraalPyPrivate_Descr_NewClassMethod(long methodDefPtr, long n
104103
Object type = NativeToPythonClassInternalNode.executeUncached(typeRaw);
105104
PBuiltinFunction func = MethodDescriptorWrapper.createWrapperFunction(language, name, methPtr, type, flags);
106105
assert func != null;
106+
WriteAttributeToPythonObjectNode.executeUncached(func, T___NAME__, name);
107+
CFunctionDocUtils.writeDocAndTextSignature(func, name, doc);
107108
PDecoratedMethod classMethod = PFactory.createBuiltinClassmethodFromCallableObj(language, func);
108109
WriteAttributeToPythonObjectNode.executeUncached(classMethod, T___NAME__, name);
109-
WriteAttributeToPythonObjectNode.executeUncached(classMethod, T___DOC__, doc);
110110
HiddenAttr.WriteLongNode.executeUncached(classMethod, METHOD_DEF_PTR, methodDefPtr);
111111
return PythonToNativeInternalNode.executeUncached(classMethod, true);
112112
}

graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextFuncBuiltins.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2022, 2025, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2022, 2026, Oracle and/or its affiliates. All rights reserved.
33
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
*
55
* The Universal Permissive License (UPL), Version 1.0
@@ -45,7 +45,6 @@
4545
import static com.oracle.graal.python.builtins.objects.cext.capi.transitions.ArgDescriptor.ConstCharPtrAsTruffleString;
4646
import static com.oracle.graal.python.builtins.objects.cext.capi.transitions.ArgDescriptor.PyObject;
4747
import static com.oracle.graal.python.builtins.objects.cext.capi.transitions.ArgDescriptor.PyObjectTransfer;
48-
import static com.oracle.graal.python.nodes.SpecialAttributeNames.T___DOC__;
4948

5049
import com.oracle.graal.python.PythonLanguage;
5150
import com.oracle.graal.python.builtins.modules.cext.PythonCextBuiltins.CApiBinaryBuiltinNode;
@@ -108,7 +107,8 @@ private static PNone setDoc(Object functionObj, TruffleString doc) {
108107
} else {
109108
throw CompilerDirectives.shouldNotReachHere("Unexpected object passed to GraalPyCFunction_SetDoc");
110109
}
111-
function.setAttribute(T___DOC__, doc != null ? doc : PNone.NONE);
110+
CFunctionDocUtils.writeDocAndTextSignature(function, function.getName(),
111+
doc != null ? doc : PNone.NO_VALUE);
112112
return PNone.NO_VALUE;
113113
}
114114
}

graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextMethodBuiltins.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,6 @@
5050
import static com.oracle.graal.python.builtins.objects.cext.capi.transitions.ArgDescriptor.PyTypeObject;
5151
import static com.oracle.graal.python.builtins.objects.cext.common.CExtContext.METH_METHOD;
5252
import static com.oracle.graal.python.runtime.nativeaccess.NativeMemory.NULLPTR;
53-
import static com.oracle.graal.python.nodes.SpecialAttributeNames.T___DOC__;
5453
import static com.oracle.graal.python.nodes.SpecialAttributeNames.T___MODULE__;
5554
import static com.oracle.graal.python.nodes.SpecialAttributeNames.T___NAME__;
5655

@@ -83,7 +82,7 @@ static PythonBuiltinObject cFunctionNewExMethodNode(PythonLanguage language, lon
8382
PBuiltinFunction func = MethodDescriptorWrapper.createWrapperFunction(language, name, methPtr, PNone.NO_VALUE, flags);
8483
HiddenAttr.WriteLongNode.executeUncached(func, METHOD_DEF_PTR, methodDefPtr);
8584
WriteAttributeToPythonObjectNode.executeUncached(func, T___NAME__, name);
86-
WriteAttributeToPythonObjectNode.executeUncached(func, T___DOC__, doc);
85+
CFunctionDocUtils.writeDocAndTextSignature(func, name, doc);
8786
PBuiltinMethod method;
8887
if (cls != PNone.NO_VALUE) {
8988
method = PFactory.createBuiltinMethod(language, self, func, cls);

graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextTypeBuiltins.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,10 @@ int trace(long ptr) {
293293
private static PythonBuiltinObject typeAddMethod(PythonLanguage language, long methodDefPtr, TruffleString name, long methPtr, int flags, Object type, Object doc) {
294294
assert doc == PNone.NO_VALUE || doc instanceof TruffleString;
295295
PBuiltinFunction func = MethodDescriptorWrapper.createWrapperFunction(language, name, methPtr, type, flags);
296+
if (func != null) {
297+
WriteAttributeToPythonObjectNode.executeUncached(func, T___NAME__, name);
298+
CFunctionDocUtils.writeDocAndTextSignature(func, name, doc);
299+
}
296300
if (CExtContext.isMethClass(flags)) {
297301
if (CExtContext.isMethStatic(flags)) {
298302
assert func == null;
@@ -303,8 +307,6 @@ private static PythonBuiltinObject typeAddMethod(PythonLanguage language, long m
303307
} else if (CExtContext.isMethStatic(flags)) {
304308
return PFactory.createStaticmethodFromCallableObj(language, func);
305309
}
306-
WriteAttributeToPythonObjectNode.executeUncached(func, T___NAME__, name);
307-
WriteAttributeToPythonObjectNode.executeUncached(func, T___DOC__, doc);
308310
HiddenAttr.WriteLongNode.executeUncached(func, METHOD_DEF_PTR, methodDefPtr);
309311
return func;
310312
}

graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/capi/CExtNodes.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@
8484

8585
import com.oracle.graal.python.PythonLanguage;
8686
import com.oracle.graal.python.builtins.PythonBuiltinClassType;
87+
import com.oracle.graal.python.builtins.modules.cext.CFunctionDocUtils;
8788
import com.oracle.graal.python.builtins.objects.PNone;
8889
import com.oracle.graal.python.builtins.objects.PythonAbstractObject;
8990
import com.oracle.graal.python.builtins.objects.bytes.PByteArray;
@@ -1199,9 +1200,7 @@ static PBuiltinFunction createLegacyMethod(long methodDefPtr, int element, Pytho
11991200
PBuiltinFunction function = PFactory.createBuiltinFunction(language, methodName, null, PythonUtils.EMPTY_OBJECT_ARRAY, kwDefaults, flags, rootNode);
12001201
HiddenAttr.WriteLongNode.executeUncached(function, METHOD_DEF_PTR, methodDefPtr);
12011202

1202-
// write doc string; we need to directly write to the storage otherwise it is disallowed
1203-
// writing to builtin types.
1204-
WriteAttributeToPythonObjectNode.executeUncached(function, SpecialAttributeNames.T___DOC__, methodDoc);
1203+
CFunctionDocUtils.writeDocAndTextSignature(function, methodName, methodDoc);
12051204

12061205
return function;
12071206
}

graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/function/AbstractFunctionBuiltins.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2017, 2025, Oracle and/or its affiliates.
2+
* Copyright (c) 2017, 2026, Oracle and/or its affiliates.
33
* Copyright (c) 2014, Regents of the University of California
44
*
55
* All rights reserved.
@@ -348,8 +348,12 @@ static Object setFunction(PFunction self, Object value,
348348

349349
@Specialization(guards = "isNoValue(none)")
350350
@TruffleBoundary
351-
static TruffleString getBuiltin(PBuiltinFunction self, @SuppressWarnings("unused") PNone none,
351+
static Object getBuiltin(PBuiltinFunction self, @SuppressWarnings("unused") PNone none,
352352
@Bind Node inliningTarget) {
353+
Object storedSignature = ReadAttributeFromObjectNode.getUncached().execute(self, T___TEXT_SIGNATURE__);
354+
if (storedSignature != PNone.NO_VALUE) {
355+
return storedSignature;
356+
}
353357
Signature signature = self.getSignature();
354358
if (signature.isHidden()) {
355359
throw PRaiseNode.raiseStatic(inliningTarget, AttributeError, ErrorMessages.HAS_NO_ATTR, self, T___TEXT_SIGNATURE__);

graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/function/PBuiltinFunction.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
package com.oracle.graal.python.builtins.objects.function;
2727

2828
import static com.oracle.graal.python.nodes.SpecialAttributeNames.T___DOC__;
29+
import static com.oracle.graal.python.nodes.SpecialAttributeNames.T___TEXT_SIGNATURE__;
2930
import static com.oracle.graal.python.nodes.StringLiterals.T_DOT;
3031

3132
import java.util.Arrays;
@@ -256,6 +257,10 @@ public PBuiltinFunction boundToObject(PythonBuiltinClassType klass, PythonLangua
256257
} else {
257258
PBuiltinFunction func = PFactory.createBuiltinFunction(language, this, klass);
258259
func.setAttribute(T___DOC__, getAttribute(T___DOC__));
260+
Object textSignature = getAttribute(T___TEXT_SIGNATURE__);
261+
if (textSignature != PNone.NO_VALUE) {
262+
func.setAttribute(T___TEXT_SIGNATURE__, textSignature);
263+
}
259264
return func;
260265
}
261266
}

0 commit comments

Comments
 (0)