Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add basic support for PEP 702 (@deprecated). #17476

Open
wants to merge 28 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
f04f4e3
Add basic support for PEP 702 (@deprecated).
tyralla Jul 3, 2024
973bf2d
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 3, 2024
a88f4b4
use `type: ignore[deprecated]` when importing `abstractproperty`
tyralla Jul 3, 2024
91612c9
Merge remote-tracking branch 'mypypy/feature/support_deprecated' into…
tyralla Jul 3, 2024
faa4911
accept deprecated.args >= 1
tyralla Jul 4, 2024
101e9b8
": " instead of " - "
tyralla Jul 4, 2024
e3dfacb
only consider `warnings.deprecated` and `typing_extensions.deprecated…
tyralla Jul 4, 2024
cbf7574
note instead of error
tyralla Jul 4, 2024
9a947a5
remove walrusses
tyralla Jul 4, 2024
6d92318
document the new option
tyralla Jul 4, 2024
afa0336
motivate the semantic analyzer
tyralla Jul 4, 2024
7bfb534
`report-deprecated-as-error` instead of `warn-deprecated` and three n…
tyralla Jul 4, 2024
1042c65
fix a typo in error_code_list2.rst
tyralla Jul 4, 2024
978b1a2
additional note when PEP 702 is unsatisfied with the order of `@depre…
tyralla Jul 4, 2024
b0ced07
mention the affected overload
tyralla Jul 4, 2024
a07fc64
Merge remote-tracking branch 'mypypy/feature/support_deprecated' into…
tyralla Jul 4, 2024
b527250
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 4, 2024
f636024
add an annotation required by mypyc
tyralla Jul 4, 2024
63a725e
Merge remote-tracking branch 'mypypy/feature/support_deprecated' into…
tyralla Jul 4, 2024
966ac8b
refactor: create the whole deprecation warning in one place
tyralla Jul 5, 2024
6a7dfe0
refactor: get rid of the `memberaccess` parameter
tyralla Jul 5, 2024
1a40953
refactor: merge `check_deprecated_function` and `check_deprecated_cla…
tyralla Jul 5, 2024
1372e66
refactor: convert `get_deprecation_warning` to `create_deprecation_wa…
tyralla Jul 5, 2024
286371f
prefix the warnings with `class ...`, `function ...`, or `overload ..…
tyralla Jul 5, 2024
6f54dab
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 5, 2024
8c0260e
Consider `CallableType.deprecated` in `__hash__`, `__eq__`, `serializ…
tyralla Aug 27, 2024
cf2dcaf
Add a few fine-grained tests (of which `testAddFunctionDeprecationInd…
tyralla Aug 27, 2024
6a93d6a
Merge branch 'feature/support_deprecated' of https://github.com/tyral…
tyralla Aug 27, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/source/command_line.rst
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,12 @@ potentially problematic or redundant in some way.

This limitation will be removed in future releases of mypy.

.. option:: --warn-deprecated

This flag will make mypy report an error whenever your code imports a deprecated
function or class with a ``from mod import depr" statement or uses a deprecated
function, method or class imported otherwise or defined locally. Features are
considered deprecated when decorated with ``warnings.deprecated``.

.. _miscellaneous-strictness-flags:

Expand Down
34 changes: 34 additions & 0 deletions docs/source/error_code_list2.rst
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,40 @@ 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]
--------------------------------------------------------------

If you use :option:`--warn-deprecated <mypy --warn-deprecated>`, mypy generates a note if
Copy link
Member

@ilevkivskyi ilevkivskyi Jul 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like there has been some back and forth so docs are out of sync now. To summarize how I see the logic should be:

  • Deprecation messages are always shown as notes by default (with code shown, see my previous comment), this will result in exit code 0 (which is kind of my main concern, I don't want users CI to break unnecessarily on new mypy version).
  • People who still don't want to see the notes, can use --disable-error-code=deprecated and/or # type: ignore[deprecated].
  • There should be a flag that turns notes into errors (for more pedantic people). They will still be able to selectively silence the errors using # type: ignore[deprecated].

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • There should be a flag that turns notes into errors (for more pedantic people).

Does Mypy implement other checks that can result in errors or notes, depending on an option? (I'm just asking to avoid reinventing the wheel.)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TBH I don't remember. But in any case it should be as simple as changing last line in your warn_deprecated() method in checker.py to something like

if self.options.error_on_deprecated:  # or other flag name
    self.error(...)
else:
    self.note(...)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That seems doable, thanks. I will implement it later.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done! report-deprecated-as-error is the shortest understandable description I could come up with. Other suggestions are welcome, of course.

your code imports a deprecated function or class with a ``from mod import depr" statement
or uses a deprecated function, method or class imported otherwise or defined locally.
Features are considered deprecated when decorated with ``warnings.deprecated``, as
specified in `PEP 702 <https://peps.python.org/pep-0702>`_.

.. 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: warn-deprecated

# Note: 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")

# Note: __main__.old_function is deprecated: use new_function
old_function()

.. _code-redundant-expr:

Check that expression is redundant [redundant-expr]
Expand Down
84 changes: 84 additions & 0 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@
)
from mypy.types import (
ANY_STRATEGY,
DEPRECATED_TYPE_NAMES,
MYPYC_NATIVE_INT_NAMES,
OVERLOAD_NAMES,
AnyType,
Expand Down Expand Up @@ -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):
ct.deprecated = self.get_deprecation_warning(item.decorators)

