Skip to content
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
84 changes: 81 additions & 3 deletions mypyc/codegen/emitclass.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
GENERATOR_HELPER_NAME,
IS_FREE_THREADED,
MYPYC_DEFAULTS_SETUP,
MYPYC_WARN_DEPRECATED,
NATIVE_PREFIX,
PREFIX,
REG_PREFIX,
Expand Down Expand Up @@ -272,8 +273,10 @@ def generate_class(cl: ClassIR, module: str, emitter: Emitter) -> None:

generate_full = not cl.is_trait and not cl.builtin_base
needs_getseters = cl.needs_getseters_table
is_deprecated = MYPYC_WARN_DEPRECATED in cl.method_decls

if not cl.builtin_base:
# A class with a built-in base class normally inherits tp_new.
if not cl.builtin_base or is_deprecated:
fields["tp_new"] = new_name

managed_dict = has_managed_dict(cl, emitter)
Expand Down Expand Up @@ -429,8 +432,19 @@ def emit_line() -> None:

if cl.is_trait:
generate_new_for_trait(cl, new_name, emitter)
elif cl.builtin_base and is_deprecated:
generate_new_for_deprecated_builtin_subclass(cl, new_name, emitter)
emit_line()

init_subclass_name = None
if is_deprecated and "__init_subclass__" not in cl.methods:
init_subclass_name = f"{name_prefix}_init_subclass"
generate_init_subclass_for_deprecated_class(cl, init_subclass_name, emitter)
emit_line()

generate_methods_table(cl, methods_name, setup_name if generate_full else None, emitter)
generate_methods_table(
cl, methods_name, setup_name if generate_full else None, init_subclass_name, emitter
)
emit_line()

