From 2bd25ba695ee6e002e90632b23d61b0745ec6e0c Mon Sep 17 00:00:00 2001 From: Ryan Heard Date: Sat, 10 Oct 2026 11:09:06 -0400 Subject: [PATCH] [mypyc] Support @deprecated on functions and methods --- mypyc/doc/differences_from_python.rst | 19 ++ mypyc/irbuild/builder.py | 1 + mypyc/irbuild/function.py | 28 ++ mypyc/irbuild/prebuildvisitor.py | 14 +- mypyc/irbuild/prepare.py | 12 +- mypyc/irbuild/util.py | 83 +++++- mypyc/lib-rt/CPy.h | 5 + mypyc/primitives/exc_ops.py | 22 +- mypyc/test-data/irbuild-basic.test | 118 ++++++++ mypyc/test-data/run-functions.test | 271 ++++++++++++++++++ test-data/unit/lib-stub/typing_extensions.pyi | 4 +- 11 files changed, 564 insertions(+), 13 deletions(-) diff --git a/mypyc/doc/differences_from_python.rst b/mypyc/doc/differences_from_python.rst index b945d92d6abed..97cb2b858ef32 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/irbuild/builder.py b/mypyc/irbuild/builder.py index 1b61cc0cd7444..0224506d2fc3d 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/function.py b/mypyc/irbuild/function.py index 3afca5c6aef84..c5ee8f5dff589 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, @@ -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, @@ -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( @@ -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? diff --git a/mypyc/irbuild/prebuildvisitor.py b/mypyc/irbuild/prebuildvisitor.py index 2d9535f06a52c..a282e3f1eb9bb 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 f2e8141b3f41d..026b4d2b741fd 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 85e4d499f3b0e..680cc7621a8c6 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 @@ -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 diff --git a/mypyc/lib-rt/CPy.h b/mypyc/lib-rt/CPy.h index 64cd25f0c3351..a5d1e3872978d 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 diff --git a/mypyc/primitives/exc_ops.py b/mypyc/primitives/exc_ops.py index 528a89ef23310..d4d328f1cbbd3 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 f22f9afff3d32..2c0656a49f4be 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/run-functions.test b/mypyc/test-data/run-functions.test index b0f184c64bf21..853ede0fc5c73 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/test-data/unit/lib-stub/typing_extensions.pyi b/test-data/unit/lib-stub/typing_extensions.pyi index 47bae94a7f7d7..4bf0ce82a30e3 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