diff --git a/mypyc/codegen/emitclass.py b/mypyc/codegen/emitclass.py index 69c0b3e7c8c6..f489516249bf 100644 --- a/mypyc/codegen/emitclass.py +++ b/mypyc/codegen/emitclass.py @@ -33,6 +33,7 @@ GENERATOR_HELPER_NAME, IS_FREE_THREADED, MYPYC_DEFAULTS_SETUP, + MYPYC_WARN_DEPRECATED, NATIVE_PREFIX, PREFIX, REG_PREFIX, @@ -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) @@ -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"] @@ -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 @@ -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) @@ -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)") @@ -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: @@ -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( diff --git a/mypyc/common.py b/mypyc/common.py index c5736a53ba9f..664481c38b06 100644 --- a/mypyc/common.py +++ b/mypyc/common.py @@ -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__" diff --git a/mypyc/doc/differences_from_python.rst b/mypyc/doc/differences_from_python.rst index b945d92d6abe..97cb2b858ef3 100644 --- a/mypyc/doc/differences_from_python.rst +++ b/mypyc/doc/differences_from_python.rst @@ -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 --------------- diff --git a/mypyc/doc/native_classes.rst b/mypyc/doc/native_classes.rst index f7d91808d912..ade123aefd7f 100644 --- a/mypyc/doc/native_classes.rst +++ b/mypyc/doc/native_classes.rst @@ -222,10 +222,18 @@ decorators can be used with native classes, however: * ``mypy_extensions.mypyc_attr`` (see :ref:`above `) * ``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 diff --git a/mypyc/irbuild/builder.py b/mypyc/irbuild/builder.py index 1b61cc0cd744..0224506d2fc3 100644 --- a/mypyc/irbuild/builder.py +++ b/mypyc/irbuild/builder.py @@ -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 diff --git a/mypyc/irbuild/classdef.py b/mypyc/irbuild/classdef.py index b1751c64eab1..3e794fb4d843 100644 --- a/mypyc/irbuild/classdef.py +++ b/mypyc/irbuild/classdef.py @@ -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, @@ -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, @@ -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, @@ -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: @@ -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): diff --git a/mypyc/irbuild/function.py b/mypyc/irbuild/function.py index 3afca5c6aef8..0db68ce8cfc5 100644 --- a/mypyc/irbuild/function.py +++ b/mypyc/irbuild/function.py @@ -45,6 +45,7 @@ ComparisonOp, GetAttr, Integer, + LoadGlobal, LoadLiteral, Register, Return, @@ -57,6 +58,7 @@ RInstance, bool_rprimitive, c_int_rprimitive, + c_pyssize_t_rprimitive, dict_rprimitive, int_rprimitive, object_rprimitive, @@ -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, @@ -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( @@ -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? diff --git a/mypyc/irbuild/prebuildvisitor.py b/mypyc/irbuild/prebuildvisitor.py index 2d9535f06a52..a282e3f1eb9b 100644 --- a/mypyc/irbuild/prebuildvisitor.py +++ b/mypyc/irbuild/prebuildvisitor.py @@ -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): @@ -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. @@ -166,7 +171,10 @@ 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 @@ -174,11 +182,11 @@ def visit_decorator(self, dec: Decorator) -> None: # 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] diff --git a/mypyc/irbuild/prepare.py b/mypyc/irbuild/prepare.py index f2e8141b3f41..026b4d2b741f 100644 --- a/mypyc/irbuild/prepare.py +++ b/mypyc/irbuild/prepare.py @@ -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, @@ -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. @@ -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" ) diff --git a/mypyc/irbuild/util.py b/mypyc/irbuild/util.py index 85e4d499f3b0..b8a68679f660 100644 --- a/mypyc/irbuild/util.py +++ b/mypyc/irbuild/util.py @@ -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 ( @@ -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 @@ -315,6 +316,7 @@ def is_decorated_class(cdef: ClassDef) -> bool: and not is_dataclass_decorator(d) and not get_mypyc_attr_call(d) and not is_final_decorator(d) + and get_deprecated_args(d) is None for d in cdef.decorators ) @@ -346,6 +348,109 @@ def is_implicit_extension_class(cdef: ClassDef) -> tuple[bool, str]: return True, "" +class DeprecatedArgs(NamedTuple): + """Arguments of a @deprecated(...) decorator that is compiled into a function or class.""" + + message: str + # None means the default category (DeprecationWarning). + category: RefExpr | None + stacklevel: int + line: int + + @property + def emits_warning(self) -> bool: + """Does the decorator emit a warning? It only sets __deprecated__ if category=None.""" + return self.category is None or self.category.fullname != "builtins.None" + + +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.emits_warning: + deprecations.append(args) + return deprecations + + +def get_class_deprecations(cdef: ClassDef) -> list[DeprecatedArgs]: + """Return the @deprecated decorators of a class that are compiled into a native class. + + The runtime decorator replaces __new__ and __init_subclass__ of the class, so that + creating an instance or a subclass emits the warning. Native classes don't support + this: instances are created without looking up __new__, and it can't be replaced + anyway. A native class emits the warnings itself instead, and the decorators aren't + applied (see ExtClassBuilder.add_deprecations in mypyc.irbuild.classdef). + + The outermost decorator is first, since it emits its warning first. The decorators + of a non-extension class are all applied at runtime, including these. + """ + deprecations = [] + for d in cdef.decorators: + args = get_deprecated_args(d) + if args is not None: + deprecations.append(args) + return deprecations + + def get_func_def(op: FuncDef | Decorator | OverloadedFuncDef) -> FuncDef: if isinstance(op, OverloadedFuncDef): assert op.impl diff --git a/mypyc/lib-rt/CPy.h b/mypyc/lib-rt/CPy.h index 64cd25f0c335..307c89ebc1b3 100644 --- a/mypyc/lib-rt/CPy.h +++ b/mypyc/lib-rt/CPy.h @@ -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 @@ -1049,6 +1054,7 @@ PyObject *CPyType_FromTemplateWrapper(PyObject *template_, PyObject *orig_bases, PyObject *modname); bool CPy_InitSubclass(PyObject *type); +PyObject *CPy_SuperInitSubclass(PyObject *type, PyObject *cls, PyObject *args, PyObject *kwds); int CPyDataclass_SleightOfHand(PyObject *dataclass_dec, PyObject *tp, PyObject *dict, PyObject *annotations, PyObject *dataclass_type); diff --git a/mypyc/lib-rt/misc_ops.c b/mypyc/lib-rt/misc_ops.c index edb8bdd9789b..c05e691b4912 100644 --- a/mypyc/lib-rt/misc_ops.c +++ b/mypyc/lib-rt/misc_ops.c @@ -364,6 +364,24 @@ bool CPy_InitSubclass(PyObject *type) { return true; } +// Call super(type, cls).__init_subclass__(*args, **kwds). This is used by the +// __init_subclass__ method of a native class that is decorated with @deprecated. +PyObject *CPy_SuperInitSubclass(PyObject *type, PyObject *cls, PyObject *args, PyObject *kwds) { + PyObject *super_args[2] = {type, cls}; + PyObject *super = PyObject_Vectorcall((PyObject *)&PySuper_Type, super_args, 2, NULL); + if (super == NULL) { + return NULL; + } + PyObject *func = PyObject_GetAttr(super, mypyc_interned_str.__init_subclass__); + Py_DECREF(super); + if (func == NULL) { + return NULL; + } + PyObject *result = PyObject_Call(func, args, kwds); + Py_DECREF(func); + return result; +} + static int _CPy_UpdateObjFromDict(PyObject *obj, PyObject *dict) { Py_ssize_t pos = 0; diff --git a/mypyc/primitives/exc_ops.py b/mypyc/primitives/exc_ops.py index 528a89ef2331..d4d328f1cbbd 100644 --- a/mypyc/primitives/exc_ops.py +++ b/mypyc/primitives/exc_ops.py @@ -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. @@ -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, +) diff --git a/mypyc/test-data/irbuild-basic.test b/mypyc/test-data/irbuild-basic.test index f22f9afff3d3..2c0656a49f4b 100644 --- a/mypyc/test-data/irbuild-basic.test +++ b/mypyc/test-data/irbuild-basic.test @@ -2868,6 +2868,124 @@ L2: CPyImport_ReplaceModuleCacheForImport(r10, r9) return 1 +[case testDeprecatedDecorator] +from typing_extensions import deprecated + +@deprecated("use g()") +def f(x: int) -> int: + return x + 1 + +@deprecated("old", category=UserWarning, stacklevel=2) +def g() -> None: + pass + +@deprecated("no runtime warning", category=None) +def h() -> None: + pass + +class C: + @deprecated("use n()") + def m(self) -> int: + return 1 + + @property + @deprecated("p is deprecated") + def p(self) -> int: + return 2 + +def call(c: C) -> int: + return c.m() + c.p +[out] +def f(x): + x :: int + r0 :: object + r1 :: str + r2 :: i32 + r3 :: bit + r4 :: int +L0: + r0 = load_global PyExc_DeprecationWarning :: static + r1 = 'use g()' + r2 = CPyErr_Warn(r0, r1, 1) + r3 = r2 >= 0 :: signed + r4 = CPyTagged_Add(x, 2) + return r4 +def g(): + r0 :: object + r1 :: str + r2 :: object + r3 :: str + r4 :: i32 + r5 :: bit +L0: + r0 = builtins :: module + r1 = 'UserWarning' + r2 = CPyObject_GetAttr(r0, r1) + r3 = 'old' + r4 = CPyErr_Warn(r2, r3, 2) + r5 = r4 >= 0 :: signed + return 1 +def h(): +L0: + return 1 +def C.m(self): + self :: __main__.C + r0 :: object + r1 :: str + r2 :: i32 + r3 :: bit +L0: + r0 = load_global PyExc_DeprecationWarning :: static + r1 = 'use n()' + r2 = CPyErr_Warn(r0, r1, 1) + r3 = r2 >= 0 :: signed + return 2 +def C.p(self): + self :: __main__.C + r0 :: object + r1 :: str + r2 :: i32 + r3 :: bit +L0: + r0 = load_global PyExc_DeprecationWarning :: static + r1 = 'p is deprecated' + r2 = CPyErr_Warn(r0, r1, 1) + r3 = r2 >= 0 :: signed + return 4 +def call(c): + c :: __main__.C + r0, r1, r2 :: int +L0: + r0 = c.m() + r1 = c.p + r2 = CPyTagged_Add(r0, r1) + return r2 + +[case testDeprecatedDecoratorFromWarnings] +from warnings import deprecated + +@deprecated("use g()") +def f() -> None: + pass +[file warnings.pyi] +from typing import Callable, TypeVar + +_T = TypeVar("_T") + +def deprecated(__msg: str) -> Callable[[_T], _T]: ... +[out] +def f(): + r0 :: object + r1 :: str + r2 :: i32 + r3 :: bit +L0: + r0 = load_global PyExc_DeprecationWarning :: static + r1 = 'use g()' + r2 = CPyErr_Warn(r0, r1, 1) + r3 = r2 >= 0 :: signed + return 1 + [case testAnyAllG] from typing import Iterable diff --git a/mypyc/test-data/irbuild-classes.test b/mypyc/test-data/irbuild-classes.test index 59ab6ced0391..0333c7af9d40 100644 --- a/mypyc/test-data/irbuild-classes.test +++ b/mypyc/test-data/irbuild-classes.test @@ -1579,6 +1579,84 @@ L0: r3 = r2 == r0 return r3 +[case testDeprecatedClass] +from typing import Any +from mypy_extensions import mypyc_attr +from typing_extensions import deprecated + +@deprecated("use D") +class C: + def m(self) -> int: + return 1 + +@deprecated("old", category=UserWarning, stacklevel=2) +@mypyc_attr(native_class=True) +class D: + def __init_subclass__(cls, **kwargs: Any) -> None: + pass + +@deprecated("no runtime warning", category=None) +class E: + pass + +def f() -> int: + return C().m() +[out] +def C.m(self): + self :: __main__.C +L0: + return 2 +def C.__mypyc_warn_deprecated(): + r0 :: object + r1 :: str + r2 :: i32 + r3 :: bit +L0: + r0 = load_global PyExc_DeprecationWarning :: static + r1 = 'use D' + r2 = CPyErr_Warn(r0, r1, 1) + r3 = r2 >= 0 :: signed + return 1 +def D.__init_subclass__(cls, kwargs): + cls :: object + kwargs :: dict + r0 :: object + r1 :: str + r2 :: object + r3 :: str + r4 :: i32 + r5 :: bit +L0: + r0 = builtins :: module + r1 = 'UserWarning' + r2 = CPyObject_GetAttr(r0, r1) + r3 = 'old' + r4 = CPyErr_Warn(r2, r3, 2) + r5 = r4 >= 0 :: signed + return 1 +def D.__mypyc_warn_deprecated(): + r0 :: object + r1 :: str + r2 :: object + r3 :: str + r4 :: i32 + r5 :: bit +L0: + r0 = builtins :: module + r1 = 'UserWarning' + r2 = CPyObject_GetAttr(r0, r1) + r3 = 'old' + r4 = CPyErr_Warn(r2, r3, 2) + r5 = r4 >= 0 :: signed + return 1 +def f(): + r0 :: __main__.C + r1 :: int +L0: + r0 = C() + r1 = r0.m() + return r1 + [case testReservedName] from typing import Any, overload diff --git a/mypyc/test-data/run-classes.test b/mypyc/test-data/run-classes.test index d8965568af3d..7a8ab35884b8 100644 --- a/mypyc/test-data/run-classes.test +++ b/mypyc/test-data/run-classes.test @@ -1344,6 +1344,273 @@ assert GrandChild.required == 10, f"expected 10, got {GrandChild.required}" assert NoOverride.name == "base" assert NoOverride.required == 4, f"expected 4, got {NoOverride.required}" +[case testDeprecatedClass] +from dataclasses import dataclass +from typing import Any, Dict, List +from mypy_extensions import mypyc_attr, trait +from typing_extensions import deprecated + +class CustomWarning(UserWarning): + pass + +# This would be a compile error if the decorator made the class non-native. +@deprecated("Old is deprecated") +@mypyc_attr(native_class=True) +class Old: + def __init__(self, x: int) -> None: + self.x = x + + def get(self) -> int: + return self.x + +class Child(Old): + pass + +@deprecated("NoInit is deprecated") +class NoInit: + pass + +@deprecated("Custom is deprecated", category=CustomWarning, stacklevel=2) +class Custom: + pass + +@deprecated("Silent is deprecated", category=None) +class Silent: + pass + +class SilentChild(Silent): + pass + +@deprecated("first") +@deprecated("second", category=CustomWarning) +class Twice: + pass + +@deprecated("OldError is deprecated") +class OldError(Exception): + pass + +class ChildError(OldError): + pass + +@deprecated("OldDict is deprecated") +class OldDict(Dict[str, int]): + pass + +@deprecated("Open is deprecated") +@mypyc_attr(allow_interpreted_subclasses=True) +class Open: + def __init__(self) -> None: + self.y = 2 + +events: List[str] = [] + +@mypyc_attr(allow_interpreted_subclasses=True) +@deprecated("Hook is deprecated") +class Hook: + def __init_subclass__(cls, **kwargs: Any) -> None: + events.append(cls.__name__) + super().__init_subclass__(**kwargs) + +class HookChild(Hook): + pass + +@deprecated("Data is deprecated") +@dataclass +class Data: + x: int + y: str = "a" + +@deprecated("Tr is deprecated") +@trait +class Tr: + def t(self) -> int: + return 5 + +class Impl(Tr): + pass + +@deprecated("New is deprecated") +class New: + x: int + + def __new__(cls, x: int) -> "New": + self = object.__new__(cls) + self.x = x + 100 + return self + +@deprecated("WithDel is deprecated") +class WithDel: + def __init__(self) -> None: + events.append("init") + + def __del__(self) -> None: + events.append("del") + +def make_old() -> Old: + return Old(1) + +def make_child() -> Child: + return Child(2) + +def make_error() -> OldError: + return OldError("x") + +def make_data() -> Data: + return Data(1) + +def make_new() -> New: + return New(1) + +def get(o: Old) -> int: + return o.get() + o.x + +[file driver.py] +import warnings +from typing import Any, Callable + +D = DeprecationWarning + +# Creating a subclass of a deprecated class emits the warning. +with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + import native +assert [str(w.message) for w in caught] == [ + "Old is deprecated", + "OldError is deprecated", + "Hook is deprecated", + "Tr is deprecated", +], caught + +from native import ( + Child, + ChildError, + Custom, + CustomWarning, + Data, + Hook, + HookChild, + Impl, + New, + NoInit, + Old, + OldDict, + OldError, + Open, + Silent, + SilentChild, + Tr, + Twice, + WithDel, + events, + get, + make_child, + make_data, + make_error, + make_new, + make_old, +) +from testutil import assertRaises + +def check(fn: Callable[[], Any], expected: Any, *messages: str, category: Any = D) -> None: + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + result = fn() + assert result == expected, result + assert [str(w.message) for w in caught] == list(messages), caught + for w in caught: + assert w.category is category, w.category + # Compiled functions have no frames, so this is the closest interpreted caller. + assert w.filename == __file__, w.filename + +def subclass(base: Any, **kwargs: Any) -> Any: + class Sub(base, **kwargs): + pass + return Sub + +assert Old.__deprecated__ == "Old is deprecated" +assert Child.__deprecated__ == "Old is deprecated" +assert "__deprecated__" not in Child.__dict__ +assert Custom.__deprecated__ == "Custom is deprecated" +assert Silent.__deprecated__ == "Silent is deprecated" +assert Twice.__deprecated__ == "first" +assert OldError.__deprecated__ == "OldError is deprecated" +assert Data.__deprecated__ == "Data is deprecated" +assert Tr.__deprecated__ == "Tr is deprecated" + +# Only creating an instance of the class itself emits the warning. +check(lambda: Old(1).get(), 1, "Old is deprecated") +check(lambda: Old(x=1).x, 1, "Old is deprecated") +check(lambda: make_old().x, 1, "Old is deprecated") +check(lambda: type(Old.__new__(Old, 1)), Old, "Old is deprecated") +check(lambda: get(Child(2)), 4) +check(lambda: make_child().x, 2) +check(lambda: type(NoInit()), NoInit, "NoInit is deprecated") +check(lambda: type(Custom()), Custom, "Custom is deprecated", category=CustomWarning) +check(lambda: type(Silent()), Silent) +check(lambda: type(SilentChild()), SilentChild) + +with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + Twice() +assert [(w.category, str(w.message)) for w in caught] == [ + (D, "first"), + (CustomWarning, "second"), +], caught + +check(lambda: repr(OldError("x")), "OldError('x')", "OldError is deprecated") +check(lambda: repr(make_error()), "OldError('x')", "OldError is deprecated") +check(lambda: repr(ChildError("y")), "ChildError('y')") +check(lambda: OldDict(a=1), {"a": 1}, "OldDict is deprecated") +check(lambda: Open().y, 2, "Open is deprecated") +check(lambda: type(Hook()), Hook, "Hook is deprecated") +check(lambda: type(HookChild()), HookChild) +check(lambda: repr(Data(1)), "Data(x=1, y='a')", "Data is deprecated") +check(lambda: make_data().x, 1, "Data is deprecated") +check(lambda: Impl().t(), 5) +check(lambda: New(1).x, 101, "New is deprecated") +check(lambda: make_new().x, 101, "New is deprecated") + +# Subclasses that are created at runtime +assert events == ["HookChild"] +check(lambda: subclass(Open)().y, 2, "Open is deprecated") +check(lambda: subclass(subclass(Open))().y, 2, "Open is deprecated", "Open is deprecated") +check(lambda: type(subclass(Hook)()).__name__, "Sub", "Hook is deprecated") +check(lambda: subclass(HookChild).__name__, "Sub", "Hook is deprecated") +assert events == ["HookChild", "Sub", "Sub"] +check(lambda: subclass(OldError)("z").args, ("z",), "OldError is deprecated") +check(lambda: subclass(OldDict)(a=1), {"a": 1}, "OldDict is deprecated") +check(lambda: subclass(Silent).__name__, "Sub") + +# Class keyword arguments are passed on to the next __init_subclass__. +with warnings.catch_warnings(): + warnings.simplefilter("ignore") + with assertRaises(TypeError, "takes no keyword arguments"): + subclass(Open, x=1) + +# The warning is raised like any other exception if warnings are errors. +del events[:] +with warnings.catch_warnings(): + warnings.simplefilter("error") + with assertRaises(DeprecationWarning, "Old is deprecated"): + Old(1) + with assertRaises(DeprecationWarning, "Old is deprecated"): + make_old() + with assertRaises(CustomWarning, "Custom is deprecated"): + Custom() + with assertRaises(DeprecationWarning, "OldError is deprecated"): + OldError() + with assertRaises(DeprecationWarning, "New is deprecated"): + New(1) + with assertRaises(DeprecationWarning, "Open is deprecated"): + subclass(Open) + # The instance isn't created, so neither __init__ nor __del__ is called. + with assertRaises(DeprecationWarning, "WithDel is deprecated"): + WithDel() + assert events == [], events + assert Child(1).x == 1 + Silent() + [case testDefaultVars] from typing import Optional class A: diff --git a/mypyc/test-data/run-functions.test b/mypyc/test-data/run-functions.test index b0f184c64bf2..853ede0fc5c7 100644 --- a/mypyc/test-data/run-functions.test +++ b/mypyc/test-data/run-functions.test @@ -1268,6 +1268,277 @@ def foo(): pass def test_decorator_name(): assert foo.__name__ == "foo" +[case testDeprecatedDecorator] +from typing import Callable, Iterator, Union, overload +from mypy_extensions import mypyc_attr +from typing_extensions import deprecated + +class CustomWarning(UserWarning): + pass + +@deprecated("f is deprecated") +def f(x: int = 1) -> int: + return x + 1 + +@deprecated("custom is deprecated", category=CustomWarning, stacklevel=2) +def custom() -> int: + return 2 + +@deprecated("silent is deprecated", category=None) +def silent() -> int: + return 3 + +@deprecated("gen is deprecated") +def gen() -> Iterator[int]: + yield 1 + yield 2 + +@deprecated("co is deprecated") +async def co() -> int: + return 4 + +@overload +def ov(x: int) -> int: ... +@overload +@deprecated("ov(str) is deprecated") +def ov(x: str) -> str: ... +@deprecated("ov is deprecated") +def ov(x: Union[int, str]) -> Union[int, str]: + return x + +def identity(fn: Callable[[], int]) -> Callable[[], int]: + return fn + +def add_100(fn: Callable[[], int]) -> Callable[[], int]: + def wrapper() -> int: + return fn() + 100 + return wrapper + +@identity +@deprecated("under is deprecated") +def under() -> int: + return 5 + +# Only the innermost decorators are compiled into the function. This one is applied +# at runtime to the result of add_100. +@deprecated("over is deprecated") +@add_100 +def over() -> int: + return 6 + +def nested() -> Callable[[], int]: + @deprecated("nested is deprecated") + def inner() -> int: + return 7 + return inner + +# Without a literal message the decorator is also applied at runtime. +def nested_dynamic(message: str) -> Callable[[], int]: + @deprecated(message) + def inner() -> int: + return 8 + return inner + +class Base: + def __init__(self) -> None: + self._q = 0 + + def plain(self) -> int: + return 1 + + @deprecated("Base.old is deprecated") + def old(self) -> int: + return 2 + + @property + @deprecated("Base.p is deprecated") + def p(self) -> int: + return 3 + + @property + def q(self) -> int: + return self._q + + @q.setter + @deprecated("setting Base.q is deprecated") + def q(self, value: int) -> None: + self._q = value + + @classmethod + @deprecated("Base.cm is deprecated") + def cm(cls) -> str: + return cls.__name__ + + @staticmethod + @deprecated("Base.sm is deprecated") + def sm() -> int: + return 4 + + @deprecated("Base.aco is deprecated") + async def aco(self) -> int: + return 5 + +class Child(Base): + @deprecated("Child.plain is deprecated") + def plain(self) -> int: + return 10 + + def old(self) -> int: + return 20 + +@mypyc_attr(native_class=False) +class NonNative: + @deprecated("NonNative.m is deprecated") + def m(self) -> int: + return 9 + +@deprecated("Cls is deprecated") +class Cls: + def __init__(self, x: int) -> None: + self.x = x + +@deprecated("SilentCls is deprecated", category=None) +class SilentCls: + pass + +def call_f() -> int: + return f(2) + +def call_methods(b: Base) -> int: + return b.plain() + b.old() + b.p + +def set_q(b: Base, value: int) -> None: + b.q = value + +def make_cls() -> Cls: + return Cls(1) + +[file driver.py] +import asyncio +import warnings +from typing import Any, Callable + +from native import ( + Base, + Child, + Cls, + CustomWarning, + NonNative, + SilentCls, + call_f, + call_methods, + co, + custom, + f, + gen, + make_cls, + nested, + nested_dynamic, + ov, + over, + set_q, + silent, + under, +) +from testutil import assertRaises + +D = DeprecationWarning + +def check(fn: Callable[[], Any], expected: Any, *messages: str, category: Any = D) -> None: + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + result = fn() + assert result == expected, result + assert [str(w.message) for w in caught] == list(messages), caught + for w in caught: + assert w.category is category, w.category + # Compiled functions have no frames, so this is the closest interpreted caller. + assert w.filename == __file__, w.filename + +check(f, 2, "f is deprecated") +check(lambda: f(5), 6, "f is deprecated") +check(lambda: f(x=5), 6, "f is deprecated") +check(call_f, 3, "f is deprecated") +check(custom, 2, "custom is deprecated", category=CustomWarning) +check(silent, 3) +check(lambda: ov(1), 1, "ov is deprecated") +check(lambda: ov("x"), "x", "ov is deprecated") +check(under, 5, "under is deprecated") +check(over, 106, "over is deprecated") +check(nested(), 7, "nested is deprecated") +check(nested_dynamic("dynamic message"), 8, "dynamic message") + +def direct() -> int: + return f() + +def indirect() -> int: + return custom() + +def call_indirect() -> int: + return indirect() + +with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + direct() + call_indirect() +assert [w.lineno for w in caught] == [ + direct.__code__.co_firstlineno + 1, + call_indirect.__code__.co_firstlineno + 1, +], caught + +# A generator or a coroutine warns when it's created. +def create_gen() -> None: + gen() + +check(create_gen, None, "gen is deprecated") +check(lambda: list(gen()), [1, 2], "gen is deprecated") +check(lambda: co().close(), None, "co is deprecated") +check(lambda: Base().aco().close(), None, "Base.aco is deprecated") +with warnings.catch_warnings(): + warnings.simplefilter("ignore") + assert asyncio.run(co()) == 4 + assert asyncio.run(Base().aco()) == 5 + +b = Base() +c = Child() +check(b.plain, 1) +check(b.old, 2, "Base.old is deprecated") +check(lambda: b.p, 3, "Base.p is deprecated") +check(c.plain, 10, "Child.plain is deprecated") +check(c.old, 20) +check(lambda: c.p, 3, "Base.p is deprecated") +check(lambda: call_methods(b), 6, "Base.old is deprecated", "Base.p is deprecated") +check(lambda: call_methods(c), 33, "Child.plain is deprecated", "Base.p is deprecated") + +def set_q_attr() -> int: + b.q = 5 + return b.q + +check(set_q_attr, 5, "setting Base.q is deprecated") +check(lambda: set_q(b, 6), None, "setting Base.q is deprecated") +check(lambda: b.q, 6) + +check(Base.cm, "Base", "Base.cm is deprecated") +check(c.cm, "Child", "Base.cm is deprecated") +check(Base.sm, 4, "Base.sm is deprecated") +check(b.sm, 4, "Base.sm is deprecated") +check(lambda: NonNative().m(), 9, "NonNative.m is deprecated") + +check(lambda: Cls(1).x, 1, "Cls is deprecated") +check(lambda: make_cls().x, 1, "Cls is deprecated") +check(lambda: isinstance(SilentCls(), SilentCls), True) + +# The warning is raised like any other exception if warnings are errors. +with warnings.catch_warnings(): + warnings.simplefilter("error") + with assertRaises(DeprecationWarning, "f is deprecated"): + f() + with assertRaises(DeprecationWarning, "Base.p is deprecated"): + b.p + with assertRaises(CustomWarning, "custom is deprecated"): + custom() + assert silent() == 3 + [case testLambdaArgToOverloaded] from lib import sub diff --git a/mypyc/test-data/run-multimodule.test b/mypyc/test-data/run-multimodule.test index c5a614c95044..22f0fe90acd0 100644 --- a/mypyc/test-data/run-multimodule.test +++ b/mypyc/test-data/run-multimodule.test @@ -1091,6 +1091,72 @@ from other_b import B d = native.Data(A(), B(), B()) assert isinstance(d.b, B) +[case testMultiModuleDeprecatedClass] +from typing import Any +from other import Base, BaseError + +class Child(Base): + def __init_subclass__(cls, **kwargs: Any) -> None: + super().__init_subclass__(**kwargs) + + def get(self) -> int: + return super().get() + 10 + +class GrandChild(Child): + pass + +class ChildError(BaseError): + pass + +def make_base() -> Base: + return Base(1) + +def make_child() -> Child: + return Child(2) + +def make_error() -> BaseError: + return BaseError("x") + +[file other.py] +from typing_extensions import deprecated + +@deprecated("Base is deprecated") +class Base: + def __init__(self, x: int) -> None: + self.x = x + + def get(self) -> int: + return self.x + +@deprecated("BaseError is deprecated") +class BaseError(Exception): + pass + +[file driver.py] +import warnings +from typing import Any, Callable + +def check(fn: Callable[[], Any], expected: Any, *messages: str) -> None: + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + result = fn() + assert result == expected, result + assert [str(w.message) for w in caught] == list(messages), caught + +def import_native() -> None: + import native + +# Creating Child, GrandChild and ChildError +check(import_native, None, "Base is deprecated", "Base is deprecated", "BaseError is deprecated") + +from native import ChildError, GrandChild, make_base, make_child, make_error + +check(lambda: make_base().get(), 1, "Base is deprecated") +check(lambda: make_child().get(), 12) +check(lambda: GrandChild(3).get(), 13) +check(lambda: repr(make_error()), "BaseError('x')", "BaseError is deprecated") +check(lambda: repr(ChildError("y")), "ChildError('y')") + [case testMultiModuleRelative] from package.a import f [file package/__init__.py] @@ -1596,6 +1662,60 @@ print(get(), same_instance()) [rechecked native, other] [rechecked2 native] +[case testIncrementalCompilationDeprecatedClassWarning] +# Mypy can't see whether a deprecated class emits a warning (category=None), so +# native.py isn't recompiled when that changes. +from other import C + +class D(C): + def g(self) -> int: + return self.f() + 1 + +def make() -> C: + return C() + +def call(c: C) -> int: + return c.f() + c.h() + +[file other.py] +from typing_extensions import deprecated + +@deprecated("C is deprecated", category=None) +class C: + def f(self) -> int: + return 1 + + def h(self) -> int: + return 10 + +[file other.py.2] +from typing_extensions import deprecated + +@deprecated("C is deprecated") +class C: + def f(self) -> int: + return 1 + + def h(self) -> int: + return 10 + +[file driver.py] +import warnings + +with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + from native import D, call, make + print(call(make()), call(D()), D().g()) +print("warnings:", ", ".join(str(w.message) for w in caught)) +[out] +11 11 2 +warnings: +[out2] +11 11 2 +warnings: C is deprecated, C is deprecated + +[rechecked other] + [case testIncrementalCompilationWithNonClassTypeDef] import other_a [file other_a.py] diff --git a/test-data/unit/lib-stub/typing_extensions.pyi b/test-data/unit/lib-stub/typing_extensions.pyi index 47bae94a7f7d..4bf0ce82a30e 100644 --- a/test-data/unit/lib-stub/typing_extensions.pyi +++ b/test-data/unit/lib-stub/typing_extensions.pyi @@ -101,7 +101,9 @@ def dataclass_transform( ) -> Callable[[_T], _T]: ... def override(__arg: _T) -> _T: ... -def deprecated(__msg: str) -> Callable[[_T], _T]: ... +def deprecated( + __msg: str, *, category: Union[Type[Any], None] = ..., stacklevel: int = ... +) -> Callable[[_T], _T]: ... def disjoint_base(__arg: _T) -> _T: ... _FutureFeatureFixture = 0