Skip to content

[mypyc] Generate introspection signatures for compiled functions #19307

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 26 additions & 3 deletions mypyc/codegen/emitclass.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@
from collections.abc import Mapping
from typing import Callable

from mypyc.codegen.cstring import c_string_initializer
from mypyc.codegen.emit import Emitter, HeaderDeclaration, ReturnHandler
from mypyc.codegen.emitfunc import native_function_header
from mypyc.codegen.emitfunc import native_function_doc_initializer, native_function_header
from mypyc.codegen.emitwrapper import (
generate_bin_op_wrapper,
generate_bool_wrapper,
Expand All @@ -21,7 +22,13 @@
)
from mypyc.common import BITMAP_BITS, BITMAP_TYPE, NATIVE_PREFIX, PREFIX, REG_PREFIX
from mypyc.ir.class_ir import ClassIR, VTableEntries
from mypyc.ir.func_ir import FUNC_CLASSMETHOD, FUNC_STATICMETHOD, FuncDecl, FuncIR
from mypyc.ir.func_ir import (
FUNC_CLASSMETHOD,
FUNC_STATICMETHOD,
FuncDecl,
FuncIR,
get_text_signature,
)
from mypyc.ir.rtypes import RTuple, RType, object_rprimitive
from mypyc.namegen import NameGenerator
from mypyc.sametype import is_same_type
Expand Down Expand Up @@ -345,6 +352,8 @@ def emit_line() -> None:
flags.append("Py_TPFLAGS_MANAGED_DICT")
fields["tp_flags"] = " | ".join(flags)

fields["tp_doc"] = native_class_doc_initializer(cl)

emitter.emit_line(f"static PyTypeObject {emitter.type_struct_name(cl)}_template_ = {{")
emitter.emit_line("PyVarObject_HEAD_INIT(NULL, 0)")
for field, value in fields.items():
Expand Down Expand Up @@ -841,7 +850,8 @@ def generate_methods_table(cl: ClassIR, name: str, emitter: Emitter) -> None:
elif fn.decl.kind == FUNC_CLASSMETHOD:
flags.append("METH_CLASS")

emitter.emit_line(" {}, NULL}},".format(" | ".join(flags)))
doc = native_function_doc_initializer(fn)
emitter.emit_line(" {}, {}}},".format(" | ".join(flags), doc))

# Provide a default __getstate__ and __setstate__
if not cl.has_method("__setstate__") and not cl.has_method("__getstate__"):
Expand Down Expand Up @@ -1099,3 +1109,16 @@ def has_managed_dict(cl: ClassIR, emitter: Emitter) -> bool:
and cl.has_dict
and cl.builtin_base != "PyBaseExceptionObject"
)


def native_class_doc_initializer(cl: ClassIR) -> str:
init_fn = cl.get_method("__init__")
if init_fn is not None:
text_sig = get_text_signature(init_fn, bound=True)
if text_sig is None:
return "NULL"
text_sig = text_sig.replace("__init__", cl.name, 1)
else:
text_sig = f"{cl.name}()"
docstring = f"{text_sig}\n--\n\n"
return c_string_initializer(docstring.encode("ascii", errors="backslashreplace"))
18 changes: 17 additions & 1 deletion mypyc/codegen/emitfunc.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from typing import Final

from mypyc.analysis.blockfreq import frequently_executed_blocks
from mypyc.codegen.cstring import c_string_initializer
from mypyc.codegen.emit import DEBUG_ERRORS, Emitter, TracebackAndGotoHandler, c_array_initializer
from mypyc.common import (
HAVE_IMMORTAL,
Expand All @@ -16,7 +17,14 @@
TYPE_VAR_PREFIX,
)
from mypyc.ir.class_ir import ClassIR
from mypyc.ir.func_ir import FUNC_CLASSMETHOD, FUNC_STATICMETHOD, FuncDecl, FuncIR, all_values
from mypyc.ir.func_ir import (
FUNC_CLASSMETHOD,
FUNC_STATICMETHOD,
FuncDecl,
FuncIR,
all_values,
get_text_signature,
)
from mypyc.ir.ops import (
ERR_FALSE,
NAMESPACE_MODULE,
Expand Down Expand Up @@ -105,6 +113,14 @@ def native_function_header(fn: FuncDecl, emitter: Emitter) -> str:
)


def native_function_doc_initializer(func: FuncIR) -> str:
text_sig = get_text_signature(func)
if text_sig is None:
return "NULL"
docstring = f"{text_sig}\n--\n\n"
return c_string_initializer(docstring.encode("ascii", errors="backslashreplace"))


