Просмотр исходного кода

Add inspect.signature (#526)

* Add inspect.signature tests

* Implement inspect.signature
felfoldy 1 день назад
Родитель
Сommit
21248a0baf
5 измененных файлов с 246 добавлено и 0 удалено
  1. 1 0
      include/pocketpy/common/_generated.h
  2. 58 0
      python/inspect.py
  3. 0 0
      src/common/_generated.c
  4. 54 0
      src/modules/inspect.c
  5. 133 0
      tests/970_inspect.py

+ 1 - 0
include/pocketpy/common/_generated.h

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

+ 58 - 0
python/inspect.py

@@ -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))

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
src/common/_generated.c


+ 54 - 0
src/modules/inspect.c

@@ -1,6 +1,7 @@
 #include "pocketpy/pocketpy.h"
 #include "pocketpy/objects/object.h"
 #include "pocketpy/interpreter/vm.h"
+#include "pocketpy/common/_generated.h"
 
 static bool inspect_isgeneratorfunction(int argc, py_Ref argv) {
     PY_CHECK_ARGC(1);
@@ -26,9 +27,62 @@ static bool inspect_is_user_defined_type(int argc, py_Ref argv) {
     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() {
     py_Ref mod = py_newmodule("inspect");
 
     py_bindfunc(mod, "isgeneratorfunction", inspect_isgeneratorfunction);
     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");
+    }
 }

+ 133 - 0
tests/970_inspect.py

@@ -47,3 +47,136 @@ assert not isgeneratorfunction(a.not_gen_instance)
 assert isgeneratorfunction(a.gen_instance)
 assert not isgeneratorfunction(A.not_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

Некоторые файлы не были показаны из-за большого количества измененных файлов