diff --git a/include/pocketpy/common/_generated.h b/include/pocketpy/common/_generated.h index abe471e1..2d420726 100644 --- a/include/pocketpy/common/_generated.h +++ b/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[]; diff --git a/python/inspect.py b/python/inspect.py new file mode 100644 index 00000000..d52083cd --- /dev/null +++ b/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 '' + + +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 '' + + +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)) diff --git a/src/common/_generated.c b/src/common/_generated.c index 080480e2..335c830a 100644 --- a/src/common/_generated.c +++ b/src/common/_generated.c @@ -9,6 +9,7 @@ const char kPythonLibs_dataclasses[] = "def _get_annotations(cls: type):\n in const char kPythonLibs_datetime[] = "from time import localtime\nimport operator\n\nclass timedelta:\n def __init__(self, days=0, seconds=0):\n self.days = days\n self.seconds = seconds\n\n def __repr__(self):\n return f\"datetime.timedelta(days={self.days}, seconds={self.seconds})\"\n\n def __eq__(self, other) -> bool:\n if not isinstance(other, timedelta):\n return NotImplemented\n return (self.days, self.seconds) == (other.days, other.seconds)\n\n def __ne__(self, other) -> bool:\n if not isinstance(other, timedelta):\n return NotImplemented\n return (self.days, self.seconds) != (other.days, other.seconds)\n\n\nclass date:\n def __init__(self, year: int, month: int, day: int):\n self.year = year\n self.month = month\n self.day = day\n\n @staticmethod\n def today():\n t = localtime()\n return date(t.tm_year, t.tm_mon, t.tm_mday)\n \n def __cmp(self, other, op):\n if not isinstance(other, date):\n return NotImplemented\n if self.year != other.year:\n return op(self.year, other.year)\n if self.month != other.month:\n return op(self.month, other.month)\n return op(self.day, other.day)\n\n def __eq__(self, other) -> bool:\n return self.__cmp(other, operator.eq)\n \n def __ne__(self, other) -> bool:\n return self.__cmp(other, operator.ne)\n\n def __lt__(self, other: 'date') -> bool:\n return self.__cmp(other, operator.lt)\n\n def __le__(self, other: 'date') -> bool:\n return self.__cmp(other, operator.le)\n\n def __gt__(self, other: 'date') -> bool:\n return self.__cmp(other, operator.gt)\n\n def __ge__(self, other: 'date') -> bool:\n return self.__cmp(other, operator.ge)\n\n def __str__(self):\n return f\"{self.year}-{self.month:02}-{self.day:02}\"\n\n def __repr__(self):\n return f\"datetime.date({self.year}, {self.month}, {self.day})\"\n\n\nclass datetime(date):\n def __init__(self, year: int, month: int, day: int, hour: int, minute: int, second: int):\n super().__init__(year, month, day)\n # Validate and set hour, minute, and second\n if not 0 <= hour <= 23:\n raise ValueError(\"Hour must be between 0 and 23\")\n self.hour = hour\n if not 0 <= minute <= 59:\n raise ValueError(\"Minute must be between 0 and 59\")\n self.minute = minute\n if not 0 <= second <= 59:\n raise ValueError(\"Second must be between 0 and 59\")\n self.second = second\n\n def date(self) -> date:\n return date(self.year, self.month, self.day)\n\n @staticmethod\n def now():\n t = localtime()\n tm_sec = t.tm_sec\n if tm_sec == 60:\n tm_sec = 59\n return datetime(t.tm_year, t.tm_mon, t.tm_mday, t.tm_hour, t.tm_min, tm_sec)\n\n def __str__(self):\n return f\"{self.year}-{self.month:02}-{self.day:02} {self.hour:02}:{self.minute:02}:{self.second:02}\"\n\n def __repr__(self):\n return f\"datetime.datetime({self.year}, {self.month}, {self.day}, {self.hour}, {self.minute}, {self.second})\"\n\n def __cmp(self, other, op):\n if not isinstance(other, datetime):\n return NotImplemented\n if self.year != other.year:\n return op(self.year, other.year)\n if self.month != other.month:\n return op(self.month, other.month)\n if self.day != other.day:\n return op(self.day, other.day)\n if self.hour != other.hour:\n return op(self.hour, other.hour)\n if self.minute != other.minute:\n return op(self.minute, other.minute)\n return op(self.second, other.second)\n\n def __eq__(self, other) -> bool:\n return self.__cmp(other, operator.eq)\n \n def __ne__(self, other) -> bool:\n return self.__cmp(other, operator.ne)\n \n def __lt__(self, other) -> bool:\n return self.__cmp(other, operator.lt)\n \n def __le__(self, other) -> bool:\n return self.__cmp(other, operator.le)\n \n def __gt__(self, other) -> bool:\n return self.__cmp(other, operator.gt)\n \n def __ge__(self, other) -> bool:\n return self.__cmp(other, operator.ge)\n\n\n"; const char kPythonLibs_functools[] = "class cache:\n def __init__(self, f):\n self.f = f\n self.cache = {}\n\n def __call__(self, *args):\n if args not in self.cache:\n self.cache[args] = self.f(*args)\n return self.cache[args]\n \nclass lru_cache:\n def __init__(self, maxsize=128):\n self.maxsize = maxsize\n self.cache = {}\n\n def __call__(self, f):\n def wrapped(*args):\n if args in self.cache:\n res = self.cache.pop(args)\n self.cache[args] = res\n return res\n \n res = f(*args)\n if len(self.cache) >= self.maxsize:\n first_key = next(iter(self.cache))\n self.cache.pop(first_key)\n self.cache[args] = res\n return res\n return wrapped\n \ndef reduce(function, sequence, initial=...):\n it = iter(sequence)\n if initial is ...:\n try:\n value = next(it)\n except StopIteration:\n raise TypeError(\"reduce() of empty sequence with no initial value\")\n else:\n value = initial\n for element in it:\n value = function(value, element)\n return value\n\nclass partial:\n def __init__(self, f, *args, **kwargs):\n self.f = f\n if not callable(f):\n raise TypeError(\"the first argument must be callable\")\n self.args = args\n self.kwargs = kwargs\n\n def __call__(self, *args, **kwargs):\n kwargs.update(self.kwargs)\n return self.f(*self.args, *args, **kwargs)\n\n"; const char kPythonLibs_heapq[] = "# Heap queue algorithm (a.k.a. priority queue)\ndef heappush(heap, item):\n \"\"\"Push item onto heap, maintaining the heap invariant.\"\"\"\n heap.append(item)\n _siftdown(heap, 0, len(heap)-1)\n\ndef heappop(heap):\n \"\"\"Pop the smallest item off the heap, maintaining the heap invariant.\"\"\"\n lastelt = heap.pop() # raises appropriate IndexError if heap is empty\n if heap:\n returnitem = heap[0]\n heap[0] = lastelt\n _siftup(heap, 0)\n return returnitem\n return lastelt\n\ndef heapreplace(heap, item):\n \"\"\"Pop and return the current smallest value, and add the new item.\n\n This is more efficient than heappop() followed by heappush(), and can be\n more appropriate when using a fixed-size heap. Note that the value\n returned may be larger than item! That constrains reasonable uses of\n this routine unless written as part of a conditional replacement:\n\n if item > heap[0]:\n item = heapreplace(heap, item)\n \"\"\"\n returnitem = heap[0] # raises appropriate IndexError if heap is empty\n heap[0] = item\n _siftup(heap, 0)\n return returnitem\n\ndef heappushpop(heap, item):\n \"\"\"Fast version of a heappush followed by a heappop.\"\"\"\n if heap and heap[0] < item:\n item, heap[0] = heap[0], item\n _siftup(heap, 0)\n return item\n\ndef heapify(x):\n \"\"\"Transform list into a heap, in-place, in O(len(x)) time.\"\"\"\n n = len(x)\n # Transform bottom-up. The largest index there's any point to looking at\n # is the largest with a child index in-range, so must have 2*i + 1 < n,\n # or i < (n-1)/2. If n is even = 2*j, this is (2*j-1)/2 = j-1/2 so\n # j-1 is the largest, which is n//2 - 1. If n is odd = 2*j+1, this is\n # (2*j+1-1)/2 = j so j-1 is the largest, and that's again n//2-1.\n for i in reversed(range(n//2)):\n _siftup(x, i)\n\n# 'heap' is a heap at all indices >= startpos, except possibly for pos. pos\n# is the index of a leaf with a possibly out-of-order value. Restore the\n# heap invariant.\ndef _siftdown(heap, startpos, pos):\n newitem = heap[pos]\n # Follow the path to the root, moving parents down until finding a place\n # newitem fits.\n while pos > startpos:\n parentpos = (pos - 1) >> 1\n parent = heap[parentpos]\n if newitem < parent:\n heap[pos] = parent\n pos = parentpos\n continue\n break\n heap[pos] = newitem\n\ndef _siftup(heap, pos):\n endpos = len(heap)\n startpos = pos\n newitem = heap[pos]\n # Bubble up the smaller child until hitting a leaf.\n childpos = 2*pos + 1 # leftmost child position\n while childpos < endpos:\n # Set childpos to index of smaller child.\n rightpos = childpos + 1\n if rightpos < endpos and not heap[childpos] < heap[rightpos]:\n childpos = rightpos\n # Move the smaller child up.\n heap[pos] = heap[childpos]\n pos = childpos\n childpos = 2*pos + 1\n # The leaf at pos is empty now. Put newitem there, and bubble it up\n # to its final resting place (by sifting its parents down).\n heap[pos] = newitem\n _siftdown(heap, startpos, pos)"; +const char kPythonLibs_inspect[] = "class _empty:\n pass\n\n\nclass Parameter:\n POSITIONAL_ONLY = 0\n POSITIONAL_OR_KEYWORD = 1\n VAR_POSITIONAL = 2\n KEYWORD_ONLY = 3\n VAR_KEYWORD = 4\n\n empty = _empty\n\n def __init__(self, name, kind, *default):\n self.name = name\n self.kind = kind\n # pocketpy only allows literal defaults, so use *default as sentinel\n self.default = default[0] if default else _empty\n\n def __str__(self):\n res = self.name\n if self.default is not _empty:\n res += '=' + repr(self.default)\n if self.kind == Parameter.VAR_POSITIONAL:\n res = '*' + res\n elif self.kind == Parameter.VAR_KEYWORD:\n res = '**' + res\n return res\n\n def __repr__(self):\n return ''\n\n\nclass Signature:\n empty = _empty\n\n def __init__(self, parameters):\n self.parameters = {p.name: p for p in parameters}\n\n def __str__(self):\n return '(' + ', '.join([str(p) for p in self.parameters.values()]) + ')'\n\n def __repr__(self):\n return ''\n\n\ndef _make_params(func):\n return [Parameter(*entry) for entry in _signature_data(func)]\n\n\ndef signature(obj):\n if not callable(obj):\n raise TypeError(repr(obj) + ' is not a callable object')\n if isinstance(obj, type):\n return Signature(_make_params(obj.__init__)[1:])\n if hasattr(obj, '__func__'):\n return Signature(_make_params(obj.__func__)[1:])\n return Signature(_make_params(obj))\n"; const char kPythonLibs_long_v1[] = "# after v1.2.2, int is always 64-bit\nPyLong_SHIFT = 60//2 - 1\n\nPyLong_BASE = 2 ** PyLong_SHIFT\nPyLong_MASK = PyLong_BASE - 1\nPyLong_DECIMAL_SHIFT = 4\nPyLong_DECIMAL_BASE = 10 ** PyLong_DECIMAL_SHIFT\n\n##############################################################\n\ndef ulong_fromint(x: int):\n # return a list of digits and sign\n if x == 0: return [0], 1\n sign = 1 if x > 0 else -1\n if sign < 0: x = -x\n res = []\n while x:\n res.append(x & PyLong_MASK)\n x >>= PyLong_SHIFT\n return res, sign\n\ndef ulong_cmp(a: list, b: list) -> int:\n # return 1 if a>b, -1 if a len(b): return 1\n if len(a) < len(b): return -1\n for i in range(len(a)-1, -1, -1):\n if a[i] > b[i]: return 1\n if a[i] < b[i]: return -1\n return 0\n\ndef ulong_pad_(a: list, size: int):\n # pad leading zeros to have `size` digits\n delta = size - len(a)\n if delta > 0:\n a.extend([0] * delta)\n\ndef ulong_unpad_(a: list):\n # remove leading zeros\n while len(a)>1 and a[-1]==0:\n a.pop()\n\ndef ulong_add(a: list, b: list) -> list:\n res = [0] * max(len(a), len(b))\n ulong_pad_(a, len(res))\n ulong_pad_(b, len(res))\n carry = 0\n for i in range(len(res)):\n carry += a[i] + b[i]\n res[i] = carry & PyLong_MASK\n carry >>= PyLong_SHIFT\n if carry > 0:\n res.append(carry)\n return res\n\ndef ulong_inc_(a: list):\n a[0] += 1\n for i in range(len(a)):\n if a[i] < PyLong_BASE: break\n a[i] -= PyLong_BASE\n if i+1 == len(a):\n a.append(1)\n else:\n a[i+1] += 1\n \n\ndef ulong_sub(a: list, b: list) -> list:\n # a >= b\n res = []\n borrow = 0\n for i in range(len(b)):\n tmp = a[i] - b[i] - borrow\n if tmp < 0:\n tmp += PyLong_BASE\n borrow = 1\n else:\n borrow = 0\n res.append(tmp)\n for i in range(len(b), len(a)):\n tmp = a[i] - borrow\n if tmp < 0:\n tmp += PyLong_BASE\n borrow = 1\n else:\n borrow = 0\n res.append(tmp)\n ulong_unpad_(res)\n return res\n\ndef ulong_divmodi(a: list, b: int):\n # b > 0\n res = []\n carry = 0\n for i in range(len(a)-1, -1, -1):\n carry <<= PyLong_SHIFT\n carry += a[i]\n res.append(carry // b)\n carry %= b\n res.reverse()\n ulong_unpad_(res)\n return res, carry\n\n\ndef ulong_divmod(a: list, b: list):\n\n if ulong_cmp(a, b) < 0:\n return [0], a\n\n if len(b) == 1:\n q, r = ulong_divmodi(a, b[0])\n r, _ = ulong_fromint(r)\n return q, r\n\n max = (len(a) - len(b)) * PyLong_SHIFT + \x5c\n (a[-1].bit_length() - b[-1].bit_length())\n\n low = [0]\n\n high = (max // PyLong_SHIFT) * [0] + \x5c\n [(2**(max % PyLong_SHIFT)) & PyLong_MASK]\n\n while ulong_cmp(low, high) < 0:\n ulong_inc_(high)\n mid, r = ulong_divmodi(ulong_add(low, high), 2)\n if ulong_cmp(a, ulong_mul(b, mid)) >= 0:\n low = mid\n else:\n high = ulong_sub(mid, [1])\n\n q = [0] * (len(a) - len(b) + 1)\n while ulong_cmp(a, ulong_mul(b, low)) >= 0:\n q = ulong_add(q, low)\n a = ulong_sub(a, ulong_mul(b, low))\n ulong_unpad_(q)\n return q, a\n\ndef ulong_floordivi(a: list, b: int):\n # b > 0\n return ulong_divmodi(a, b)[0]\n\ndef ulong_muli(a: list, b: int):\n # b >= 0\n res = [0] * len(a)\n carry = 0\n for i in range(len(a)):\n carry += a[i] * b\n res[i] = carry & PyLong_MASK\n carry >>= PyLong_SHIFT\n if carry > 0:\n res.append(carry)\n return res\n\ndef ulong_mul(a: list, b: list):\n N = len(a) + len(b)\n # use grade-school multiplication\n res = [0] * N\n for i in range(len(a)):\n carry = 0\n for j in range(len(b)):\n carry += res[i+j] + a[i] * b[j]\n res[i+j] = carry & PyLong_MASK\n carry >>= PyLong_SHIFT\n res[i+len(b)] = carry\n ulong_unpad_(res)\n return res\n\ndef ulong_powi(a: list, b: int):\n # b >= 0\n if b == 0: return [1]\n res = [1]\n while b:\n if b & 1:\n res = ulong_mul(res, a)\n a = ulong_mul(a, a)\n b >>= 1\n return res\n\ndef ulong_repr(x: list) -> str:\n res = []\n while len(x)>1 or x[0]>0: # non-zero\n x, r = ulong_divmodi(x, PyLong_DECIMAL_BASE)\n res.append(str(r).zfill(PyLong_DECIMAL_SHIFT))\n res.reverse()\n s = ''.join(res)\n if len(s) == 0: return '0'\n if len(s) > 1: s = s.lstrip('0')\n return s\n\ndef ulong_fromstr(s: str):\n if s[-1] == 'L':\n s = s[:-1]\n res, base = [0], [1]\n if s[0] == '-':\n sign = -1\n s = s[1:]\n else:\n sign = 1\n s = s[::-1]\n for c in s:\n c = ord(c) - 48\n assert 0 <= c <= 9\n res = ulong_add(res, ulong_muli(base, c))\n base = ulong_muli(base, 10)\n return res, sign\n\nclass long:\n def __init__(self, x):\n if type(x) is tuple:\n self.digits, self.sign = x\n elif type(x) is int:\n self.digits, self.sign = ulong_fromint(x)\n elif type(x) is float:\n self.digits, self.sign = ulong_fromint(int(x))\n elif type(x) is str:\n self.digits, self.sign = ulong_fromstr(x)\n elif type(x) is long:\n self.digits, self.sign = x.digits.copy(), x.sign\n else:\n raise TypeError('expected int or str')\n \n def __len__(self):\n return len(self.digits)\n\n def __add__(self, other):\n if type(other) is int:\n other = long(other)\n elif type(other) is not long:\n return NotImplemented\n if self.sign == other.sign:\n return long((ulong_add(self.digits, other.digits), self.sign))\n else:\n cmp = ulong_cmp(self.digits, other.digits)\n if cmp == 0:\n return long(0)\n if cmp > 0:\n return long((ulong_sub(self.digits, other.digits), self.sign))\n else:\n return long((ulong_sub(other.digits, self.digits), other.sign))\n \n def __radd__(self, other):\n return self.__add__(other)\n \n def __sub__(self, other):\n if type(other) is int:\n other = long(other)\n elif type(other) is not long:\n return NotImplemented\n if self.sign != other.sign:\n return long((ulong_add(self.digits, other.digits), self.sign))\n cmp = ulong_cmp(self.digits, other.digits)\n if cmp == 0:\n return long(0)\n if cmp > 0:\n return long((ulong_sub(self.digits, other.digits), self.sign))\n else:\n return long((ulong_sub(other.digits, self.digits), -other.sign))\n \n def __rsub__(self, other):\n if type(other) is int:\n other = long(other)\n elif type(other) is not long:\n return NotImplemented\n return other.__sub__(self)\n \n def __mul__(self, other):\n if type(other) is int:\n return long((\n ulong_muli(self.digits, abs(other)),\n self.sign * (1 if other >= 0 else -1)\n ))\n elif type(other) is long:\n return long((\n ulong_mul(self.digits, other.digits),\n self.sign * other.sign\n ))\n return NotImplemented\n \n def __rmul__(self, other):\n return self.__mul__(other)\n \n #######################################################\n def __divmod__(self, other):\n if type(other) is int:\n assert self.sign == 1 and other > 0\n q, r = ulong_divmodi(self.digits, other)\n return long((q, 1)), r\n if type(other) is long:\n assert self.sign == 1 and other.sign == 1\n q, r = ulong_divmod(self.digits, other.digits)\n assert len(other)>1 or other.digits[0]>0\n return long((q, 1)), long((r, 1))\n raise NotImplementedError\n\n def __floordiv__(self, other):\n return self.__divmod__(other)[0]\n\n def __mod__(self, other):\n return self.__divmod__(other)[1]\n\n def __pow__(self, other: int):\n assert type(other) is int and other >= 0\n if self.sign == -1 and other & 1:\n sign = -1\n else:\n sign = 1\n return long((ulong_powi(self.digits, other), sign))\n \n def __lshift__(self, other: int):\n assert type(other) is int and other >= 0\n x = self.digits.copy()\n q, r = divmod(other, PyLong_SHIFT)\n x = [0]*q + x\n for _ in range(r): x = ulong_muli(x, 2)\n return long((x, self.sign))\n \n def __rshift__(self, other: int):\n assert type(other) is int and other >= 0\n x = self.digits.copy()\n q, r = divmod(other, PyLong_SHIFT)\n x = x[q:]\n if not x: return long(0)\n for _ in range(r): x = ulong_floordivi(x, 2)\n return long((x, self.sign))\n \n def __neg__(self):\n return long((self.digits, -self.sign))\n \n def __cmp__(self, other):\n if type(other) is int:\n other = long(other)\n elif type(other) is not long:\n return NotImplemented\n if self.sign > other.sign:\n return 1\n elif self.sign < other.sign:\n return -1\n else:\n return ulong_cmp(self.digits, other.digits)\n \n def __eq__(self, other):\n return self.__cmp__(other) == 0\n def __ne__(self, other):\n return self.__cmp__(other) != 0\n def __lt__(self, other):\n return self.__cmp__(other) < 0\n def __le__(self, other):\n return self.__cmp__(other) <= 0\n def __gt__(self, other):\n return self.__cmp__(other) > 0\n def __ge__(self, other):\n return self.__cmp__(other) >= 0\n \n def __repr__(self):\n prefix = '-' if self.sign < 0 else ''\n return prefix + ulong_repr(self.digits) + 'L'"; const char kPythonLibs_operator[] = "# https://docs.python.org/3/library/operator.html#mapping-operators-to-functions\n\ndef le(a, b): return a <= b\ndef lt(a, b): return a < b\ndef ge(a, b): return a >= b\ndef gt(a, b): return a > b\ndef eq(a, b): return a == b\ndef ne(a, b): return a != b\n\ndef and_(a, b): return a & b\ndef or_(a, b): return a | b\ndef xor(a, b): return a ^ b\ndef invert(a): return ~a\ndef lshift(a, b): return a << b\ndef rshift(a, b): return a >> b\n\ndef is_(a, b): return a is b\ndef is_not(a, b): return a is not b\ndef not_(a): return not a\ndef truth(a): return bool(a)\ndef contains(a, b): return b in a\n\ndef add(a, b): return a + b\ndef sub(a, b): return a - b\ndef mul(a, b): return a * b\ndef truediv(a, b): return a / b\ndef floordiv(a, b): return a // b\ndef mod(a, b): return a % b\ndef pow(a, b): return a ** b\ndef neg(a): return -a\ndef matmul(a, b): return a @ b\n\ndef getitem(a, b): return a[b]\ndef setitem(a, b, c): a[b] = c\ndef delitem(a, b): del a[b]\n\ndef iadd(a, b): a += b; return a\ndef isub(a, b): a -= b; return a\ndef imul(a, b): a *= b; return a\ndef itruediv(a, b): a /= b; return a\ndef ifloordiv(a, b): a //= b; return a\ndef imod(a, b): a %= b; return a\n# def ipow(a, b): a **= b; return a\n# def imatmul(a, b): a @= b; return a\ndef iand(a, b): a &= b; return a\ndef ior(a, b): a |= b; return a\ndef ixor(a, b): a ^= b; return a\ndef ilshift(a, b): a <<= b; return a\ndef irshift(a, b): a >>= b; return a\n\nclass attrgetter:\n def __init__(self, attr):\n self.attr = attr\n def __call__(self, obj):\n return getattr(obj, self.attr)\n\nclass itemgetter:\n def __init__(self, item):\n self.item = item\n def __call__(self, obj):\n return obj[self.item]\n"; const char kPythonLibs_typing[] = "class _Placeholder:\n def __init__(self, *args, **kwargs):\n pass\n def __getitem__(self, *args):\n return self\n def __call__(self, *args, **kwargs):\n return self\n def __and__(self, other):\n return self\n def __or__(self, other):\n return self\n def __xor__(self, other):\n return self\n\n\n_PLACEHOLDER = _Placeholder()\n\nSequence = _PLACEHOLDER\nList = _PLACEHOLDER\nDict = _PLACEHOLDER\nTuple = _PLACEHOLDER\nSet = _PLACEHOLDER\nAny = _PLACEHOLDER\nUnion = _PLACEHOLDER\nOptional = _PLACEHOLDER\nCallable = _PLACEHOLDER\nType = _PLACEHOLDER\nTypeAlias = _PLACEHOLDER\nNewType = _PLACEHOLDER\n\nClassVar = _PLACEHOLDER\n\nLiteral = _PLACEHOLDER\nLiteralString = _PLACEHOLDER\n\nIterable = _PLACEHOLDER\nGenerator = _PLACEHOLDER\nIterator = _PLACEHOLDER\n\nHashable = _PLACEHOLDER\n\nTypeVar = _PLACEHOLDER\nSelf = _PLACEHOLDER\n\nProtocol = object\nGeneric = object\nNever = object\n\nTYPE_CHECKING = False\n\n# decorators\noverload = lambda x: x\noverride = lambda x: x\nfinal = lambda x: x\n\n# exhaustiveness checking\nassert_never = lambda x: x\n\nTypedDict = dict\nNotRequired = _PLACEHOLDER\nReadOnly = _PLACEHOLDER\nRequired = _PLACEHOLDER\nTypeIs = _PLACEHOLDER\nTypeGuard = _PLACEHOLDER\n\ncast = lambda _, val: val\n"; @@ -23,6 +24,7 @@ const char* load_kPythonLib(const char* name) { if (strcmp(name, "datetime") == 0) return kPythonLibs_datetime; if (strcmp(name, "functools") == 0) return kPythonLibs_functools; if (strcmp(name, "heapq") == 0) return kPythonLibs_heapq; + if (strcmp(name, "inspect") == 0) return kPythonLibs_inspect; if (strcmp(name, "long_v1") == 0) return kPythonLibs_long_v1; if (strcmp(name, "operator") == 0) return kPythonLibs_operator; if (strcmp(name, "typing") == 0) return kPythonLibs_typing; diff --git a/src/modules/inspect.c b/src/modules/inspect.c index 0898a7b1..fa920da7 100644 --- a/src/modules/inspect.c +++ b/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"); + } } diff --git a/tests/970_inspect.py b/tests/970_inspect.py index 6ce9a108..aec8ec5c 100644 --- a/tests/970_inspect.py +++ b/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