diff --git a/docs/source/command_line.rst b/docs/source/command_line.rst index 20fb3821438a..92db5d59d0ee 100644 --- a/docs/source/command_line.rst +++ b/docs/source/command_line.rst @@ -537,6 +537,12 @@ potentially problematic or redundant in some way. This limitation will be removed in future releases of mypy. +.. option:: --report-deprecated-as-error + + By default, mypy emits notes if your code imports or uses deprecated + features. This flag converts such notes to errors, causing mypy to + eventually finish with a non-zero exit code. Features are considered + deprecated when decorated with ``warnings.deprecated``. .. _miscellaneous-strictness-flags: diff --git a/docs/source/error_code_list2.rst b/docs/source/error_code_list2.rst index 6d50e217a77d..eb18d76e2f2f 100644 --- a/docs/source/error_code_list2.rst +++ b/docs/source/error_code_list2.rst @@ -231,6 +231,44 @@ incorrect control flow or conditional checks that are accidentally always true o # Error: Statement is unreachable [unreachable] print('unreachable') +.. _code-deprecated: + +Check that imported or used feature is deprecated [deprecated] +-------------------------------------------------------------- + +By default, mypy generates a note if your code imports a deprecated feature explicitly with a +``from mod import depr`` statement or uses a deprecated feature imported otherwise or defined +locally. Features are considered deprecated when decorated with ``warnings.deprecated``, as +specified in `PEP 702 `_. You can silence single notes via +``# type: ignore[deprecated]`` or turn off this check completely via ``--disable-error-code=deprecated``. +Use the :option:`--report-deprecated-as-error ` option for +more strictness, which turns all such notes into errors. + +.. note:: + + The ``warnings`` module provides the ``@deprecated`` decorator since Python 3.13. + To use it with older Python versions, import it from ``typing_extensions`` instead. + +Examples: + +.. code-block:: python + + # mypy: report-deprecated-as-error + + # Error: abc.abstractproperty is deprecated: Deprecated, use 'property' with 'abstractmethod' instead + from abc import abstractproperty + + from typing_extensions import deprecated + + @deprecated("use new_function") + def old_function() -> None: + print("I am old") + + # Error: __main__.old_function is deprecated: use new_function + old_function() + old_function() # type: ignore[deprecated] + + .. _code-redundant-expr: Check that expression is redundant [redundant-expr] diff --git a/mypy/checker.py b/mypy/checker.py index 8d77bb02eeb2..4bbd49cd7198 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -2838,6 +2838,9 @@ def check_metaclass_compatibility(self, typ: TypeInfo) -> None: ) def visit_import_from(self, node: ImportFrom) -> None: + for name, _ in node.names: + if (sym := self.globals.get(name)) is not None: + self.warn_deprecated(sym.node, node) self.check_import(node) def visit_import_all(self, node: ImportAll) -> None: @@ -2926,6 +2929,16 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None: Handle all kinds of assignment statements (simple, indexed, multiple). """ + + if isinstance(s.rvalue, TempNode) and s.rvalue.no_rhs: + for lvalue in s.lvalues: + if ( + isinstance(lvalue, NameExpr) + and isinstance(var := lvalue.node, Var) + and isinstance(instance := get_proper_type(var.type), Instance) + ): + self.check_deprecated(instance.type, s) + # Avoid type checking type aliases in stubs to avoid false # positives about modern type syntax available in stubs such # as X | Y. @@ -4671,6 +4684,16 @@ def visit_operator_assignment_stmt(self, s: OperatorAssignmentStmt) -> None: if inplace: # There is __ifoo__, treat as x = x.__ifoo__(y) rvalue_type, method_type = self.expr_checker.check_op(method, lvalue_type, s.rvalue, s) + if isinstance(inst := get_proper_type(lvalue_type), Instance) and isinstance( + defn := inst.type.get_method(method), OverloadedFuncDef + ): + for item in defn.items: + if ( + isinstance(item, Decorator) + and isinstance(typ := item.func.type, CallableType) + and (bind_self(typ) == method_type) + ): + self.warn_deprecated(item.func, s) if not is_subtype(rvalue_type, lvalue_type): self.msg.incompatible_operator_assignment(s.op, s) else: @@ -7535,6 +7558,29 @@ def has_valid_attribute(self, typ: Type, name: str) -> bool: def get_expression_type(self, node: Expression, type_context: Type | None = None) -> Type: return self.expr_checker.accept(node, type_context=type_context) + def check_deprecated(self, node: SymbolNode | None, context: Context) -> None: + """Warn if deprecated and not directly imported with a `from` statement.""" + if isinstance(node, Decorator): + node = node.func + if isinstance(node, (FuncDef, OverloadedFuncDef, TypeInfo)) and ( + node.deprecated is not None + ): + for imp in self.tree.imports: + if isinstance(imp, ImportFrom) and any(node.name == n[0] for n in imp.names): + break + else: + self.warn_deprecated(node, context) + + def warn_deprecated(self, node: SymbolNode | None, context: Context) -> None: + """Warn if deprecated.""" + if isinstance(node, Decorator): + node = node.func + if isinstance(node, (FuncDef, OverloadedFuncDef, TypeInfo)) and ( + (deprecated := node.deprecated) is not None + ): + warn = self.msg.fail if self.options.report_deprecated_as_error else self.msg.note + warn(deprecated, context, code=codes.DEPRECATED) + class CollectArgTypeVarTypes(TypeTraverserVisitor): """Collects the non-nested argument types in a set.""" diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 98e6eb6a7fc3..0754b1db7141 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -126,6 +126,7 @@ validate_instance, ) from mypy.typeops import ( + bind_self, callable_type, custom_special_method, erase_to_union_or_bound, @@ -354,7 +355,9 @@ def visit_name_expr(self, e: NameExpr) -> Type: """ self.chk.module_refs.update(extract_refexpr_names(e)) result = self.analyze_ref_expr(e) - return self.narrow_type_from_binder(e, result) + narrowed = self.narrow_type_from_binder(e, result) + self.chk.check_deprecated(e.node, e) + return narrowed def analyze_ref_expr(self, e: RefExpr, lvalue: bool = False) -> Type: result: Type | None = None @@ -1479,6 +1482,10 @@ def check_call_expr_with_callee_type( object_type=object_type, ) proper_callee = get_proper_type(callee_type) + if isinstance(e.callee, NameExpr) and isinstance(e.callee.node, OverloadedFuncDef): + for item in e.callee.node.items: + if isinstance(item, Decorator) and (item.func.type == callee_type): + self.chk.check_deprecated(item.func, e) if isinstance(e.callee, RefExpr) and isinstance(proper_callee, CallableType): # Cache it for find_isinstance_check() if proper_callee.type_guard is not None: @@ -3267,7 +3274,9 @@ def visit_member_expr(self, e: MemberExpr, is_lvalue: bool = False) -> Type: """Visit member expression (of form e.id).""" self.chk.module_refs.update(extract_refexpr_names(e)) result = self.analyze_ordinary_member_access(e, is_lvalue) - return self.narrow_type_from_binder(e, result) + narrowed = self.narrow_type_from_binder(e, result) + self.chk.warn_deprecated(e.node, e) + return narrowed def analyze_ordinary_member_access(self, e: MemberExpr, is_lvalue: bool) -> Type: """Analyse member expression or member lvalue.""" @@ -3956,7 +3965,7 @@ def lookup_definer(typ: Instance, attr_name: str) -> str | None: # This is the case even if the __add__ method is completely missing and the __radd__ # method is defined. - variants_raw = [(left_op, left_type, right_expr)] + variants_raw = [(op_name, left_op, left_type, right_expr)] elif ( is_subtype(right_type, left_type) and isinstance(left_type, Instance) @@ -3977,19 +3986,25 @@ def lookup_definer(typ: Instance, attr_name: str) -> str | None: # As a special case, the alt_promote check makes sure that we don't use the # __radd__ method of int if the LHS is a native int type. - variants_raw = [(right_op, right_type, left_expr), (left_op, left_type, right_expr)] + variants_raw = [ + (rev_op_name, right_op, right_type, left_expr), + (op_name, left_op, left_type, right_expr), + ] else: # In all other cases, we do the usual thing and call __add__ first and # __radd__ second when doing "A() + B()". - variants_raw = [(left_op, left_type, right_expr), (right_op, right_type, left_expr)] + variants_raw = [ + (op_name, left_op, left_type, right_expr), + (rev_op_name, right_op, right_type, left_expr), + ] # STEP 3: # We now filter out all non-existent operators. The 'variants' list contains # all operator methods that are actually present, in the order that Python # attempts to invoke them. - variants = [(op, obj, arg) for (op, obj, arg) in variants_raw if op is not None] + variants = [(na, op, obj, arg) for (na, op, obj, arg) in variants_raw if op is not None] # STEP 4: # We now try invoking each one. If an operation succeeds, end early and return @@ -3998,13 +4013,23 @@ def lookup_definer(typ: Instance, attr_name: str) -> str | None: errors = [] results = [] - for method, obj, arg in variants: + for name, method, obj, arg in variants: with self.msg.filter_errors(save_filtered_errors=True) as local_errors: result = self.check_method_call(op_name, obj, method, [arg], [ARG_POS], context) if local_errors.has_new_errors(): errors.append(local_errors.filtered_errors()) results.append(result) else: + if isinstance(obj, Instance) and isinstance( + defn := obj.type.get_method(name), OverloadedFuncDef + ): + for item in defn.items: + if ( + isinstance(item, Decorator) + and isinstance(typ := item.func.type, CallableType) + and bind_self(typ) == result[1] + ): + self.chk.check_deprecated(item.func, context) return result # We finish invoking above operators and no early return happens. Therefore, diff --git a/mypy/checkmember.py b/mypy/checkmember.py index adfd5c2a97d6..4448072d52c3 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -316,9 +316,12 @@ def analyze_instance_member_access( if method.is_property: assert isinstance(method, OverloadedFuncDef) - first_item = method.items[0] - assert isinstance(first_item, Decorator) - return analyze_var(name, first_item.var, typ, info, mx) + getter = method.items[0] + assert isinstance(getter, Decorator) + if mx.is_lvalue and (len(items := method.items) > 1): + mx.chk.warn_deprecated(items[1], mx.context) + return analyze_var(name, getter.var, typ, info, mx) + if mx.is_lvalue: mx.msg.cant_assign_to_method(mx.context) if not isinstance(method, OverloadedFuncDef): @@ -493,6 +496,8 @@ def analyze_member_var_access( # It was not a method. Try looking up a variable. v = lookup_member_var_or_accessor(info, name, mx.is_lvalue) + mx.chk.warn_deprecated(v, mx.context) + vv = v if isinstance(vv, Decorator): # The associated Var node of a decorator contains the type. @@ -1010,6 +1015,8 @@ def analyze_class_attribute_access( # on the class object itself rather than the instance. return None + mx.chk.warn_deprecated(node.node, mx.context) + is_decorated = isinstance(node.node, Decorator) is_method = is_decorated or isinstance(node.node, FuncBase) if mx.is_lvalue: diff --git a/mypy/errorcodes.py b/mypy/errorcodes.py index a170b5d4d65a..b835f27bbad9 100644 --- a/mypy/errorcodes.py +++ b/mypy/errorcodes.py @@ -304,5 +304,11 @@ def __hash__(self) -> int: "General", ) +DEPRECATED: Final = ErrorCode( + "deprecated", + "Warn when importing or using deprecated (overloaded) functions, methods or classes", + "General", +) + # This copy will not include any error codes defined later in the plugins. mypy_error_codes = error_codes.copy() diff --git a/mypy/errors.py b/mypy/errors.py index d6dcd4e49e13..13452b14a237 100644 --- a/mypy/errors.py +++ b/mypy/errors.py @@ -20,7 +20,7 @@ # Show error codes for some note-level messages (these usually appear alone # and not as a comment for a previous error-level message). -SHOW_NOTE_CODES: Final = {codes.ANNOTATION_UNCHECKED} +SHOW_NOTE_CODES: Final = {codes.ANNOTATION_UNCHECKED, codes.DEPRECATED} # Do not add notes with links to error code docs to errors with these codes. # We can tweak this set as we get more experience about what is helpful and what is not. @@ -194,6 +194,9 @@ def on_error(self, file: str, info: ErrorInfo) -> bool: Return True to filter out the error, preventing it from being seen by other ErrorWatcher further down the stack and from being recorded by Errors """ + if info.code == codes.DEPRECATED: + return False + self._has_new_errors = True if isinstance(self._filter, bool): should_filter = self._filter diff --git a/mypy/main.py b/mypy/main.py index f177bb1c2062..0674e3b7e79b 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -799,6 +799,13 @@ def add_invertible_flag( help="Warn about statements or expressions inferred to be unreachable", group=lint_group, ) + add_invertible_flag( + "--report-deprecated-as-error", + default=False, + strict_flag=False, + help="Report importing or using deprecated features as errors instead of notes", + group=lint_group, + ) # Note: this group is intentionally added here even though we don't add # --strict to this group near the end. diff --git a/mypy/nodes.py b/mypy/nodes.py index 39cbee3c8525..c4d23ca7bff8 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -561,17 +561,19 @@ class OverloadedFuncDef(FuncBase, SymbolNode, Statement): Overloaded variants must be consecutive in the source file. """ - __slots__ = ("items", "unanalyzed_items", "impl") + __slots__ = ("items", "unanalyzed_items", "impl", "deprecated") items: list[OverloadPart] unanalyzed_items: list[OverloadPart] impl: OverloadPart | None + deprecated: str | None def __init__(self, items: list[OverloadPart]) -> None: super().__init__() self.items = items self.unanalyzed_items = items.copy() self.impl = None + self.deprecated = None if items: # TODO: figure out how to reliably set end position (we don't know the impl here). self.set_line(items[0].line, items[0].column) @@ -596,6 +598,7 @@ def serialize(self) -> JsonDict: "fullname": self._fullname, "impl": None if self.impl is None else self.impl.serialize(), "flags": get_flags(self, FUNCBASE_FLAGS), + "deprecated": self.deprecated, } @classmethod @@ -615,6 +618,7 @@ def deserialize(cls, data: JsonDict) -> OverloadedFuncDef: res.type = typ res._fullname = data["fullname"] set_flags(res, data["flags"]) + res.deprecated = data["deprecated"] # NOTE: res.info will be set in the fixup phase. return res @@ -781,6 +785,7 @@ class FuncDef(FuncItem, SymbolNode, Statement): # Present only when a function is decorated with @typing.datasclass_transform or similar "dataclass_transform_spec", "docstring", + "deprecated", ) __match_args__ = ("name", "arguments", "type", "body") @@ -810,6 +815,7 @@ def __init__( self.is_mypy_only = False self.dataclass_transform_spec: DataclassTransformSpec | None = None self.docstring: str | None = None + self.deprecated: str | None = None @property def name(self) -> str: @@ -840,6 +846,7 @@ def serialize(self) -> JsonDict: if self.dataclass_transform_spec is None else self.dataclass_transform_spec.serialize() ), + "deprecated": self.deprecated, } @classmethod @@ -867,6 +874,7 @@ def deserialize(cls, data: JsonDict) -> FuncDef: if data["dataclass_transform_spec"] is not None else None ) + ret.deprecated = data["deprecated"] # Leave these uninitialized so that future uses will trigger an error del ret.arguments del ret.max_pos @@ -2942,6 +2950,7 @@ class is generic then it will be a type constructor of higher kind. "self_type", "dataclass_transform_spec", "is_type_check_only", + "deprecated", ) _fullname: str # Fully qualified name @@ -3095,6 +3104,9 @@ class is generic then it will be a type constructor of higher kind. # Is set to `True` when class is decorated with `@typing.type_check_only` is_type_check_only: bool + # The type's deprecation message (in case it is deprecated) + deprecated: str | None + FLAGS: Final = [ "is_abstract", "is_enum", @@ -3152,6 +3164,7 @@ def __init__(self, names: SymbolTable, defn: ClassDef, module_name: str) -> None self.self_type = None self.dataclass_transform_spec = None self.is_type_check_only = False + self.deprecated = None def add_type_vars(self) -> None: self.has_type_var_tuple_type = False @@ -3374,6 +3387,7 @@ def serialize(self) -> JsonDict: if self.dataclass_transform_spec is not None else None ), + "deprecated": self.deprecated, } return data @@ -3441,6 +3455,7 @@ def deserialize(cls, data: JsonDict) -> TypeInfo: ti.dataclass_transform_spec = DataclassTransformSpec.deserialize( data["dataclass_transform_spec"] ) + ti.deprecated = data.get("deprecated") return ti diff --git a/mypy/options.py b/mypy/options.py index 56bd92957b41..d315d297e023 100644 --- a/mypy/options.py +++ b/mypy/options.py @@ -173,6 +173,9 @@ def __init__(self) -> None: # declared with a precise type self.warn_return_any = False + # Report importing or using deprecated features as errors instead of notes. + self.report_deprecated_as_error = False + # Warn about unused '# type: ignore' comments self.warn_unused_ignores = False diff --git a/mypy/semanal.py b/mypy/semanal.py index 27abf2c1dc4c..37aafc3b2647 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -249,6 +249,7 @@ from mypy.types import ( ASSERT_TYPE_NAMES, DATACLASS_TRANSFORM_NAMES, + DEPRECATED_TYPE_NAMES, FINAL_DECORATOR_NAMES, FINAL_TYPE_NAMES, IMPORTED_REVEAL_TYPE_NAMES, @@ -1260,10 +1261,51 @@ def analyze_overloaded_func_def(self, defn: OverloadedFuncDef) -> None: return # We know this is an overload def. Infer properties and perform some checks. + self.process_deprecated_overload(defn) self.process_final_in_overload(defn) self.process_static_or_class_method_in_overload(defn) self.process_overload_impl(defn) + def process_deprecated_overload(self, defn: OverloadedFuncDef) -> None: + if defn.is_property: + return + + if isinstance(impl := defn.impl, Decorator) and ( + (deprecated := impl.func.deprecated) is not None + ): + defn.deprecated = deprecated + for item in defn.items: + if isinstance(item, Decorator): + item.func.deprecated = deprecated + + for item in defn.items: + deprecation = False + if isinstance(item, Decorator): + for d in item.decorators: + if deprecation and refers_to_fullname(d, OVERLOAD_NAMES): + self.msg.note("@overload should be placed before @deprecated", d) + elif (deprecated := self.get_deprecated(d)) is not None: + deprecation = True + if isinstance(typ := item.func.type, CallableType): + typestr = f" {typ} " + else: + typestr = " " + item.func.deprecated = ( + f"overload{typestr}of function {defn.fullname} is deprecated: " + f"{deprecated}" + ) + + @staticmethod + def get_deprecated(expression: Expression) -> str | None: + if ( + isinstance(expression, CallExpr) + and refers_to_fullname(expression.callee, DEPRECATED_TYPE_NAMES) + and (len(args := expression.args) >= 1) + and isinstance(deprecated := args[0], StrExpr) + ): + return deprecated.value + return None + def process_overload_impl(self, defn: OverloadedFuncDef) -> None: """Set flags for an overload implementation. @@ -1440,15 +1482,17 @@ def analyze_property_with_multi_part_definition(self, defn: OverloadedFuncDef) - for i, item in enumerate(items[1:]): if isinstance(item, Decorator): if len(item.decorators) >= 1: - node = item.decorators[0] - if isinstance(node, MemberExpr): - if node.name == "setter": + first_node = item.decorators[0] + if isinstance(first_node, MemberExpr): + if first_node.name == "setter": # The first item represents the entire property. first_item.var.is_settable_property = True # Get abstractness from the original definition. item.func.abstract_status = first_item.func.abstract_status - if node.name == "deleter": + if first_node.name == "deleter": item.func.abstract_status = first_item.func.abstract_status + for other_node in item.decorators[1:]: + other_node.accept(self) else: self.fail( f"Only supported top decorator is @{first_item.func.name}.setter", item @@ -1460,6 +1504,14 @@ def analyze_property_with_multi_part_definition(self, defn: OverloadedFuncDef) - for i in reversed(deleted_items): del items[i] + for item in items[1:]: + if isinstance(item, Decorator): + for d in item.decorators: + if (deprecated := self.get_deprecated(d)) is not None: + item.func.deprecated = ( + f"function {item.fullname} is deprecated: {deprecated}" + ) + def add_function_to_symbol_table(self, func: FuncDef | OverloadedFuncDef) -> None: if self.is_class_scope(): assert self.type is not None @@ -1663,6 +1715,8 @@ def visit_decorator(self, dec: Decorator) -> None: d.callee, DATACLASS_TRANSFORM_NAMES ): dec.func.dataclass_transform_spec = self.parse_dataclass_transform_spec(d) + elif (deprecated := self.get_deprecated(d)) is not None: + dec.func.deprecated = f"function {dec.fullname} is deprecated: {deprecated}" elif not dec.var.is_property: # We have seen a "non-trivial" decorator before seeing @property, if # we will see a @property later, give an error, as we don't support this. @@ -2099,6 +2153,8 @@ def analyze_class_decorator_common( info.is_final = True elif refers_to_fullname(decorator, TYPE_CHECK_ONLY_NAMES): info.is_type_check_only = True + elif (deprecated := self.get_deprecated(decorator)) is not None: + info.deprecated = f"class {defn.fullname} is deprecated: {deprecated}" def clean_up_bases_and_infer_type_variables( self, defn: ClassDef, base_type_exprs: list[Expression], context: Context diff --git a/mypy/server/astdiff.py b/mypy/server/astdiff.py index d5a303128126..131a13ffd62d 100644 --- a/mypy/server/astdiff.py +++ b/mypy/server/astdiff.py @@ -256,6 +256,7 @@ def snapshot_definition(node: SymbolNode | None, common: SymbolSnapshot) -> Symb signature, is_trivial_body, dataclass_transform_spec.serialize() if dataclass_transform_spec is not None else None, + node.deprecated if isinstance(node, FuncDef) else None, ) elif isinstance(node, Var): return ("Var", common, snapshot_optional_type(node.type), node.is_final) diff --git a/test-data/unit/check-deprecated.test b/test-data/unit/check-deprecated.test new file mode 100644 index 000000000000..f587034d8059 --- /dev/null +++ b/test-data/unit/check-deprecated.test @@ -0,0 +1,396 @@ +-- Type checker test cases for reporting deprecations. + + +[case testDeprecatedDisableNotes] +# flags: --disable-error-code=deprecated + +from typing_extensions import deprecated + +@deprecated("use f2 instead") +def f() -> None: ... + +f() + +[builtins fixtures/tuple.pyi] + + +[case testDeprecatedAsNoteWithErrorCode] +# flags: --show-error-codes + +from typing_extensions import deprecated + +@deprecated("use f2 instead") +def f() -> None: ... + +f() # type: ignore[deprecated] +f() # N: function __main__.f is deprecated: use f2 instead [deprecated] + +[builtins fixtures/tuple.pyi] + + +[case testDeprecatedAsErrorWithErrorCode] +# flags: --report-deprecated-as-error --show-error-codes + +from typing_extensions import deprecated + +@deprecated("use f2 instead") +def f() -> None: ... + +f() # type: ignore[deprecated] +f() # E: function __main__.f is deprecated: use f2 instead [deprecated] + +[builtins fixtures/tuple.pyi] + + +[case testDeprecatedFunction] + +from typing_extensions import deprecated + +@deprecated("use f2 instead") +def f() -> None: ... + +f # N: function __main__.f is deprecated: use f2 instead # type: ignore[deprecated] +f(1) # N: function __main__.f is deprecated: use f2 instead \ + # E: Too many arguments for "f" +f[1] # N: function __main__.f is deprecated: use f2 instead \ + # E: Value of type "Callable[[], None]" is not indexable +g = f # N: function __main__.f is deprecated: use f2 instead +g() +t = (f, f, g) # N: function __main__.f is deprecated: use f2 instead + +[builtins fixtures/tuple.pyi] + + +[case testDeprecatedFunctionDifferentModule] + +import m +import p.s +import m as n +import p.s as ps +from m import f # N: function m.f is deprecated: use f2 instead +from p.s import g # N: function p.s.g is deprecated: use g2 instead +from k import * + +m.f() # N: function m.f is deprecated: use f2 instead +p.s.g() # N: function p.s.g is deprecated: use g2 instead +n.f() # N: function m.f is deprecated: use f2 instead +ps.g() # N: function p.s.g is deprecated: use g2 instead +f() +g() +h() # N: function k.h is deprecated: use h2 instead + +[file m.py] +from typing_extensions import deprecated + +@deprecated("use f2 instead") +def f() -> None: ... + +[file p/s.py] +from typing_extensions import deprecated + +@deprecated("use g2 instead") +def g() -> None: ... + +[file k.py] +from typing_extensions import deprecated + +@deprecated("use h2 instead") +def h() -> None: ... + +[builtins fixtures/tuple.pyi] + + +[case testDeprecatedClass] + +from typing_extensions import deprecated + +@deprecated("use C2 instead") +class C: ... + +c: C # N: class __main__.C is deprecated: use C2 instead +C() # N: class __main__.C is deprecated: use C2 instead +C.missing() # N: class __main__.C is deprecated: use C2 instead \ + # E: "Type[C]" has no attribute "missing" +C.__init__(c) # N: class __main__.C is deprecated: use C2 instead +C(1) # N: class __main__.C is deprecated: use C2 instead \ + # E: Too many arguments for "C" +D = C # N: class __main__.C is deprecated: use C2 instead +D() +t = (C, C, D) # N: class __main__.C is deprecated: use C2 instead + +[builtins fixtures/tuple.pyi] + + +[case testDeprecatedClassDifferentModule] + +import m +import p.s +import m as n +import p.s as ps +from m import C # N: class m.C is deprecated: use C2 instead +from p.s import D # N: class p.s.D is deprecated: use D2 instead +from k import * + +m.C() # N: class m.C is deprecated: use C2 instead +p.s.D() # N: class p.s.D is deprecated: use D2 instead +n.C() # N: class m.C is deprecated: use C2 instead +ps.D() # N: class p.s.D is deprecated: use D2 instead +C() +D() +E() # N: class k.E is deprecated: use E2 instead + +[file m.py] +from typing_extensions import deprecated + +@deprecated("use C2 instead") +class C: ... + +[file p/s.py] +from typing_extensions import deprecated + +@deprecated("use D2 instead") +class D: ... + +[file k.py] +from typing_extensions import deprecated + +@deprecated("use E2 instead") +class E: ... + +[builtins fixtures/tuple.pyi] + + +[case testDeprecatedClassInitMethod] + +from typing_extensions import deprecated + +@deprecated("use C2 instead") +class C: + def __init__(self) -> None: ... + +c: C # N: class __main__.C is deprecated: use C2 instead +C() # N: class __main__.C is deprecated: use C2 instead +C.__init__(c) # N: class __main__.C is deprecated: use C2 instead + +[builtins fixtures/tuple.pyi] + + +[case testDeprecatedSpecialMethods] + +from typing import Iterator +from typing_extensions import deprecated + +class A: + @deprecated("no A + int") + def __add__(self, v: int) -> None: ... + + @deprecated("no int + A") + def __radd__(self, v: int) -> None: ... + + @deprecated("no A = A + int") + def __iadd__(self, v: int) -> A: ... + + @deprecated("no iteration") + def __iter__(self) -> Iterator[int]: ... + + @deprecated("no in") + def __contains__(self, v: int) -> int: ... + + @deprecated("no integer") + def __int__(self) -> int: ... + + @deprecated("no inversion") + def __invert__(self) -> A: ... + +class B: + @deprecated("still no in") + def __contains__(self, v: int) -> int: ... + +a = A() +b = B() +a + 1 # N: function __main__.A.__add__ is deprecated: no A + int +1 + a # N: function __main__.A.__radd__ is deprecated: no int + A +a += 1 # N: function __main__.A.__iadd__ is deprecated: no A = A + int +for i in a: # N: function __main__.A.__iter__ is deprecated: no iteration + reveal_type(i) # N: Revealed type is "builtins.int" +1 in a # N: function __main__.A.__contains__ is deprecated: no in +1 in b # N: function __main__.B.__contains__ is deprecated: still no in +~a # N: function __main__.A.__invert__ is deprecated: no inversion + +[builtins fixtures/tuple.pyi] + + +[case testDeprecatedOverloadedSpecialMethods] + +from typing import Iterator, Union +from typing_extensions import deprecated, overload + +class A: + @overload + @deprecated("no A + int") + def __add__(self, v: int) -> None: ... + @overload + def __add__(self, v: str) -> None: ... + def __add__(self, v: Union[int, str]) -> None: ... + + @overload + def __radd__(self, v: int) -> None: ... + @overload + @deprecated("no str + A") + def __radd__(self, v: str) -> None: ... + def __radd__(self, v: Union[int, str]) -> None: ... + + @overload + def __iadd__(self, v: int) -> A: ... + @overload + def __iadd__(self, v: str) -> A: ... + @deprecated("no A += Any") + def __iadd__(self, v: Union[int, str]) -> A: ... + +a = A() +a + 1 # N: overload def (__main__.A, builtins.int) of function __main__.A.__add__ is deprecated: no A + int +a + "x" +1 + a +"x" + a # N: overload def (__main__.A, builtins.str) of function __main__.A.__radd__ is deprecated: no str + A +a += 1 # N: function __main__.A.__iadd__ is deprecated: no A += Any +a += "x" # N: function __main__.A.__iadd__ is deprecated: no A += Any + +[builtins fixtures/tuple.pyi] + + +[case testDeprecatedMethod] + +from typing_extensions import deprecated + +class C: + @deprecated("use g instead") + def f(self) -> None: ... + + def g(self) -> None: ... + + @staticmethod + @deprecated("use g instead") + def h() -> None: ... + + @deprecated("use g instead") + @staticmethod + def k() -> None: ... + +C.f # N: function __main__.C.f is deprecated: use g instead +C().f # N: function __main__.C.f is deprecated: use g instead +C().f() # N: function __main__.C.f is deprecated: use g instead +C().f(1) # N: function __main__.C.f is deprecated: use g instead \ + # E: Too many arguments for "f" of "C" +f = C().f # N: function __main__.C.f is deprecated: use g instead +f() +t = (C.f, C.f, C.g) # N: function __main__.C.f is deprecated: use g instead + +C().g() +C().h() # N: function __main__.C.h is deprecated: use g instead +C().k() # N: function __main__.C.k is deprecated: use g instead + +[builtins fixtures/callable.pyi] + + +[case testDeprecatedClassWithDeprecatedMethod] + +from typing_extensions import deprecated + +@deprecated("use D instead") +class C: + @deprecated("use g instead") + def f(self) -> None: ... + def g(self) -> None: ... + +C().f() # N: class __main__.C is deprecated: use D instead \ + # N: function __main__.C.f is deprecated: use g instead +C().g() # N: class __main__.C is deprecated: use D instead + +[builtins fixtures/callable.pyi] + + +[case testDeprecatedProperty] + +from typing_extensions import deprecated + +class C: + @property + @deprecated("use f2 instead") + def f(self) -> int: ... + + @property + def g(self) -> int: ... + @g.setter + @deprecated("use g2 instead") + def g(self, v: int) -> None: ... + + +C.f # N: function __main__.C.f is deprecated: use f2 instead +C().f # N: function __main__.C.f is deprecated: use f2 instead +C().f() # N: function __main__.C.f is deprecated: use f2 instead \ + # E: "int" not callable +C().f = 1 # N: function __main__.C.f is deprecated: use f2 instead \ + # E: Property "f" defined in "C" is read-only + + +C.g +C().g +C().g = 1 # N: function __main__.C.g is deprecated: use g2 instead +C().g = "x" # N: function __main__.C.g is deprecated: use g2 instead \ + # E: Incompatible types in assignment (expression has type "str", variable has type "int") + +[builtins fixtures/property.pyi] + + +[case testDeprecatedOverloadedFunction] + +from typing import Union +from typing_extensions import deprecated, overload + +@overload +def f(x: int) -> int: ... +@overload +def f(x: str) -> str: ... +@deprecated("use f2 instead") +def f(x: Union[int, str]) -> Union[int, str]: ... + +f # N: function __main__.f is deprecated: use f2 instead +f(1) # N: function __main__.f is deprecated: use f2 instead +f("x") # N: function __main__.f is deprecated: use f2 instead +f(1.0) # N: function __main__.f is deprecated: use f2 instead \ + # E: No overload variant of "f" matches argument type "float" \ + # N: Possible overload variants: \ + # N: def f(x: int) -> int \ + # N: def f(x: str) -> str + +@overload +@deprecated("work with str instead") +def g(x: int) -> int: ... +@overload +def g(x: str) -> str: ... +def g(x: Union[int, str]) -> Union[int, str]: ... + +g +g(1) # N: overload def (x: builtins.int) -> builtins.int of function __main__.g is deprecated: work with str instead +g("x") +g(1.0) # E: No overload variant of "g" matches argument type "float" \ + # N: Possible overload variants: \ + # N: def g(x: int) -> int \ + # N: def g(x: str) -> str + +@overload +def h(x: int) -> int: ... +@deprecated("work with int instead") +@overload # N: @overload should be placed before @deprecated +def h(x: str) -> str: ... +def h(x: Union[int, str]) -> Union[int, str]: ... + +h +h(1) +h("x") # N: overload def (x: builtins.str) -> builtins.str of function __main__.h is deprecated: work with int instead +h(1.0) # E: No overload variant of "h" matches argument type "float" \ + # N: Possible overload variants: \ + # N: def h(x: int) -> int \ + # N: def h(x: str) -> str + +[builtins fixtures/tuple.pyi] diff --git a/test-data/unit/fine-grained.test b/test-data/unit/fine-grained.test index 15e47ff296ea..d4c61cbf1d5b 100644 --- a/test-data/unit/fine-grained.test +++ b/test-data/unit/fine-grained.test @@ -10671,3 +10671,237 @@ class f: ... == main:2: error: Incompatible types in assignment (expression has type "f", variable has type "int") main:4: error: Incompatible types in assignment (expression has type "f", variable has type "int") + + +[case testDeprecatedAddKeepChangeAndRemoveFunctionDeprecation] +from a import f +f() +import a +a.f() + +[file a.py] +def f() -> None: ... + +[file a.py.2] +from typing_extensions import deprecated +@deprecated("use f2 instead") +def f() -> None: ... + +[file a.py.3] +from typing_extensions import deprecated +@deprecated("use f2 instead") +def f() -> None: ... + +[file a.py.4] +from typing_extensions import deprecated +@deprecated("use f3 instead") +def f() -> None: ... + +[file a.py.5] +def f() -> None: ... + +[builtins fixtures/tuple.pyi] +[out] +== +main:1: note: function a.f is deprecated: use f2 instead +main:4: note: function a.f is deprecated: use f2 instead +== +main:1: note: function a.f is deprecated: use f2 instead +main:4: note: function a.f is deprecated: use f2 instead +== +main:1: note: function a.f is deprecated: use f3 instead +main:4: note: function a.f is deprecated: use f3 instead +== + + +[case testDeprecatedRemoveFunctionDeprecation] +from a import f +f() +import a +a.f() + +[file a.py] +from typing_extensions import deprecated +@deprecated("use f2 instead") +def f() -> None: ... + +[file a.py.2] +def f() -> None: ... + +[builtins fixtures/tuple.pyi] +[out] +main:1: note: function a.f is deprecated: use f2 instead +main:4: note: function a.f is deprecated: use f2 instead +== + +[case testDeprecatedKeepFunctionDeprecation] +from a import f +f() +import a +a.f() + +[file a.py] +from typing_extensions import deprecated +@deprecated("use f2 instead") +def f() -> None: ... + +[file a.py.2] +from typing_extensions import deprecated +@deprecated("use f2 instead") +def f() -> None: ... + +[builtins fixtures/tuple.pyi] +[out] +main:1: note: function a.f is deprecated: use f2 instead +main:4: note: function a.f is deprecated: use f2 instead +== +main:1: note: function a.f is deprecated: use f2 instead +main:4: note: function a.f is deprecated: use f2 instead + + +[case testDeprecatedAddFunctionDeprecationIndirectImport] +from b import f +f() +import b +b.f() + +[file b.py] +from a import f + +[file a.py] +def f() -> int: ... + +[file a.py.2] +from typing_extensions import deprecated +@deprecated("use f2 instead") +def f() -> int: ... + +[builtins fixtures/tuple.pyi] +[out] +== +b.py:1: note: function a.f is deprecated: use f2 instead +main:1: note: function a.f is deprecated: use f2 instead +main:4: note: function a.f is deprecated: use f2 instead + + +[case testDeprecatedChangeFunctionDeprecationIndirectImport] +from b import f +f() +import b +b.f() + +[file b.py] +from a import f + +[file a.py] +from typing_extensions import deprecated +@deprecated("use f1 instead") +def f() -> int: ... + +[file a.py.2] +from typing_extensions import deprecated +@deprecated("use f2 instead") +def f() -> int: ... + +[builtins fixtures/tuple.pyi] +[out] +b.py:1: note: function a.f is deprecated: use f1 instead +main:1: note: function a.f is deprecated: use f1 instead +main:4: note: function a.f is deprecated: use f1 instead +== +b.py:1: note: function a.f is deprecated: use f2 instead +main:1: note: function a.f is deprecated: use f2 instead +main:4: note: function a.f is deprecated: use f2 instead + +[case testDeprecatedRemoveFunctionDeprecationIndirectImport] +from b import f +f() +import b +b.f() + +[file b.py] +from a import f + +[file a.py] +from typing_extensions import deprecated +@deprecated("use f1 instead") +def f() -> int: ... + +[file a.py.2] +def f() -> int: ... + +[builtins fixtures/tuple.pyi] +[out] +b.py:1: note: function a.f is deprecated: use f1 instead +main:1: note: function a.f is deprecated: use f1 instead +main:4: note: function a.f is deprecated: use f1 instead +== + +[case testDeprecatedFunctionAlreadyDecorated1-only_when_cache] +from b import f +x: str = f() +import b +y: str = b.f() + +[file b.py] +from a import f + +[file a.py] +from typing import Callable + +def d(t: Callable[[], str]) -> Callable[[], str]: ... + +@d +def f() -> str: ... + +[file a.py.2] +from typing import Callable +from typing_extensions import deprecated + +def d(t: Callable[[], str]) -> Callable[[], str]: ... + +@deprecated("deprecated decorated function") +@d +def f() -> str: ... + +[builtins fixtures/tuple.pyi] +[out] +== +b.py:1: note: function a.f is deprecated: deprecated decorated function +main:1: note: function a.f is deprecated: deprecated decorated function +main:4: note: function a.f is deprecated: deprecated decorated function + + +[case testDeprecatedFunctionAlreadyDecorated2-only_when_nocache] +from b import f +x: str = f() +import b +y: str = b.f() + +[file b.py] +from a import f + +[file a.py] +from typing import Callable + +def d(t: Callable[[], str]) -> Callable[[], str]: ... + +@d +def f() -> str: ... + +[file a.py.2] +from typing import Callable +from typing_extensions import deprecated + +def d(t: Callable[[], str]) -> Callable[[], str]: ... + +@deprecated("deprecated decorated function") +@d +def f() -> str: ... + +[builtins fixtures/tuple.pyi] +[out] +== +main:1: note: function a.f is deprecated: deprecated decorated function +main:4: note: function a.f is deprecated: deprecated decorated function +b.py:1: note: function a.f is deprecated: deprecated decorated function diff --git a/test-data/unit/pythoneval.test b/test-data/unit/pythoneval.test index 89f01bff963e..174ba8e98c3f 100644 --- a/test-data/unit/pythoneval.test +++ b/test-data/unit/pythoneval.test @@ -954,7 +954,7 @@ a = [] # type: List[Dict[str, str]] sorted(a, key=lambda y: y['']) [case testAbstractProperty] -from abc import abstractproperty, ABCMeta +from abc import abstractproperty, ABCMeta # type: ignore[deprecated] class A(metaclass=ABCMeta): @abstractproperty def x(self) -> int: pass