Add inspect.signature (#526)

* Add inspect.signature tests

* Implement inspect.signature
This commit is contained in:
felfoldy 2026-07-06 06:00:58 +02:00 committed by GitHub
parent 5d20084db7
commit 21248a0baf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 248 additions and 0 deletions

View File

@ -11,6 +11,7 @@ extern const char kPythonLibs_dataclasses[];
extern const char kPythonLibs_datetime[]; extern const char kPythonLibs_datetime[];
extern const char kPythonLibs_functools[]; extern const char kPythonLibs_functools[];
extern const char kPythonLibs_heapq[]; extern const char kPythonLibs_heapq[];
extern const char kPythonLibs_inspect[];
extern const char kPythonLibs_long_v1[]; extern const char kPythonLibs_long_v1[];
extern const char kPythonLibs_operator[]; extern const char kPythonLibs_operator[];
extern const char kPythonLibs_typing[]; extern const char kPythonLibs_typing[];

58
python/inspect.py Normal file
View File

@ -0,0 +1,58 @@
class _empty:
pass
class Parameter:
POSITIONAL_ONLY = 0
POSITIONAL_OR_KEYWORD = 1
VAR_POSITIONAL = 2
KEYWORD_ONLY = 3
VAR_KEYWORD = 4
empty = _empty
def __init__(self, name, kind, *default):
self.name = name
self.kind = kind
# pocketpy only allows literal defaults, so use *default as sentinel
self.default = default[0] if default else _empty
def __str__(self):
res = self.name
if self.default is not _empty:
res += '=' + repr(self.default)
if self.kind == Parameter.VAR_POSITIONAL:
res = '*' + res
elif self.kind == Parameter.VAR_KEYWORD:
res = '**' + res
return res
def __repr__(self):
return '<Parameter "' + str(self) + '">'
class Signature:
empty = _empty
def __init__(self, parameters):
self.parameters = {p.name: p for p in parameters}
def __str__(self):
return '(' + ', '.join([str(p) for p in self.parameters.values()]) + ')'
def __repr__(self):
return '<Signature ' + str(self) + '>'
def _make_params(func):
return [Parameter(*entry) for entry in _signature_data(func)]
def signature(obj):
if not callable(obj):
raise TypeError(repr(obj) + ' is not a callable object')
if isinstance(obj, type):
return Signature(_make_params(obj.__init__)[1:])
if hasattr(obj, '__func__'):
return Signature(_make_params(obj.__func__)[1:])
return Signature(_make_params(obj))

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,7 @@
#include "pocketpy/pocketpy.h" #include "pocketpy/pocketpy.h"
#include "pocketpy/objects/object.h" #include "pocketpy/objects/object.h"
#include "pocketpy/interpreter/vm.h" #include "pocketpy/interpreter/vm.h"
#include "pocketpy/common/_generated.h"
static bool inspect_isgeneratorfunction(int argc, py_Ref argv) { static bool inspect_isgeneratorfunction(int argc, py_Ref argv) {
PY_CHECK_ARGC(1); PY_CHECK_ARGC(1);
@ -26,9 +27,62 @@ static bool inspect_is_user_defined_type(int argc, py_Ref argv) {
return true; return true;
} }
// Returns a tuple of (name, kind[, default]) entries.
static bool inspect__signature_data(int argc, py_Ref argv) {
PY_CHECK_ARGC(1);
if(!py_istype(argv, tp_function)) {
return ValueError("no signature found for '%t' object", argv->type);
}
Function* fn = py_touserdata(argv);
FuncDecl* decl = fn->decl;
const CodeObject* co = &decl->code;
bool has_starred_arg = decl->starred_arg != -1;
bool has_starred_kwarg = decl->starred_kwarg != -1;
int total = decl->args.length + decl->kwargs.length + (int)has_starred_arg +
(int)has_starred_kwarg;
py_TValue* items = py_newtuple(py_retval(), total);
int j = 0;
for(int i = 0; i < decl->args.length; i++) {
int32_t index = c11__getitem(int32_t, &decl->args, i);
py_Name name = c11__getitem(py_Name, &co->varnames, index);
py_TValue* entry = py_newtuple(&items[j++], 2);
py_newstr(&entry[0], py_name2str(name));
py_newint(&entry[1], 1);
}
if(has_starred_arg) {
py_Name name = c11__getitem(py_Name, &co->varnames, decl->starred_arg);
py_TValue* entry = py_newtuple(&items[j++], 2);
py_newstr(&entry[0], py_name2str(name));
py_newint(&entry[1], 2);
}
for(int i = 0; i < decl->kwargs.length; i++) {
FuncDeclKwArg kv = c11__getitem(FuncDeclKwArg, &decl->kwargs, i);
py_TValue* entry = py_newtuple(&items[j++], 3);
py_newstr(&entry[0], py_name2str(kv.key));
// defaults after *args can only be passed by keyword
py_newint(&entry[1], has_starred_arg ? 3 : 1);
entry[2] = kv.value;
}
if(has_starred_kwarg) {
py_Name name = c11__getitem(py_Name, &co->varnames, decl->starred_kwarg);
py_TValue* entry = py_newtuple(&items[j++], 2);
py_newstr(&entry[0], py_name2str(name));
py_newint(&entry[1], 4);
}
return true;
}
void pk__add_module_inspect() { void pk__add_module_inspect() {
py_Ref mod = py_newmodule("inspect"); py_Ref mod = py_newmodule("inspect");
py_bindfunc(mod, "isgeneratorfunction", inspect_isgeneratorfunction); py_bindfunc(mod, "isgeneratorfunction", inspect_isgeneratorfunction);
py_bindfunc(mod, "is_user_defined_type", inspect_is_user_defined_type); py_bindfunc(mod, "is_user_defined_type", inspect_is_user_defined_type);
py_bindfunc(mod, "_signature_data", inspect__signature_data);
if(!py_exec(kPythonLibs_inspect, "inspect.py", EXEC_MODE, mod)) {
py_printexc();
c11__abort("failed to execute inspect.py");
}
} }

View File

@ -47,3 +47,136 @@ assert not isgeneratorfunction(a.not_gen_instance)
assert isgeneratorfunction(a.gen_instance) assert isgeneratorfunction(a.gen_instance)
assert not isgeneratorfunction(A.not_gen_instance) assert not isgeneratorfunction(A.not_gen_instance)
assert isgeneratorfunction(A.gen_instance) assert isgeneratorfunction(A.gen_instance)
# ---------------- inspect.signature ----------------
from inspect import signature, Parameter
# simple positional-or-keyword parameters
def f1(a, b):
return a + b
sig = signature(f1)
assert str(sig) == '(a, b)'
params = sig.parameters
assert list(params) == ['a', 'b']
assert params['a'].name == 'a'
assert params['a'].kind == Parameter.POSITIONAL_OR_KEYWORD
assert params['a'].default is Parameter.empty
assert params['b'].kind == Parameter.POSITIONAL_OR_KEYWORD
# no parameters
def f2():
pass
sig = signature(f2)
assert str(sig) == '()'
assert len(sig.parameters) == 0
# default values
def f3(a, b=2, c='x'):
pass
sig = signature(f3)
assert str(sig) == "(a, b=2, c='x')"
params = sig.parameters
assert params['a'].default is Parameter.empty
assert params['b'].default == 2
assert params['c'].default == 'x'
# *args and **kwargs
def f4(a, *args, **kwargs):
pass
sig = signature(f4)
assert str(sig) == '(a, *args, **kwargs)'
params = sig.parameters
assert list(params) == ['a', 'args', 'kwargs']
assert params['a'].kind == Parameter.POSITIONAL_OR_KEYWORD
assert params['args'].kind == Parameter.VAR_POSITIONAL
assert params['args'].default is Parameter.empty
assert params['kwargs'].kind == Parameter.VAR_KEYWORD
assert params['kwargs'].default is Parameter.empty
# keyword-only parameters (defaults after *args)
def f5(a, *c, b=1, **d):
pass
sig = signature(f5)
assert str(sig) == '(a, *c, b=1, **d)'
assert list(sig.parameters) == ['a', 'c', 'b', 'd']
assert sig.parameters['b'].kind == Parameter.KEYWORD_ONLY
assert sig.parameters['b'].default == 1
# lambda
sig = signature(lambda x, y=3: x + y)
assert str(sig) == '(x, y=3)'
assert list(sig.parameters) == ['x', 'y']
assert sig.parameters['y'].default == 3
# generator function
sig = signature(g)
assert str(sig) == '(a, b)'
# methods: bound methods drop `self`, unbound functions keep it
class B:
def m(self, x, y=1):
pass
@staticmethod
def sm(x, y):
pass
@classmethod
def cm(cls, x):
pass
b = B()
assert str(signature(B.m)) == '(self, x, y=1)'
assert str(signature(b.m)) == '(x, y=1)'
assert list(signature(b.m).parameters) == ['x', 'y']
assert str(signature(B.sm)) == '(x, y)'
assert str(signature(b.sm)) == '(x, y)'
assert str(signature(B.cm)) == '(x)'
assert str(signature(b.cm)) == '(x)'
# calling signature() on a class inspects __init__ (without `self`)
class C:
def __init__(self, a, b=5):
pass
sig = signature(C)
assert str(sig) == '(a, b=5)'
assert list(sig.parameters) == ['a', 'b']
# decorated function (plain wrapper)
def deco(fn):
def wrapper(*args, **kwargs):
return fn(*args, **kwargs)
return wrapper
@deco
def f6(a, b):
pass
assert str(signature(f6)) == '(*args, **kwargs)'
# Parameter equality / repr basics
p = signature(f3).parameters['b']
assert p.name == 'b'
assert p.default == 2
assert p.kind == Parameter.POSITIONAL_OR_KEYWORD
# kinds are distinct
kinds = [
Parameter.POSITIONAL_OR_KEYWORD,
Parameter.VAR_POSITIONAL,
Parameter.KEYWORD_ONLY,
Parameter.VAR_KEYWORD,
]
assert len(set(kinds)) == 4
# non-callable raises TypeError
try:
signature(42)
print('failed to raise TypeError')
exit(1)
except TypeError:
pass