def generate_native_function(
fn: FuncIR, emitter: Emitter, source_path: str, module_name: str
) -> None:
Expand Down
13 changes: 10 additions & 3 deletions mypyc/codegen/emitmodule.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,11 @@
from mypyc.codegen.cstring import c_string_initializer
from mypyc.codegen.emit import Emitter, EmitterContext, HeaderDeclaration, c_array_initializer
from mypyc.codegen.emitclass import generate_class, generate_class_type_decl
from mypyc.codegen.emitfunc import generate_native_function, native_function_header
from mypyc.codegen.emitfunc import (
generate_native_function,
native_function_doc_initializer,
native_function_header,
)
from mypyc.codegen.emitwrapper import (
generate_legacy_wrapper_function,
generate_wrapper_function,
Expand Down Expand Up @@ -915,11 +919,14 @@ def emit_module_methods(
flag = "METH_FASTCALL"
else:
flag = "METH_VARARGS"
doc = native_function_doc_initializer(fn)
emitter.emit_line(
(
'{{"{name}", (PyCFunction){prefix}{cname}, {flag} | METH_KEYWORDS, '
"NULL /* docstring */}},"
).format(name=name, cname=fn.cname(emitter.names), prefix=PREFIX, flag=flag)
"{doc} /* docstring */}},"
).format(
name=name, cname=fn.cname(emitter.names), prefix=PREFIX, flag=flag, doc=doc
)
)
emitter.emit_line("{NULL, NULL, 0, NULL}")
emitter.emit_line("};")
Expand Down
3 changes: 2 additions & 1 deletion mypyc/doc/differences_from_python.rst
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,8 @@ non-exhaustive list of what won't work:
- Instance ``__annotations__`` is usually not kept
- Frames of compiled functions can't be inspected using ``inspect``
- Compiled methods aren't considered methods by ``inspect.ismethod``
- ``inspect.signature`` chokes on compiled functions
- ``inspect.signature`` chokes on compiled functions with default arguments that
are not simple literals
- ``inspect.iscoroutinefunction`` and ``asyncio.iscoroutinefunction`` will always return False for compiled functions, even those defined with `async def`

Profiling hooks and tracing
Expand Down
96 changes: 95 additions & 1 deletion mypyc/ir/func_ir.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from __future__ import annotations

import inspect
from collections.abc import Sequence
from typing import Final

Expand All @@ -11,13 +12,24 @@
Assign,
AssignMulti,
BasicBlock,
Box,
ControlOp,
DeserMaps,
Float,
Integer,
LoadAddress,
LoadLiteral,
Register,
TupleSet,
Value,
)
from mypyc.ir.rtypes import RType, bitmap_rprimitive, deserialize_type
from mypyc.ir.rtypes import (
RType,
bitmap_rprimitive,
deserialize_type,
is_bool_rprimitive,
is_none_rprimitive,
)
from mypyc.namegen import NameGenerator


Expand Down Expand Up @@ -379,3 +391,85 @@ def all_values_full(args: list[Register], blocks: list[BasicBlock]) -> list[Valu
values.append(op)

return values


_ARG_KIND_TO_INSPECT: Final = {
ArgKind.ARG_POS: inspect.Parameter.POSITIONAL_OR_KEYWORD,
ArgKind.ARG_OPT: inspect.Parameter.POSITIONAL_OR_KEYWORD,
ArgKind.ARG_STAR: inspect.Parameter.VAR_POSITIONAL,
ArgKind.ARG_NAMED: inspect.Parameter.KEYWORD_ONLY,
ArgKind.ARG_STAR2: inspect.Parameter.VAR_KEYWORD,
ArgKind.ARG_NAMED_OPT: inspect.Parameter.KEYWORD_ONLY,
}

# Sentinel indicating a value that cannot be represented in a text signature.
_NOT_REPRESENTABLE = object()


def get_text_signature(fn: FuncIR, *, bound: bool = False) -> str | None:
"""Return a text signature in CPython's internal doc format, or None
if the function's signature cannot be represented.
"""
parameters = []
mark_self = fn.class_name is not None and fn.decl.kind != FUNC_STATICMETHOD and not bound
# Pre-scan for end of positional-only parameters.
# This is needed to handle signatures like 'def foo(self, __x)', where mypy
# currently sees 'self' as being positional-or-keyword and '__x' as positional-only.
pos_only_idx = -1
sig = fn.decl.bound_sig if bound and fn.decl.bound_sig is not None else fn.decl.sig
for idx, arg in enumerate(sig.args):
if arg.pos_only and arg.kind in (ArgKind.ARG_POS, ArgKind.ARG_OPT):
pos_only_idx = idx
for idx, arg in enumerate(sig.args):
if arg.name.startswith("__bitmap") or arg.name == "__mypyc_self__":
continue
kind = (
inspect.Parameter.POSITIONAL_ONLY
if idx <= pos_only_idx
else _ARG_KIND_TO_INSPECT[arg.kind]
)
default: object = inspect.Parameter.empty
if arg.optional:
default = _find_default_argument(arg.name, fn.blocks)
if default is _NOT_REPRESENTABLE:
# This default argument cannot be represented in a __text_signature__
return None

curr_param = inspect.Parameter(arg.name, kind, default=default)
parameters.append(curr_param)
if mark_self:
# Parameter.__init__/Parameter.replace do not accept $
curr_param._name = f"${arg.name}" # type: ignore[attr-defined]
mark_self = False
return f"{fn.name}{inspect.Signature(parameters)}"


def _find_default_argument(name: str, blocks: list[BasicBlock]) -> object:
# Find assignment inserted by gen_arg_defaults. Assumed to be the first assignment.
for block in blocks:
for op in block.ops:
if isinstance(op, Assign) and op.dest.name == name:
return _extract_python_literal(op.src)
return _NOT_REPRESENTABLE


def _extract_python_literal(value: Value) -> object:
if isinstance(value, Integer):
if is_none_rprimitive(value.type):
return None
val = value.numeric_value()
if is_bool_rprimitive(value.type):
return bool(val)
return val
elif isinstance(value, Float):
return value.value
elif isinstance(value, LoadLiteral):
return value.value
elif isinstance(value, Box):
return _extract_python_literal(value.src)
elif isinstance(value, TupleSet):
items = tuple(_extract_python_literal(item) for item in value.items)
if any(itm is _NOT_REPRESENTABLE for itm in items):
return _NOT_REPRESENTABLE
return items
return _NOT_REPRESENTABLE
Loading