if defn.is_property:
# HACK: Infer the type of the property.
assert isinstance(defn.items[0], Decorator)
Expand All @@ -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:
Expand Down Expand Up @@ -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
typ.deprecated = self.get_deprecation_warning(defn.decorators)
for base in typ.mro[1:]:
if base.is_final:
self.fail(message_registry.CANNOT_INHERIT_FROM_FINAL.format(base.name), defn)
Expand Down Expand Up @@ -2835,6 +2852,14 @@ 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) and ((depr := sym.node.deprecated) is not None):
self.warn_deprecated(sym.node, depr, node)
elif isinstance(co := get_proper_type(sym.type), (CallableType, Overloaded)) and (
(depr := co.deprecated) is not None
):
self.warn_deprecated(co, depr, node)
self.check_import(node)

def visit_import_all(self, node: ImportAll) -> None:
Expand Down Expand Up @@ -2923,6 +2948,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_class(instance.type, s, False)

# Avoid type checking type aliases in stubs to avoid false
# positives about modern type syntax available in stubs such
# as X | Y.
Expand Down Expand Up @@ -4667,6 +4702,7 @@ 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)
self.check_deprecated_function(method_type, s, True)
if not is_subtype(rvalue_type, lvalue_type):
self.msg.incompatible_operator_assignment(s.op, s)
else:
Expand Down Expand Up @@ -4992,6 +5028,8 @@ def visit_del_stmt(self, s: DelStmt) -> None:
)

def visit_decorator(self, e: Decorator) -> None:
if isinstance(co := e.func.type, (CallableType, Overloaded)):
co.deprecated = self.get_deprecation_warning(e.decorators)
for d in e.decorators:
if isinstance(d, RefExpr):
if d.fullname == "typing.no_type_check":
Expand Down Expand Up @@ -7519,6 +7557,52 @@ 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 get_deprecation_warning(self, decorators: Iterable[Expression]) -> str | None:
for decorator in decorators:
if (
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)
):
return arg.value
return None

def check_deprecated_function(self, typ: Type, context: Context, memberaccess: bool) -> None:
if isinstance(typ := get_proper_type(typ), (CallableType, Overloaded)):
self._check_deprecated(typ, context, memberaccess)

def check_deprecated_class(self, typ: TypeInfo, context: Context, memberaccess: bool) -> None:
self._check_deprecated(typ, context, memberaccess)

def _check_deprecated(
self, typ: CallableType | Overloaded | TypeInfo, context: Context, memberaccess: bool
) -> None:
if (depr := typ.deprecated) is not None:
if memberaccess:
self.warn_deprecated(typ, depr, context)
else:
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, depr, context)

