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
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
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
28 changes: 28 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 @@ -82,6 +84,7 @@
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 +269,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 +320,29 @@ 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 message, category, stacklevel, line in builder.fdefs_to_deprecations.get(fitem, []):
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
14 changes: 11 additions & 3 deletions mypyc/irbuild/prebuildvisitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from mypy.types import Type
from mypyc.errors import Errors
from mypyc.irbuild.missingtypevisitor import MissingTypesVisitor
from mypyc.irbuild.util import DeprecatedArgs, get_compiled_deprecations, get_runtime_decorators


class _LambdaChecker(TraverserVisitor):
Expand Down Expand Up @@ -114,6 +115,10 @@ def __init__(
# Map function to indices of decorators to remove
self.decorators_to_remove: dict[FuncDef, list[int]] = decorators_to_remove

# Map function to its @deprecated decorators that aren't applied at runtime.
# The function emits their warnings when it's called instead.
self.funcs_to_deprecations: dict[FuncDef, list[DeprecatedArgs]] = {}

# A mapping of import groups (a series of Import nodes with
# nothing in between) where each group is keyed by its first
# import node.
Expand Down Expand Up @@ -166,19 +171,22 @@ def record_deleted_names(self, expr: Expression) -> None:
self.record_deleted_names(item)

def visit_decorator(self, dec: Decorator) -> None:
if dec.decorators:
if deprecations := get_compiled_deprecations(dec):
self.funcs_to_deprecations[dec.func] = deprecations
decorators = get_runtime_decorators(dec)
if decorators:
# Only add the function being decorated if there exist
# (ordinary) decorators in the decorator list. Certain
# decorators (such as @property, @abstractmethod) are
# special cased and removed from this list by
# mypy. Functions decorated only by special decorators
# (and property setters) are not treated as decorated
# functions by the IR builder.
if isinstance(dec.decorators[0], MemberExpr) and dec.decorators[0].name == "setter":
if isinstance(decorators[0], MemberExpr) and decorators[0].name == "setter":
# Property setters are not treated as decorated methods.
self.prop_setters.add(dec.func)
else:
decorators_to_store = dec.decorators.copy()
decorators_to_store = decorators
if dec.func in self.decorators_to_remove:
to_remove = self.decorators_to_remove[dec.func]

Expand Down
12 changes: 7 additions & 5 deletions mypyc/irbuild/prepare.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
default_attr_name,
get_func_def,
get_mypyc_attrs,
get_runtime_decorators,
is_class_body_final,
is_dataclass,
is_decorated_class,
Expand Down Expand Up @@ -313,9 +314,10 @@ def prepare_method_def(
# TODO: do something about abstract methods here. Currently, they are handled just like
# normal methods.
decl = prepare_func_def(module_name, cdef.name, node.func, mapper, options)
if not node.decorators:
decorators = get_runtime_decorators(node)
if not decorators:
ir.method_decls[node.name] = decl
elif isinstance(node.decorators[0], MemberExpr) and node.decorators[0].name == "setter":
elif isinstance(decorators[0], MemberExpr) and decorators[0].name == "setter":
# Make property setter name different than getter name so there are no
# name clashes when generating C code, and property lookup at the IR level
# works correctly.
Expand Down Expand Up @@ -373,9 +375,9 @@ def is_valid_multipart_property_def(prop: OverloadedFuncDef) -> bool:
isinstance(getter, Decorator)
and isinstance(setter, Decorator)
and getter.func.is_property
and len(setter.decorators) == 1
and isinstance(setter.decorators[0], MemberExpr)
and setter.decorators[0].name == "setter"
and len(decorators := get_runtime_decorators(setter)) == 1
and isinstance(decorators[0], MemberExpr)
and decorators[0].name == "setter"
)


Expand Down
83 changes: 81 additions & 2 deletions mypyc/irbuild/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from __future__ import annotations

from typing import Any, Final, Literal, TypedDict
from typing import Any, Final, Literal, NamedTuple, TypedDict
from typing_extensions import NotRequired

from mypy.nodes import (
Expand All @@ -27,12 +27,13 @@
StrExpr,
TempNode,
TupleExpr,
TypeInfo,
UnaryExpr,
Var,
is_class_var,
)
from mypy.semanal import refers_to_fullname
from mypy.types import FINAL_DECORATOR_NAMES
from mypy.types import DEPRECATED_TYPE_NAMES, FINAL_DECORATOR_NAMES
from mypyc.errors import Errors
from mypyc.ir.class_ir import ClassIR
from mypyc.ir.rtypes import RType, is_none_rprimitive, is_object_rprimitive, is_optional_type
Expand Down Expand Up @@ -346,6 +347,84 @@ def is_implicit_extension_class(cdef: ClassDef) -> tuple[bool, str]:
return True, ""


class DeprecatedArgs(NamedTuple):
"""Arguments of a @deprecated(...) decorator that is compiled into the function."""

message: str
# None means the default category (DeprecationWarning).
category: RefExpr | None
stacklevel: int
line: int


def get_deprecated_args(d: Expression) -> DeprecatedArgs | None:
"""Return the arguments of a @deprecated(...) decorator (PEP 702) that mypyc compiles.

The runtime decorator sets the "__deprecated__" attribute on the function that it
wraps, but compiled functions don't support setting attributes. A compiled function
emits the warning itself when it's called instead, and the decorator isn't applied
(see get_runtime_decorators).

This requires arguments that mean the same when they are evaluated on each call: a
string literal message, a module-level class or None as the category, and an int
literal stacklevel. Return None if the decorator is something else, and it will be
applied at runtime like other decorators.
"""
if not (
isinstance(d, CallExpr)
and refers_to_fullname(d.callee, DEPRECATED_TYPE_NAMES)
and d.arg_kinds
and d.arg_kinds[0] == ARG_POS
and isinstance(d.args[0], StrExpr)
):
return None
category: RefExpr | None = None
stacklevel = 1
for arg, kind, name in zip(d.args[1:], d.arg_kinds[1:], d.arg_names[1:]):
if kind != ARG_NAMED:
return None
if (
name == "category"
and isinstance(arg, RefExpr)
and arg.kind == GDEF
and (arg.fullname == "builtins.None" or isinstance(arg.node, TypeInfo))
):
category = arg
elif name == "stacklevel" and isinstance(arg, IntExpr):
stacklevel = arg.value
else:
return None
return DeprecatedArgs(d.args[0].value, category, stacklevel, d.line)


def get_runtime_decorators(dec: Decorator) -> list[Expression]:
"""Return the decorators that are applied to a function at runtime.

Mypy has already removed the decorators that it special cases, such as @property.
Also leave out @deprecated decorators that are compiled into the function. This
only includes the innermost ones, since others don't receive a compiled function.
"""
decorators = dec.decorators.copy()
while decorators and get_deprecated_args(decorators[-1]) is not None:
decorators.pop()
return decorators


def get_compiled_deprecations(dec: Decorator) -> list[DeprecatedArgs]:
"""Return the @deprecated decorators of a function that get_runtime_decorators omits.

The outermost decorator is first, since it emits its warning first. A decorator with
category=None emits no warning, so it has no item.
"""
deprecations = []
for d in dec.decorators[len(get_runtime_decorators(dec)) :]:
args = get_deprecated_args(d)
assert args is not None
if args.category is None or args.category.fullname != "builtins.None":
deprecations.append(args)
return deprecations


def get_func_def(op: FuncDef | Decorator | OverloadedFuncDef) -> FuncDef:
if isinstance(op, OverloadedFuncDef):
assert op.impl
Expand Down
5 changes: 5 additions & 0 deletions mypyc/lib-rt/CPy.h
Original file line number Diff line number Diff line change
Expand Up @@ -996,6 +996,11 @@ void CPy_TypeErrorTraceback(const char *filename, const char *funcname, int line
void CPy_AttributeError(const char *filename, const char *funcname, const char *classname,
const char *attrname, int line, PyObject *globals);

// Issue a warning, like warnings.warn(message, category, stack_level).
static inline int CPyErr_Warn(PyObject *category, PyObject *message, Py_ssize_t stack_level) {
return PyErr_WarnFormat(category, stack_level, "%U", message);
}


// Misc operations

Expand Down
22 changes: 20 additions & 2 deletions mypyc/primitives/exc_ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,16 @@
from __future__ import annotations

from mypyc.ir.ops import ERR_ALWAYS, ERR_FALSE, ERR_NEVER
from mypyc.ir.rtypes import bit_rprimitive, exc_rtuple, object_rprimitive, void_rtype
from mypyc.primitives.registry import custom_op, custom_primitive_op
from mypyc.ir.rtypes import (
bit_rprimitive,
c_int_rprimitive,
c_pyssize_t_rprimitive,
exc_rtuple,
object_rprimitive,
str_rprimitive,
void_rtype,
)
from mypyc.primitives.registry import ERR_NEG_INT, custom_op, custom_primitive_op

# If the argument is a class, raise an instance of the class. Otherwise, assume
# that the argument is an exception object, and raise it.
Expand Down Expand Up @@ -117,3 +125,13 @@
get_exc_info_op = custom_op(
arg_types=[], return_type=exc_rtuple, c_function_name="CPy_GetExcInfo", error_kind=ERR_NEVER
)

# Issue a warning, like warnings.warn(message, category, stacklevel). Arguments are
# (category, message, stack level). Compiled functions have no frame, so stack level 1
# refers to the closest interpreted caller.
warn_op = custom_op(
arg_types=[object_rprimitive, str_rprimitive, c_pyssize_t_rprimitive],
return_type=c_int_rprimitive,
c_function_name="CPyErr_Warn",
error_kind=ERR_NEG_INT,
)
Loading
Loading