flags = ["Py_TPFLAGS_DEFAULT", "Py_TPFLAGS_HEAPTYPE", "Py_TPFLAGS_BASETYPE"]
Expand Down Expand Up @@ -797,6 +811,8 @@ def generate_constructor_for_class(
type_arg = "(PyObject *)" + emitter.type_struct_name(cl)
new_args = ", ".join(fn_args)

emit_deprecation_warning(cl, emitter)

use_wrapper = (
cl.has_method("__new__")
and len(fn.sig.args) == 2
Expand Down Expand Up @@ -876,6 +892,8 @@ def generate_new_for_class(
emitter.emit_line("return NULL;")
emitter.emit_line("}")

emit_deprecation_warning(cl, emitter, type_arg="type")

type_arg = "(PyObject*)type"
new_args = "args, kwds"
emit_setup_or_dunder_new_call(cl, setup_name, type_arg, False, new_args, emitter)
Expand All @@ -901,6 +919,56 @@ def generate_new_for_class(
emitter.emit_line("}")


def emit_deprecation_warning(cl: ClassIR, emitter: Emitter, type_arg: str | None = None) -> None:
"""Emit C code to issue the warnings of a class that is decorated with @deprecated.

This does nothing for other classes. If type_arg is given, it's the name of a C
variable with the type object of the instance that is being created. Only warn if
it's the class itself, like the runtime decorator. The code returns NULL on a raised
exception (if the warning is turned into an error).
"""
warn_fn = cl.method_decls.get(MYPYC_WARN_DEPRECATED)
if warn_fn is None:
return
cond = f"{emitter.native_function_call(warn_fn)}() == 2"
if type_arg is not None:
cond = f"{type_arg} == {emitter.type_struct_name(cl)} && {cond}"
emitter.emit_line(f"if ({cond})")
emitter.emit_line(" return NULL;")


def generate_new_for_deprecated_builtin_subclass(
cl: ClassIR, func_name: str, emitter: Emitter
) -> None:
"""Generate tp_new that issues the warnings of a class decorated with @deprecated.

This is for a class with a built-in base class. It would otherwise inherit tp_new.
"""
emitter.emit_line("static PyObject *")
emitter.emit_line(f"{func_name}(PyTypeObject *type, PyObject *args, PyObject *kwds)")
emitter.emit_line("{")
emit_deprecation_warning(cl, emitter, type_arg="type")
emitter.emit_line(f"return {emitter.type_struct_name(cl)}->tp_base->tp_new(type, args, kwds);")
emitter.emit_line("}")


def generate_init_subclass_for_deprecated_class(
cl: ClassIR, func_name: str, emitter: Emitter
) -> None:
"""Generate __init_subclass__ that issues the warnings of a class decorated with @deprecated.

The runtime decorator also defines this method if the class doesn't, so that
creating a subclass issues the warnings.
"""
emitter.emit_line("static PyObject *")
emitter.emit_line(f"{func_name}(PyObject *cls, PyObject *args, PyObject *kwds)")
emitter.emit_line("{")
emit_deprecation_warning(cl, emitter)
type_arg = "(PyObject *)" + emitter.type_struct_name(cl)
emitter.emit_line(f"return CPy_SuperInitSubclass({type_arg}, cls, args, kwds);")
emitter.emit_line("}")


def generate_new_for_trait(cl: ClassIR, func_name: str, emitter: Emitter) -> None:
emitter.emit_line("static PyObject *")
emitter.emit_line(f"{func_name}(PyTypeObject *type, PyObject *args, PyObject *kwds)")
Expand Down Expand Up @@ -1101,7 +1169,11 @@ def generate_finalize_for_class(


def generate_methods_table(
cl: ClassIR, name: str, setup_name: str | None, emitter: Emitter
cl: ClassIR,
name: str,
setup_name: str | None,
init_subclass_name: str | None,
emitter: Emitter,
) -> None:
emitter.emit_line(f"static PyMethodDef {name}[] = {{")
if setup_name:
Expand All @@ -1125,6 +1197,12 @@ def generate_methods_table(
doc = native_function_doc_initializer(fn)
emitter.emit_line(" {}, PyDoc_STR({})}},".format(" | ".join(flags), doc))

if init_subclass_name:
emitter.emit_line(
f'{{"__init_subclass__", (PyCFunction){init_subclass_name},'
" METH_VARARGS | METH_KEYWORDS | METH_CLASS, NULL},"
)

# Provide a default __getstate__ and __setstate__
if not cl.has_method("__setstate__") and not cl.has_method("__getstate__"):
emitter.emit_lines(
Expand Down
1 change: 1 addition & 0 deletions mypyc/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
PROPSET_PREFIX: Final = "__mypyc_setter__"
SELF_NAME: Final = "__mypyc_self__"
MYPYC_DEFAULTS_SETUP: Final = "__mypyc_defaults_setup"
MYPYC_WARN_DEPRECATED: Final = "__mypyc_warn_deprecated"
GENERATOR_ATTRIBUTE_PREFIX: Final = "__mypyc_generator_attribute__"
GENERATOR_FRAME_ATTRIBUTE_PREFIX: Final = "__mypyc_generator_frame_attribute__"
CPYFUNCTION_NAME = "__cpyfunction__"
Expand Down
19 changes: 19 additions & 0 deletions mypyc/doc/differences_from_python.rst
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,25 @@ methods with mocks in tests.
compiled code generally doesn't use this namespace, so any changes
will only be visible to non-compiled code.

Function attributes
-------------------

Compiled functions and methods don't support setting arbitrary
attributes. This also affects decorators that set an attribute on the
function they are given, instead of on a wrapper function that they
return. (Nested functions do support attributes.)

``@deprecated`` (from ``warnings`` or ``typing_extensions``) is such a
decorator. Mypyc special cases it and doesn't call it at runtime.
Instead, the compiled function issues the warning when it's called.
The function won't have the ``__deprecated__`` attribute. This applies
if the message is a string literal, ``category`` is omitted, ``None``
or a class defined at module top level, and ``stacklevel`` is omitted
or an integer literal. ``@deprecated`` must also be applied directly to
the function, not to the result of another decorator (it can be
below decorators such as ``@property`` and ``@classmethod``). In other
cases the decorator is called at runtime as usual.

Stack overflows
---------------

Expand Down
8 changes: 8 additions & 0 deletions mypyc/doc/native_classes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -222,10 +222,18 @@ decorators can be used with native classes, however:
* ``mypy_extensions.mypyc_attr`` (see :ref:`above <inheritance>`)
* ``dataclasses.dataclass``
* ``@attr.s(auto_attribs=True)``
* ``warnings.deprecated`` and ``typing_extensions.deprecated``

Dataclasses and attrs classes have partial native support, and they aren't as
efficient as pure native classes.

Mypyc doesn't call ``@deprecated`` at runtime. Instead, the native class
has the ``__deprecated__`` attribute and issues the warning itself when
an instance or a subclass of it is created. This applies if the message
is a string literal, ``category`` is omitted, ``None`` or a class defined
at module top level, and ``stacklevel`` is omitted or an integer literal.
In other cases ``@deprecated`` is an unsupported class decorator.

.. note::

If a class definition uses an unsupported class decorator, *mypyc
Expand Down
1 change: 1 addition & 0 deletions mypyc/irbuild/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,7 @@ def __init__(
self.encapsulating_funcs = pbv.encapsulating_funcs
self.nested_fitems = pbv.nested_funcs.keys()
self.fdefs_to_decorators = pbv.funcs_to_decorators
self.fdefs_to_deprecations = pbv.funcs_to_deprecations
self.module_import_groups = pbv.module_import_groups
self.comprehension_to_fitem = pbv.comprehension_to_fitem
self.deleted_vars = pbv.deleted_vars
Expand Down
63 changes: 61 additions & 2 deletions mypyc/irbuild/classdef.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,14 @@
is_class_var,
)
from mypy.types import Instance, UnboundType, get_proper_type
from mypyc.common import GENERATOR_HELPER_NAME, MYPYC_DEFAULTS_SETUP, PROPSET_PREFIX
from mypyc.common import (
GENERATOR_HELPER_NAME,
MYPYC_DEFAULTS_SETUP,
MYPYC_WARN_DEPRECATED,
PROPSET_PREFIX,
)
from mypyc.ir.class_ir import ClassIR, NonExtClassInfo
from mypyc.ir.func_ir import FuncDecl, FuncSignature
from mypyc.ir.func_ir import FUNC_STATICMETHOD, FuncDecl, FuncIR, FuncSignature
from mypyc.ir.ops import (
NAMESPACE_TYPE,
BasicBlock,
Expand All @@ -52,6 +57,7 @@
from mypyc.ir.rtypes import RType, bool_rprimitive, dict_rprimitive, object_rprimitive
from mypyc.irbuild.builder import IRBuilder, create_type_params
from mypyc.irbuild.function import (
gen_deprecation_warning,
gen_property_getter_ir,
gen_property_setter_ir,
handle_ext_method,
Expand All @@ -61,6 +67,7 @@
from mypyc.irbuild.util import (
dataclass_type,
default_attr_name,
get_class_deprecations,
get_func_def,
is_constant,
is_dataclass_decorator,
Expand Down Expand Up @@ -325,11 +332,20 @@ def __init__(self, builder: IRBuilder, cdef: ClassDef) -> None:
super().__init__(builder, cdef)
# If the class is not decorated, generate an extension class for it.
self.type_obj: Value = allocate_class(builder, cdef)
# The @deprecated decorators of the class that emit a warning. They aren't
# applied at runtime (see add_deprecations).
self.deprecation_warnings = [d for d in get_class_deprecations(cdef) if d.emits_warning]

def class_body_obj(self) -> Value | None:
return self.type_obj

def add_method(self, fdef: FuncDef) -> None:
if fdef.name == "__init_subclass__" and self.deprecation_warnings:
# Creating a subclass of a deprecated class emits the warning. The runtime
# decorator wraps an __init_subclass__ that the class defines to do this.
self.builder.fdefs_to_deprecations[fdef] = (
self.deprecation_warnings + self.builder.fdefs_to_deprecations.get(fdef, [])
)
handle_ext_method(self.builder, self.cdef, fdef)

def add_attr(self, lvalue: NameExpr, stmt: AssignmentStmt) -> None:
Expand Down Expand Up @@ -364,6 +380,49 @@ def finalize(self, ir: ClassIR) -> None:
ir.attrs_with_defaults.update(attrs_with_defaults)
generate_attr_defaults_init(self.builder, self.cdef, default_assignments)
create_ne_from_eq(self.builder, self.cdef)
self.add_deprecations(ir)

def add_deprecations(self, ir: ClassIR) -> None:
"""Deprecate the class if it's decorated with @deprecated (PEP 702).

These decorators aren't applied at runtime (see get_class_deprecations in
mypyc.irbuild.util). Set __deprecated__ like they do, and generate a function that
emits their warnings. The generated C code calls it when an instance of the class
itself (but not of a subclass) is created, and in __init_subclass__ if the class
doesn't define it (see mypyc.codegen.emitclass).
"""
deprecations = get_class_deprecations(self.cdef)
if not deprecations:
return
builder = self.builder
# The outermost decorator would be applied last.
message, _, _, line = deprecations[0]
builder.primitive_op(
py_setattr_op,
[self.type_obj, builder.load_str("__deprecated__"), builder.load_str(message)],
line,
)
if not self.deprecation_warnings:
return
builder.enter(MYPYC_WARN_DEPRECATED, ret_type=bool_rprimitive)
for deprecation in self.deprecation_warnings:
gen_deprecation_warning(builder, deprecation)
builder.add(Return(builder.true()))
arg_regs, args, blocks, ret_type, _ = builder.leave()
decl = FuncDecl(
MYPYC_WARN_DEPRECATED,
ir.name,
builder.module_name,
FuncSignature(args, ret_type),
FUNC_STATICMETHOD,
internal=True,
)
# This isn't added to ir.methods, so that it doesn't get a vtable entry. The
# layout of the vtable would otherwise depend on a decorator argument (category)
# that doesn't affect the interface of the module as mypy sees it, and modules
# with subclasses wouldn't be recompiled in incremental mode when it changes.
ir.method_decls[MYPYC_WARN_DEPRECATED] = decl
builder.functions.append(FuncIR(decl, arg_regs, blocks, self.cdef.line))


class DataClassBuilder(ExtClassBuilder):
Expand Down
35 changes: 35 additions & 0 deletions mypyc/irbuild/function.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
ComparisonOp,
GetAttr,
Integer,
LoadGlobal,
LoadLiteral,
Register,
Return,
Expand All @@ -57,6 +58,7 @@
RInstance,
bool_rprimitive,
c_int_rprimitive,
c_pyssize_t_rprimitive,
dict_rprimitive,
int_rprimitive,
object_rprimitive,
Expand All @@ -77,11 +79,13 @@
)
from mypyc.irbuild.generator import gen_generator_func, gen_generator_func_body
from mypyc.irbuild.targets import AssignmentTarget
from mypyc.irbuild.util import DeprecatedArgs
from mypyc.primitives.dict_ops import (
dict_get_method_with_none,
dict_new_op,
exact_dict_set_item_op,
)
from mypyc.primitives.exc_ops import warn_op
from mypyc.primitives.generic_ops import (
generic_getattr,
generic_setattr,
Expand Down Expand Up @@ -266,6 +270,8 @@ def c() -> None:
if is_nested or in_non_ext:
setup_callable_class(builder)

gen_deprecation_warnings(builder, fitem)

if is_generator:
# First generate a function that just constructs and returns a generator object.
func_ir, func_reg = gen_generator_func(
Expand Down Expand Up @@ -315,6 +321,35 @@ def gen_func_body(
return func_ir, func_reg


def gen_deprecation_warnings(builder: IRBuilder, fitem: FuncItem) -> None:
"""Generate the warnings that a function decorated with @deprecated emits when called.

These decorators aren't applied at runtime (see get_deprecated_args in
mypyc.irbuild.util). A generator or a coroutine warns when it's created, like the
wrapper of the runtime decorator does, and not when it starts to run.
"""
if not isinstance(fitem, FuncDef):
return
for deprecation in builder.fdefs_to_deprecations.get(fitem, []):
gen_deprecation_warning(builder, deprecation)


def gen_deprecation_warning(builder: IRBuilder, deprecation: DeprecatedArgs) -> None:
"""Generate the warning of a @deprecated decorator that isn't applied at runtime."""
message, category, stacklevel, line = deprecation
if category is None:
category_reg: Value = builder.add(
LoadGlobal(object_rprimitive, "PyExc_DeprecationWarning", line)
)
else:
category_reg = builder.accept(category)
builder.call_c(
warn_op,
[category_reg, builder.load_str(message), Integer(stacklevel, c_pyssize_t_rprimitive)],
line,
)


def has_nested_func_self_reference(builder: IRBuilder, fitem: FuncItem) -> bool:
"""Does a nested function contain a self-reference in its body?

Expand Down
Loading
Loading