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

Merged
merged 40 commits into from
Oct 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 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
a6d0e59
Move the complete creation process of warning notes from `checker.py`…
tyralla Sep 22, 2024
6163787
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 22, 2024
49ee7a8
Move the `deprecation` attribute from the nodes `CallableType` and `O…
tyralla Sep 28, 2024
250e171
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 28, 2024
09a53d5
Modify function`snapshot_symbol_table` by removing the "CrossRef" sp…
tyralla Sep 29, 2024
31c4296
`typ: SymbolNode` -> `node: SymbolNode`
tyralla Sep 29, 2024
90fb06d
`type_` -> typ
tyralla Sep 29, 2024
beed6a5
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 29, 2024
8eb2e77
Revert commit 09a53d5b (regarding `astdiff.py` but not `fine-grained.…
tyralla Oct 6, 2024
988bf5f
Merge branch 'master' into feature/support_deprecated
tyralla Oct 6, 2024
80ddc68
Add `testDeprecateFunctionAlreadyDecorated`.
tyralla Oct 6, 2024
d104f26
Add `deprecated` to the `Func` snapshot and adjust test `testDeprecat…
tyralla Oct 6, 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 @@ -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:

Expand Down
38 changes: 38 additions & 0 deletions docs/source/error_code_list2.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 <https://peps.python.org/pep-0702>`_. 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 <mypy --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]
Expand Down
46 changes: 46 additions & 0 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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)
tyralla marked this conversation as resolved.
Show resolved Hide resolved
):
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.
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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."""
Expand Down
39 changes: 32 additions & 7 deletions mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@
validate_instance,
)
from mypy.typeops import (
bind_self,
callable_type,
custom_special_method,
erase_to_union_or_bound,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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."""
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -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,
Expand Down
13 changes: 10 additions & 3 deletions mypy/checkmember.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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:
Expand Down
6 changes: 6 additions & 0 deletions mypy/errorcodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
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()
5 changes: 4 additions & 1 deletion mypy/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions mypy/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading
Loading