diff --git a/docs/source/command_line.rst b/docs/source/command_line.rst index 50a6ef65f4d0..019e576b6e79 100644 --- a/docs/source/command_line.rst +++ b/docs/source/command_line.rst @@ -533,6 +533,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 0655ef2d35d8..aa45fe2ecbdf 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 2df74cf7be8d..b997e9598a96 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -184,6 +184,7 @@ ) from mypy.types import ( ANY_STRATEGY, + DEPRECATED_TYPE_NAMES, MYPYC_NATIVE_INT_NAMES, OVERLOAD_NAMES, AnyType, @@ -636,6 +637,10 @@ def _visit_overloaded_func_def(self, defn: OverloadedFuncDef) -> None: if len(defn.items) == 1: self.fail(message_registry.MULTIPLE_OVERLOADS_REQUIRED, defn) + for item in defn.items: + if isinstance(item, Decorator) and isinstance(ct := item.func.type, CallableType): + self.create_deprecation_warning(ct, item.decorators) + if defn.is_property: # HACK: Infer the type of the property. assert isinstance(defn.items[0], Decorator) @@ -654,6 +659,17 @@ def _visit_overloaded_func_def(self, defn: OverloadedFuncDef) -> None: self.fail(message_registry.INCONSISTENT_ABSTRACT_OVERLOAD, defn) if defn.impl: defn.impl.accept(self) + if ( + isinstance(defn.impl, Decorator) + and isinstance(defn.impl.func.type, CallableType) + and ((deprecated := defn.impl.func.type.deprecated) is not None) + ): + if isinstance(defn.type, (CallableType, Overloaded)): + defn.type.deprecated = deprecated + for subdef in defn.items: + type_ = get_proper_type(subdef.type) + if isinstance(type_, (CallableType, Overloaded)): + type_.deprecated = deprecated if not defn.is_property: self.check_overlapping_overloads(defn) if defn.type is None: @@ -2406,6 +2422,7 @@ def check__exit__return_type(self, defn: FuncItem) -> None: def visit_class_def(self, defn: ClassDef) -> None: """Type check a class definition.""" typ = defn.info + self.create_deprecation_warning(typ, defn.decorators) for base in typ.mro[1:]: if base.is_final: self.fail(message_registry.CANNOT_INHERIT_FROM_FINAL.format(base.name), defn) @@ -2835,6 +2852,12 @@ 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: + if isinstance(sym.node, TypeInfo): + self.warn_deprecated(sym.node, node) + elif isinstance(typ := get_proper_type(sym.type), (CallableType, Overloaded)): + self.warn_deprecated(typ, node) self.check_import(node) def visit_import_all(self, node: ImportAll) -> None: @@ -2923,6 +2946,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. @@ -4667,6 +4700,8 @@ 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(method_type := get_proper_type(method_type), (CallableType, Overloaded)): + self.warn_deprecated(method_type, s) if not is_subtype(rvalue_type, lvalue_type): self.msg.incompatible_operator_assignment(s.op, s) else: @@ -4992,6 +5027,8 @@ def visit_del_stmt(self, s: DelStmt) -> None: ) def visit_decorator(self, e: Decorator) -> None: + if isinstance(ct := e.func.type, CallableType): + self.create_deprecation_warning(ct, e.decorators) for d in e.decorators: if isinstance(d, RefExpr): if d.fullname == "typing.no_type_check": @@ -7519,6 +7556,69 @@ 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 create_deprecation_warning( + self, typ: CallableType | Overloaded | TypeInfo, decorators: Iterable[Expression] + ) -> None: + """Create a warning when visiting a deprecated (decorated) callable, overload or + class that may be used later if the deprecated feature is used.""" + + if isinstance(typ, CallableType): + if (defn := typ.definition) is None: + name = f"function {typ.name}" + else: + name = f"function {defn.fullname}" + elif isinstance(typ, Overloaded): + if isinstance(func := typ.items[0].definition, FuncDef): + name = f"function {func.fullname}" + else: + name = f"function {typ.name}" + else: + name = f"class {typ.fullname}" + + overload = False + deprecation: str | None = None + for decorator in decorators: + if ( + isinstance(typ, CallableType) + and isinstance(decorator, NameExpr) + and (decorator.fullname in OVERLOAD_NAMES) + ): + overload = True + if deprecation is not None: + self.msg.note("@overload should be placed before @deprecated", decorator) + elif ( + isinstance(decorator, CallExpr) + and isinstance(callee := decorator.callee, NameExpr) + and (callee.fullname in DEPRECATED_TYPE_NAMES) + and (len(args := decorator.args) >= 1) + and isinstance(arg := args[0], StrExpr) + and ((value := arg.value) is not None) + ): + deprecation = value + + if deprecation is not None: + if overload: + typ.deprecated = f"overload {typ} of {name} is deprecated: {deprecation}" + else: + typ.deprecated = f"{name} is deprecated: {deprecation}" + + def check_deprecated( + self, typ: CallableType | Overloaded | TypeInfo, context: Context + ) -> None: + """Warn if deprecated and not directly imported with a `from` statement.""" + if typ.deprecated is not None: + for imp in self.tree.imports: + if isinstance(imp, ImportFrom) and any(typ.name == n[0] for n in imp.names): + break + else: + self.warn_deprecated(typ, context) + + def warn_deprecated(self, typ: CallableType | Overloaded | TypeInfo, context: Context) -> None: + """Warn if deprecated.""" + if (deprecated := typ.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 3532e18b93b2..f7c0ad709b9b 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -354,7 +354,12 @@ 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) + if isinstance(e.node, TypeInfo): + self.chk.check_deprecated(e.node, e) + elif isinstance(typ := get_proper_type(narrowed), (CallableType, Overloaded)): + self.chk.check_deprecated(typ, e) + return narrowed def analyze_ref_expr(self, e: RefExpr, lvalue: bool = False) -> Type: result: Type | None = None @@ -1475,6 +1480,8 @@ def check_call_expr_with_callee_type( object_type=object_type, ) proper_callee = get_proper_type(callee_type) + if isinstance(proper_callee, (CallableType, Overloaded)): + self.chk.check_deprecated(proper_callee, 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: @@ -3252,7 +3259,12 @@ 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) + if isinstance(e.node, TypeInfo): + self.chk.warn_deprecated(e.node, e) + elif isinstance(typ := get_proper_type(narrowed), (CallableType, Overloaded)): + self.chk.warn_deprecated(typ, e) + return narrowed def analyze_ordinary_member_access(self, e: MemberExpr, is_lvalue: bool) -> Type: """Analyse member expression or member lvalue.""" @@ -3477,6 +3489,8 @@ def visit_op_expr(self, e: OpExpr) -> Type: else: assert_never(use_reverse) e.method_type = method_type + if isinstance(mt := get_proper_type(method_type), (CallableType, Overloaded)): + self.chk.check_deprecated(mt, e) return result else: raise RuntimeError(f"Unknown operator {e.op}") @@ -3793,6 +3807,8 @@ def check_method_call_by_name( chk=self.chk, in_literal_context=self.is_literal_context(), ) + if isinstance(mt := get_proper_type(method_type), (CallableType, Overloaded)): + self.chk.warn_deprecated(mt, context) return self.check_method_call(method, base_type, method_type, args, arg_kinds, context) def check_union_method_call_by_name( diff --git a/mypy/checkmember.py b/mypy/checkmember.py index 0f117f5475ed..8f490ed99ac9 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -316,9 +316,19 @@ 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) + and isinstance(setter := items[1], Decorator) + ): + if isinstance(co := setter.func.type, (CallableType, Overloaded)): + mx.chk.warn_deprecated(co, mx.context) + return analyze_var(name, getter.var, typ, info, mx) + elif isinstance(co := method.type, (CallableType, Overloaded)): + mx.chk.warn_deprecated(co, mx.context) + if mx.is_lvalue: mx.msg.cant_assign_to_method(mx.context) if not isinstance(method, OverloadedFuncDef): @@ -773,6 +783,9 @@ def analyze_var( result = t typ = get_proper_type(typ) + if var.is_property and isinstance(typ, CallableType): + mx.chk.warn_deprecated(typ, mx.context) + call_type: ProperType | None = None if var.is_initialized_in_class and (not is_instance_var(var) or mx.is_operator): if isinstance(typ, FunctionLike) and not typ.is_type_obj(): diff --git a/mypy/errorcodes.py b/mypy/errorcodes.py index 6e8763264ddd..308d2afed43c 100644 --- a/mypy/errorcodes.py +++ b/mypy/errorcodes.py @@ -293,5 +293,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..a4fbbd79b367 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. diff --git a/mypy/main.py b/mypy/main.py index 05044335ecee..a0b925230702 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -800,6 +800,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 2eb39d4baaf6..abf311bcb7f2 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -2942,6 +2942,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 +3096,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 +3156,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 +3379,7 @@ def serialize(self) -> JsonDict: if self.dataclass_transform_spec is not None else None ), + "deprecated": self.deprecated, } return data @@ -3441,6 +3447,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 5ef6bc2a35e7..afee07bb3c0b 100644 --- a/mypy/options.py +++ b/mypy/options.py @@ -172,6 +172,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 f857c3e73381..53047cad8ea4 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -1435,15 +1435,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 diff --git a/mypy/types.py b/mypy/types.py index 2e7cbfd4e733..5b2a5e3a6f4d 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -1789,6 +1789,7 @@ class CallableType(FunctionLike): # (this is used for error messages) "imprecise_arg_kinds", "unpack_kwargs", # Was an Unpack[...] with **kwargs used to define this callable? + "deprecated", # The callable's deprecation message (in case it is deprecated) ) def __init__( @@ -1815,6 +1816,7 @@ def __init__( from_concatenate: bool = False, imprecise_arg_kinds: bool = False, unpack_kwargs: bool = False, + deprecated: str | None = None, ) -> None: super().__init__(line, column) assert len(arg_types) == len(arg_kinds) == len(arg_names) @@ -1863,6 +1865,7 @@ def __init__( self.type_guard = type_guard self.type_is = type_is self.unpack_kwargs = unpack_kwargs + self.deprecated = deprecated def copy_modified( self: CT, @@ -1887,6 +1890,7 @@ def copy_modified( from_concatenate: Bogus[bool] = _dummy, imprecise_arg_kinds: Bogus[bool] = _dummy, unpack_kwargs: Bogus[bool] = _dummy, + deprecated: Bogus[str | None] = _dummy, ) -> CT: modified = CallableType( arg_types=arg_types if arg_types is not _dummy else self.arg_types, @@ -1918,6 +1922,7 @@ def copy_modified( else self.imprecise_arg_kinds ), unpack_kwargs=unpack_kwargs if unpack_kwargs is not _dummy else self.unpack_kwargs, + deprecated=deprecated if deprecated is not _dummy else self.deprecated, ) # Optimization: Only NewTypes are supported as subtypes since # the class is effectively final, so we can use a cast safely. @@ -2197,6 +2202,7 @@ def __hash__(self) -> int: tuple(self.arg_names), tuple(self.arg_kinds), self.fallback, + self.deprecated, ) ) @@ -2211,6 +2217,7 @@ def __eq__(self, other: object) -> bool: and self.is_type_obj() == other.is_type_obj() and self.is_ellipsis_args == other.is_ellipsis_args and self.fallback == other.fallback + and self.deprecated == other.deprecated ) else: return NotImplemented @@ -2237,6 +2244,7 @@ def serialize(self) -> JsonDict: "from_concatenate": self.from_concatenate, "imprecise_arg_kinds": self.imprecise_arg_kinds, "unpack_kwargs": self.unpack_kwargs, + "deprecated": self.deprecated, } @classmethod @@ -2262,6 +2270,7 @@ def deserialize(cls, data: JsonDict) -> CallableType: from_concatenate=data["from_concatenate"], imprecise_arg_kinds=data["imprecise_arg_kinds"], unpack_kwargs=data["unpack_kwargs"], + deprecated=data["deprecated"], ) @@ -2280,7 +2289,7 @@ class Overloaded(FunctionLike): implementation. """ - __slots__ = ("_items",) + __slots__ = ("_items", "deprecated") _items: list[CallableType] # Must not be empty @@ -2288,6 +2297,7 @@ def __init__(self, items: list[CallableType]) -> None: super().__init__(items[0].line, items[0].column) self._items = items self.fallback = items[0].fallback + self.deprecated: str | None = None @property def items(self) -> list[CallableType]: @@ -2332,12 +2342,18 @@ def __eq__(self, other: object) -> bool: return self.items == other.items def serialize(self) -> JsonDict: - return {".class": "Overloaded", "items": [t.serialize() for t in self.items]} + return { + ".class": "Overloaded", + "items": [t.serialize() for t in self.items], + "deprecated": self.deprecated, + } @classmethod def deserialize(cls, data: JsonDict) -> Overloaded: assert data[".class"] == "Overloaded" - return Overloaded([CallableType.deserialize(t) for t in data["items"]]) + s = Overloaded([CallableType.deserialize(t) for t in data["items"]]) + s.deprecated = data.get("deprecated") + return s class TupleType(ProperType): diff --git a/test-data/unit/check-deprecated.test b/test-data/unit/check-deprecated.test new file mode 100644 index 000000000000..94af203d5a82 --- /dev/null +++ b/test-data/unit/check-deprecated.test @@ -0,0 +1,395 @@ +-- 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() # N: function __main__.f is deprecated: use f2 instead +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("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.__iter__ is deprecated: no iteration +1 in b # N: function __main__.B.__contains__ is deprecated: 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() # N: function __main__.C.f is deprecated: use g instead +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 # E: Property "f" defined in "C" is read-only \ + # N: function __main__.C.f is deprecated: use f2 instead + +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 2ad31311a402..2d6675a5316b 100644 --- a/test-data/unit/fine-grained.test +++ b/test-data/unit/fine-grained.test @@ -10545,3 +10545,139 @@ m.py:9: error: Argument 1 to "foo" has incompatible type "int"; expected "str" m.py:9: error: Argument 2 to "foo" has incompatible type "str"; expected "int" m.py:10: error: Unexpected keyword argument "a" for "foo" partial.py:4: note: "foo" defined here + +[case testAddKeepChangeAndRemoveFunctionDeprecation] +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 testRemoveFunctionDeprecation] +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 testKeepFunctionDeprecation] +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 testAddFunctionDeprecationIndirectImport1-only_when_nocache] +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] +== +main:1: note: function a.f is deprecated: use f2 instead +main:4: note: function a.f is deprecated: use f2 instead +b.py:1: note: function a.f is deprecated: use f2 instead + +[case testAddFunctionDeprecationIndirectImport2-only_when_cache] +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 diff --git a/test-data/unit/pythoneval.test b/test-data/unit/pythoneval.test index 222430c3ef55..e5eabf11f2c9 100644 --- a/test-data/unit/pythoneval.test +++ b/test-data/unit/pythoneval.test @@ -956,7 +956,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