diff --git a/include/pocketpy/objects/codeobject.h b/include/pocketpy/objects/codeobject.h index 6691eb68..babc6fe8 100644 --- a/include/pocketpy/objects/codeobject.h +++ b/include/pocketpy/objects/codeobject.h @@ -114,6 +114,8 @@ typedef struct FuncDecl { int starred_kwarg; // index in co->varnames, -1 if no **kwarg bool nested; // whether this function is nested + py_TValue annotations; // dict[str, str], nil if empty + char* docstring; FuncType type; @@ -128,6 +130,7 @@ void FuncDecl__add_arg(FuncDecl* self, py_Name name); void FuncDecl__add_kwarg(FuncDecl* self, py_Name name, const py_TValue* value); void FuncDecl__add_starred_arg(FuncDecl* self, py_Name name); void FuncDecl__add_starred_kwarg(FuncDecl* self, py_Name name); +void FuncDecl__add_annotation(FuncDecl* self, py_Name name, c11_sv hint); void FuncDecl__gc_mark(const FuncDecl* self, c11_vector* p_stack); void FuncDecl__dtor(FuncDecl* self); diff --git a/python/inspect.py b/python/inspect.py index d52083cd..bcc97fa3 100644 --- a/python/inspect.py +++ b/python/inspect.py @@ -11,15 +11,20 @@ class Parameter: empty = _empty - def __init__(self, name, kind, *default): + def __init__(self, name, kind, *default, annotation=None): self.name = name self.kind = kind # pocketpy only allows literal defaults, so use *default as sentinel self.default = default[0] if default else _empty + self.annotation = _empty if annotation is None else annotation def __str__(self): res = self.name - if self.default is not _empty: + if self.annotation is not _empty: + res += ': ' + self.annotation + if self.default is not _empty: + res += ' = ' + repr(self.default) + elif self.default is not _empty: res += '=' + repr(self.default) if self.kind == Parameter.VAR_POSITIONAL: res = '*' + res @@ -34,25 +39,34 @@ class Parameter: class Signature: empty = _empty - def __init__(self, parameters): + def __init__(self, parameters, return_annotation=None): self.parameters = {p.name: p for p in parameters} + self.return_annotation = _empty if return_annotation is None else return_annotation def __str__(self): - return '(' + ', '.join([str(p) for p in self.parameters.values()]) + ')' + res = '(' + ', '.join([str(p) for p in self.parameters.values()]) + ')' + if self.return_annotation is not _empty: + res += ' -> ' + self.return_annotation + return res def __repr__(self): return '' -def _make_params(func): - return [Parameter(*entry) for entry in _signature_data(func)] +def _from_function(func, drop_first): + annotations = func.__annotations__ + params = [Parameter(*entry, annotation=annotations.get(entry[0])) + for entry in _signature_data(func)] + if drop_first: + params = params[1:] + return Signature(params, annotations.get('return')) 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:]) + return _from_function(obj.__init__, True) if hasattr(obj, '__func__'): - return Signature(_make_params(obj.__func__)[1:]) - return Signature(_make_params(obj)) + return _from_function(obj.__func__, True) + return _from_function(obj, False) diff --git a/src/common/_generated.c b/src/common/_generated.c index 335c830a..d77b1dbb 100644 --- a/src/common/_generated.c +++ b/src/common/_generated.c @@ -9,7 +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_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, annotation=None):\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 self.annotation = _empty if annotation is None else annotation\n\n def __str__(self):\n res = self.name\n if self.annotation is not _empty:\n res += ': ' + self.annotation\n if self.default is not _empty:\n res += ' = ' + repr(self.default)\n elif 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, return_annotation=None):\n self.parameters = {p.name: p for p in parameters}\n self.return_annotation = _empty if return_annotation is None else return_annotation\n\n def __str__(self):\n res = '(' + ', '.join([str(p) for p in self.parameters.values()]) + ')'\n if self.return_annotation is not _empty:\n res += ' -> ' + self.return_annotation\n return res\n\n def __repr__(self):\n return ''\n\n\ndef _from_function(func, drop_first):\n annotations = func.__annotations__\n params = [Parameter(*entry, annotation=annotations.get(entry[0]))\n for entry in _signature_data(func)]\n if drop_first:\n params = params[1:]\n return Signature(params, annotations.get('return'))\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 _from_function(obj.__init__, True)\n if hasattr(obj, '__func__'):\n return _from_function(obj.__func__, True)\n return _from_function(obj, False)\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"; diff --git a/src/compiler/compiler.c b/src/compiler/compiler.c index 3a3491ed..5dc7de1f 100644 --- a/src/compiler/compiler.c +++ b/src/compiler/compiler.c @@ -2389,7 +2389,11 @@ static Error* _compile_f_args(Compiler* self, FuncDecl* decl, bool is_lambda) { } // eat type hints - if(!is_lambda && match(TK_COLON)) check(consume_type_hints(self)); + if(!is_lambda && match(TK_COLON)) { + c11_sv hint; + check(consume_type_hints_sv(self, &hint)); + FuncDecl__add_annotation(decl, name, hint); + } if(state == 0 && curr()->type == TK_ASSIGN) state = 2; switch(state) { case 0: FuncDecl__add_arg(decl, name); break; @@ -2438,7 +2442,11 @@ static Error* compile_function(Compiler* self, int decorators) { check(_compile_f_args(self, decl, false)); consume(TK_RPAREN); } - if(match(TK_ARROW)) check(consume_type_hints(self)); + if(match(TK_ARROW)) { + c11_sv hint; + check(consume_type_hints_sv(self, &hint)); + FuncDecl__add_annotation(decl, py_name("return"), hint); + } check(compile_block_body(self)); check(pop_context(self)); diff --git a/src/interpreter/vm.c b/src/interpreter/vm.c index 1db9ef6d..f2e6f844 100644 --- a/src/interpreter/vm.c +++ b/src/interpreter/vm.c @@ -636,6 +636,7 @@ void FuncDecl__gc_mark(const FuncDecl* self, c11_vector* p_stack) { FuncDeclKwArg* kw = c11__at(FuncDeclKwArg, &self->kwargs, j); pk__mark_value(&kw->value); } + pk__mark_value(&self->annotations); } void CodeObject__gc_mark(const CodeObject* self, c11_vector* p_stack) { diff --git a/src/modules/builtins.c b/src/modules/builtins.c index af6029c6..99d3980f 100644 --- a/src/modules/builtins.c +++ b/src/modules/builtins.c @@ -562,6 +562,17 @@ static bool function__doc__(int argc, py_Ref argv) { return true; } +static bool function__annotations__(int argc, py_Ref argv) { + PY_CHECK_ARGC(1); + Function* func = py_touserdata(py_arg(0)); + if(py_isnil(&func->decl->annotations)) { + py_newdict(py_retval()); + } else { + py_assign(py_retval(), &func->decl->annotations); + } + return true; +} + static bool function__name__(int argc, py_Ref argv) { PY_CHECK_ARGC(1); Function* func = py_touserdata(py_arg(0)); @@ -589,6 +600,7 @@ py_Type pk_function__register() { pk_newtype("function", tp_object, NULL, (void (*)(void*))Function__dtor, false, true); py_bindproperty(type, "__doc__", function__doc__, NULL); py_bindproperty(type, "__name__", function__name__, NULL); + py_bindproperty(type, "__annotations__", function__annotations__, NULL); py_bindmagic(type, __repr__, function__repr__); return type; } diff --git a/src/objects/codeobject.c b/src/objects/codeobject.c index 3ba251a5..396fd879 100644 --- a/src/objects/codeobject.c +++ b/src/objects/codeobject.c @@ -37,6 +37,7 @@ FuncDecl_ FuncDecl__rcnew(SourceData_ src, c11_sv name) { self->starred_arg = -1; self->starred_kwarg = -1; self->nested = false; + self->annotations = *py_NIL(); self->docstring = NULL; self->type = FuncType_UNSET; @@ -90,6 +91,13 @@ void FuncDecl__add_starred_kwarg(FuncDecl* self, py_Name name) { self->starred_kwarg = index; } +void FuncDecl__add_annotation(FuncDecl* self, py_Name name, c11_sv hint) { + if(py_isnil(&self->annotations)) py_newdict(&self->annotations); + py_TValue value; + py_newstrv(&value, hint); + py_dict_setitem_by_str(&self->annotations, py_name2str(name), &value); +} + void CodeObject__ctor(CodeObject* self, SourceData_ src, c11_sv name) { self->src = src; PK_INCREF(src); diff --git a/src/objects/codeobject_ser.c b/src/objects/codeobject_ser.c index 383a9923..e76bc1fb 100644 --- a/src/objects/codeobject_ser.c +++ b/src/objects/codeobject_ser.c @@ -277,6 +277,13 @@ static CodeObject CodeObject__deserialize(c11_deserializer* d, const char* filen return co; } +static bool annotation__serialize(py_Ref key, py_Ref val, void* ctx) { + c11_serializer* s = ctx; + c11_serializer__write_cstr(s, py_tostr(key)); + c11_serializer__write_cstr(s, py_tostr(val)); + return true; +} + // Serialize FuncDecl static void FuncDecl__serialize(c11_serializer* s, const FuncDecl* decl, @@ -317,6 +324,14 @@ static void FuncDecl__serialize(c11_serializer* s, // type c11_serializer__write_i8(s, (int8_t)decl->type); + + // annotations + py_Ref annotations = (py_Ref)&decl->annotations; + bool empty = py_isnil(annotations); + c11_serializer__write_i32(s, empty ? 0 : py_dict_len(annotations)); + c11_serializer__write_mark(s, '['); + if(!empty) py_dict_apply(annotations, annotation__serialize, s); + c11_serializer__write_mark(s, ']'); } // Deserialize FuncDecl @@ -374,6 +389,18 @@ static FuncDecl_ FuncDecl__deserialize(c11_deserializer* d, SourceData_ embedded // type self->type = (FuncType)c11_deserializer__read_i8(d); + + // annotations + self->annotations = *py_NIL(); + int annotations_len = c11_deserializer__read_i32(d); + c11_deserializer__consume_mark(d, '['); + for(int i = 0; i < annotations_len; i++) { + const char* name_str = c11_deserializer__read_cstr(d); + const char* hint_str = c11_deserializer__read_cstr(d); + c11_sv hint = {hint_str, (int)strlen(hint_str)}; + FuncDecl__add_annotation(self, py_name(name_str), hint); + } + c11_deserializer__consume_mark(d, ']'); return self; } diff --git a/tests/970_inspect.py b/tests/970_inspect.py index aec8ec5c..ff073c3f 100644 --- a/tests/970_inspect.py +++ b/tests/970_inspect.py @@ -180,3 +180,48 @@ try: exit(1) except TypeError: pass + +# ---------------- __annotations__ ---------------- +from inspect import Signature + +def h1(a: int, *args: int, b: str = 'x', c: float = 1.5, **kwargs: str) -> bool: + pass + +assert h1.__annotations__ == { + 'a': 'int', + 'args': 'int', + 'b': 'str', + 'c': 'float', + 'kwargs': 'str', + 'return': 'bool', +} + +sig = signature(h1) +assert sig.parameters['a'].annotation == 'int' +assert sig.return_annotation == 'bool' +assert str(sig) == "(a: int, *args: int, b: str = 'x', c: float = 1.5, **kwargs: str) -> bool" + +def h2(a, b: int, c=1): + pass + +assert h2.__annotations__ == {'b': 'int'} +assert str(signature(h2)) == '(a, b: int, c=1)' + +# complex annotation expressions are preserved as written +def h3(p: list[int], q: dict[str, int]) -> 'A | None': + pass + +assert h3.__annotations__ == {'p': 'list[int]', 'q': 'dict[str, int]', 'return': "'A | None'"} + +class D: + def m(self, x: int) -> str: + return str(x) + +sig = signature(D().m) +assert sig.parameters['x'].annotation == 'int' +assert sig.return_annotation == 'str' + +assert (lambda x: x).__annotations__ == {} +sig = signature(lambda x: x) +assert sig.parameters['x'].annotation is Parameter.empty +assert sig.return_annotation is Signature.empty