def warn_deprecated(
self, type_: CallableType | Overloaded | TypeInfo, deprecated: str, context: Context
) -> None:
name = type_.name
if isinstance(type_, CallableType):
if (defn := type_.definition) is not None:
name = defn.fullname
elif isinstance(type_, Overloaded):
if isinstance(func := type_.items[0].definition, FuncDef):
name = func.fullname
else:
name = type_.fullname
self.msg.note(f"{name} is deprecated: {deprecated}", context, code=codes.DEPRECATED)


class CollectArgTypeVarTypes(TypeTraverserVisitor):
"""Collects the non-nested argument types in a set."""
Expand Down
17 changes: 15 additions & 2 deletions mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -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_class(e.node, e, False)
else:
self.chk.check_deprecated_function(narrowed, e, False)
return narrowed

def analyze_ref_expr(self, e: RefExpr, lvalue: bool = False) -> Type:
result: Type | None = None
Expand Down Expand Up @@ -1474,6 +1479,7 @@ def check_call_expr_with_callee_type(
callable_name=callable_name,
object_type=object_type,
)
self.chk.check_deprecated_function(callee_type, e, False)
proper_callee = get_proper_type(callee_type)
if isinstance(e.callee, RefExpr) and isinstance(proper_callee, CallableType):
# Cache it for find_isinstance_check()
Expand Down Expand Up @@ -3252,7 +3258,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.check_deprecated_class(e.node, e, True)
else:
self.chk.check_deprecated_function(narrowed, e, True)
return narrowed

def analyze_ordinary_member_access(self, e: MemberExpr, is_lvalue: bool) -> Type:
"""Analyse member expression or member lvalue."""
Expand Down Expand Up @@ -3477,6 +3488,7 @@ def visit_op_expr(self, e: OpExpr) -> Type:
else:
assert_never(use_reverse)
e.method_type = method_type
self.chk.check_deprecated_function(method_type, e, False)
return result
else:
raise RuntimeError(f"Unknown operator {e.op}")
Expand Down Expand Up @@ -3793,6 +3805,7 @@ def check_method_call_by_name(
chk=self.chk,
in_literal_context=self.is_literal_context(),
)
self.chk.check_deprecated_function(method_type, context, True)
return self.check_method_call(method, base_type, method_type, args, arg_kinds, context)

def check_union_method_call_by_name(
Expand Down
27 changes: 24 additions & 3 deletions mypy/checkmember.py
Original file line number Diff line number Diff line change
Expand Up @@ -316,9 +316,23 @@ 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)) and (
(deprecated := co.deprecated) is not None
):
mx.chk.warn_deprecated(co, deprecated, mx.context)
return analyze_var(name, getter.var, typ, info, mx)
elif isinstance(co := method.type, (CallableType, Overloaded)) and (
(deprecated := co.deprecated) is not None
):
mx.chk.warn_deprecated(co, deprecated, mx.context)

if mx.is_lvalue:
mx.msg.cant_assign_to_method(mx.context)
if not isinstance(method, OverloadedFuncDef):
Expand Down Expand Up @@ -773,6 +787,13 @@ def analyze_var(
result = t
typ = get_proper_type(typ)

if (
var.is_property
and isinstance(typ, CallableType)
and ((deprecated := typ.deprecated) is not None)
):
mx.chk.warn_deprecated(typ, deprecated, 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():
Expand Down
6 changes: 6 additions & 0 deletions mypy/errorcodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
tyralla marked this conversation as resolved.
Show resolved Hide resolved
)

# This copy will not include any error codes defined later in the plugins.
mypy_error_codes = error_codes.copy()
7 changes: 7 additions & 0 deletions mypy/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -800,6 +800,13 @@ def add_invertible_flag(
help="Warn about statements or expressions inferred to be unreachable",
group=lint_group,
)
add_invertible_flag(
"--warn-deprecated",
default=False,
strict_flag=False,
help="Warn when importing or using deprecated features",
group=lint_group,
)

# Note: this group is intentionally added here even though we don't add
# --strict to this group near the end.
Expand Down
7 changes: 7 additions & 0 deletions mypy/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -3374,6 +3379,7 @@ def serialize(self) -> JsonDict:
if self.dataclass_transform_spec is not None
else None
),
"deprecated": self.deprecated,
}
return data

Expand Down Expand Up @@ -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


Expand Down
Loading
Loading