diff --git a/doc/data/messages/d/deprecated-argument/details.rst b/doc/data/messages/d/deprecated-argument/details.rst index 177c854b85..46294b9d57 100644 --- a/doc/data/messages/d/deprecated-argument/details.rst +++ b/doc/data/messages/d/deprecated-argument/details.rst @@ -1,8 +1,2 @@ -This message can be raised on your own code using a `custom deprecation checker`_ (follow link for a full example). - -Loading this custom checker using ``load-plugins`` would start raising ``deprecated-argument``. - -The actual replacement then need to be studied on a case by case basis by reading the -deprecation warning or the release notes. - -.. _`custom deprecation checker`: https://github.com/pylint-dev/pylint/blob/main/examples/deprecation_checker.py +The actual replacement needs to be studied on a case by case basis +by reading the deprecation warning or the release notes. diff --git a/doc/data/messages/d/deprecated-attribute/bad.py b/doc/data/messages/d/deprecated-attribute/bad.py new file mode 100644 index 0000000000..ccd4090b56 --- /dev/null +++ b/doc/data/messages/d/deprecated-attribute/bad.py @@ -0,0 +1,4 @@ +from configparser import ParsingError + +err = ParsingError("filename") +source = err.filename # [deprecated-attribute] diff --git a/doc/data/messages/d/deprecated-attribute/details.rst b/doc/data/messages/d/deprecated-attribute/details.rst new file mode 100644 index 0000000000..46294b9d57 --- /dev/null +++ b/doc/data/messages/d/deprecated-attribute/details.rst @@ -0,0 +1,2 @@ +The actual replacement needs to be studied on a case by case basis +by reading the deprecation warning or the release notes. diff --git a/doc/data/messages/d/deprecated-attribute/good.py b/doc/data/messages/d/deprecated-attribute/good.py new file mode 100644 index 0000000000..492cd8087a --- /dev/null +++ b/doc/data/messages/d/deprecated-attribute/good.py @@ -0,0 +1,4 @@ +from configparser import ParsingError + +err = ParsingError("filename") +source = err.source diff --git a/doc/data/messages/d/deprecated-class/details.rst b/doc/data/messages/d/deprecated-class/details.rst index 1bdc8563df..46294b9d57 100644 --- a/doc/data/messages/d/deprecated-class/details.rst +++ b/doc/data/messages/d/deprecated-class/details.rst @@ -1,2 +1,2 @@ -The actual replacement need to be studied on a case by case basis +The actual replacement needs to be studied on a case by case basis by reading the deprecation warning or the release notes. diff --git a/doc/data/messages/d/deprecated-decorator/details.rst b/doc/data/messages/d/deprecated-decorator/details.rst index 1bdc8563df..46294b9d57 100644 --- a/doc/data/messages/d/deprecated-decorator/details.rst +++ b/doc/data/messages/d/deprecated-decorator/details.rst @@ -1,2 +1,2 @@ -The actual replacement need to be studied on a case by case basis +The actual replacement needs to be studied on a case by case basis by reading the deprecation warning or the release notes. diff --git a/doc/data/messages/d/deprecated-method/details.rst b/doc/data/messages/d/deprecated-method/details.rst index 1bdc8563df..46294b9d57 100644 --- a/doc/data/messages/d/deprecated-method/details.rst +++ b/doc/data/messages/d/deprecated-method/details.rst @@ -1,2 +1,2 @@ -The actual replacement need to be studied on a case by case basis +The actual replacement needs to be studied on a case by case basis by reading the deprecation warning or the release notes. diff --git a/doc/data/messages/d/deprecated-module/details.rst b/doc/data/messages/d/deprecated-module/details.rst index 1bdc8563df..46294b9d57 100644 --- a/doc/data/messages/d/deprecated-module/details.rst +++ b/doc/data/messages/d/deprecated-module/details.rst @@ -1,2 +1,2 @@ -The actual replacement need to be studied on a case by case basis +The actual replacement needs to be studied on a case by case basis by reading the deprecation warning or the release notes. diff --git a/doc/user_guide/checkers/features.rst b/doc/user_guide/checkers/features.rst index 73b9bcdc0b..cd900b9943 100644 --- a/doc/user_guide/checkers/features.rst +++ b/doc/user_guide/checkers/features.rst @@ -1044,6 +1044,8 @@ Stdlib checker Messages emitted when using Python >= 3.5. :deprecated-argument (W4903): *Using deprecated argument %s of method %s()* The argument is marked as deprecated and will be removed in the future. +:deprecated-attribute (W4906): *Using deprecated attribute %r* + The attribute is marked as deprecated and will be removed in the future. :deprecated-class (W4904): *Using deprecated class %s of module %s* The class is marked as deprecated and will be removed in the future. :deprecated-decorator (W4905): *Using deprecated decorator %s()* diff --git a/doc/user_guide/messages/messages_overview.rst b/doc/user_guide/messages/messages_overview.rst index cd63d3f85c..25073d4caf 100644 --- a/doc/user_guide/messages/messages_overview.rst +++ b/doc/user_guide/messages/messages_overview.rst @@ -229,6 +229,7 @@ All messages in the warning category: warning/consider-ternary-expression warning/dangerous-default-value warning/deprecated-argument + warning/deprecated-attribute warning/deprecated-class warning/deprecated-decorator warning/deprecated-method diff --git a/doc/whatsnew/fragments/8855.new_check b/doc/whatsnew/fragments/8855.new_check new file mode 100644 index 0000000000..43eca39240 --- /dev/null +++ b/doc/whatsnew/fragments/8855.new_check @@ -0,0 +1,3 @@ +Added a ``deprecated-attribute`` message to check deprecated attributes in the stdlib. + +Closes #8855 diff --git a/pylint/checkers/deprecated.py b/pylint/checkers/deprecated.py index 821a9836cf..3428e736f8 100644 --- a/pylint/checkers/deprecated.py +++ b/pylint/checkers/deprecated.py @@ -11,10 +11,12 @@ import astroid from astroid import nodes +from astroid.bases import Instance from pylint.checkers import utils from pylint.checkers.base_checker import BaseChecker from pylint.checkers.utils import get_import_name, infer_all, safe_infer +from pylint.interfaces import INFERENCE from pylint.typing import MessageDefinitionTuple ACCEPTABLE_NODES = ( @@ -22,6 +24,7 @@ astroid.UnboundMethod, nodes.FunctionDef, nodes.ClassDef, + astroid.Attribute, ) @@ -31,6 +34,15 @@ class DeprecatedMixin(BaseChecker): A class implementing mixin must define "deprecated-method" Message. """ + DEPRECATED_ATTRIBUTE_MESSAGE: dict[str, MessageDefinitionTuple] = { + "W4906": ( + "Using deprecated attribute %r", + "deprecated-attribute", + "The attribute is marked as deprecated and will be removed in the future.", + {"shared": True}, + ), + } + DEPRECATED_MODULE_MESSAGE: dict[str, MessageDefinitionTuple] = { "W4901": ( "Deprecated module %r", @@ -76,6 +88,11 @@ class DeprecatedMixin(BaseChecker): ), } + @utils.only_required_for_messages("deprecated-attribute") + def visit_attribute(self, node: astroid.Attribute) -> None: + """Called when an `astroid.Attribute` node is visited.""" + self.check_deprecated_attribute(node) + @utils.only_required_for_messages( "deprecated-method", "deprecated-argument", @@ -189,6 +206,25 @@ def deprecated_classes(self, module: str) -> Iterable[str]: # pylint: disable=unused-argument return () + def deprecated_attributes(self) -> Iterable[str]: + """Callback returning the deprecated attributes.""" + return () + + def check_deprecated_attribute(self, node: astroid.Attribute) -> None: + """Checks if the attribute is deprecated.""" + inferred_expr = safe_infer(node.expr) + if not isinstance(inferred_expr, (nodes.ClassDef, Instance, nodes.Module)): + return + attribute_qname = ".".join((inferred_expr.qname(), node.attrname)) + for deprecated_name in self.deprecated_attributes(): + if attribute_qname == deprecated_name: + self.add_message( + "deprecated-attribute", + node=node, + args=(attribute_qname,), + confidence=INFERENCE, + ) + def check_deprecated_module(self, node: nodes.Import, mod_path: str | None) -> None: """Checks if the module is deprecated.""" for mod_name in self.deprecated_modules(): diff --git a/pylint/checkers/stdlib.py b/pylint/checkers/stdlib.py index 932e007e13..df8b271bf7 100644 --- a/pylint/checkers/stdlib.py +++ b/pylint/checkers/stdlib.py @@ -84,7 +84,9 @@ }, (3, 9, 0): {"random.Random.shuffle": ((1, "random"),)}, (3, 12, 0): { + "argparse.BooleanOptionalAction": ((3, "type"), (4, "choices"), (7, "metavar")), "coroutine.throw": ((1, "value"), (2, "traceback")), + "email.utils.localtime": ((1, "isdst"),), "shutil.rmtree": ((2, "onerror"),), }, } @@ -228,6 +230,13 @@ "binascii.a2b_hqx", "binascii.rlecode_hqx", "binascii.rledecode_hqx", + "importlib.resources.contents", + "importlib.resources.is_resource", + "importlib.resources.open_binary", + "importlib.resources.open_text", + "importlib.resources.path", + "importlib.resources.read_binary", + "importlib.resources.read_text", }, (3, 10, 0): { "_sqlite3.enable_shared_cache", @@ -256,11 +265,16 @@ "unittest.TestLoader.loadTestsFromModule", "unittest.TestLoader.loadTestsFromTestCase", "unittest.TestLoader.getTestCaseNames", + "unittest.TestProgram.usageExit", }, (3, 12, 0): { "builtins.bool.__invert__", "datetime.datetime.utcfromtimestamp", "datetime.datetime.utcnow", + "pkgutil.find_loader", + "pkgutil.get_loader", + "pty.master_open", + "pty.slave_open", "xml.etree.ElementTree.Element.__bool__", }, }, @@ -324,7 +338,29 @@ }, }, (3, 12, 0): { + "ast": { + "Bytes", + "Ellipsis", + "NameConstant", + "Num", + "Str", + }, + "asyncio": { + "AbstractChildWatcher", + "MultiLoopChildWatcher", + "FastChildWatcher", + "SafeChildWatcher", + }, + "collections.abc": { + "ByteString", + }, + "importlib.abc": { + "ResourceReader", + "Traversable", + "TraversableResources", + }, "typing": { + "ByteString", "Hashable", "Sized", }, @@ -332,6 +368,20 @@ } +DEPRECATED_ATTRIBUTES: DeprecationDict = { + (3, 2, 0): { + "configparser.ParsingError.filename", + }, + (3, 12, 0): { + "calendar.January", + "calendar.February", + "sys.last_traceback", + "sys.last_type", + "sys.last_value", + }, +} + + def _check_mode_str(mode: Any) -> bool: # check type if not isinstance(mode, str): @@ -370,6 +420,7 @@ class StdlibChecker(DeprecatedMixin, BaseChecker): **DeprecatedMixin.DEPRECATED_ARGUMENT_MESSAGE, **DeprecatedMixin.DEPRECATED_CLASS_MESSAGE, **DeprecatedMixin.DEPRECATED_DECORATOR_MESSAGE, + **DeprecatedMixin.DEPRECATED_ATTRIBUTE_MESSAGE, "W1501": ( '"%s" is not a valid mode for open.', "bad-open-mode", @@ -489,6 +540,7 @@ def __init__(self, linter: PyLinter) -> None: self._deprecated_arguments: dict[str, tuple[tuple[int | None, str], ...]] = {} self._deprecated_classes: dict[str, set[str]] = {} self._deprecated_decorators: set[str] = set() + self._deprecated_attributes: set[str] = set() for since_vers, func_list in DEPRECATED_METHODS[sys.version_info[0]].items(): if since_vers <= sys.version_info: @@ -502,6 +554,9 @@ def __init__(self, linter: PyLinter) -> None: for since_vers, decorator_list in DEPRECATED_DECORATORS.items(): if since_vers <= sys.version_info: self._deprecated_decorators.update(decorator_list) + for since_vers, attribute_list in DEPRECATED_ATTRIBUTES.items(): + if since_vers <= sys.version_info: + self._deprecated_attributes.update(attribute_list) # Modules are checked by the ImportsChecker, because the list is # synced with the config argument deprecated-modules @@ -868,6 +923,9 @@ def deprecated_classes(self, module: str) -> Iterable[str]: def deprecated_decorators(self) -> Iterable[str]: return self._deprecated_decorators + def deprecated_attributes(self) -> Iterable[str]: + return self._deprecated_attributes + def register(linter: PyLinter) -> None: linter.register_checker(StdlibChecker(linter)) diff --git a/tests/checkers/unittest_deprecated.py b/tests/checkers/unittest_deprecated.py index a9efaa505f..f0aef8a06d 100644 --- a/tests/checkers/unittest_deprecated.py +++ b/tests/checkers/unittest_deprecated.py @@ -7,7 +7,7 @@ import astroid from pylint.checkers import BaseChecker, DeprecatedMixin -from pylint.interfaces import UNDEFINED +from pylint.interfaces import INFERENCE, UNDEFINED from pylint.testutils import CheckerTestCase, MessageTest @@ -52,11 +52,39 @@ def deprecated_arguments( def deprecated_decorators(self) -> set[str]: return {".deprecated_decorator"} + def deprecated_attributes(self) -> set[str]: + return {".DeprecatedClass.deprecated_attribute"} + # pylint: disable-next = too-many-public-methods class TestDeprecatedChecker(CheckerTestCase): CHECKER_CLASS = _DeprecatedChecker + def test_deprecated_attribute(self) -> None: + # Tests detecting deprecated attribute + node = astroid.extract_node( + """ + class DeprecatedClass: + deprecated_attribute = 42 + + obj = DeprecatedClass() + obj.deprecated_attribute + """ + ) + with self.assertAddsMessages( + MessageTest( + msg_id="deprecated-attribute", + args=(".DeprecatedClass.deprecated_attribute",), + node=node, + confidence=INFERENCE, + line=6, + col_offset=0, + end_line=6, + end_col_offset=24, + ) + ): + self.checker.visit_attribute(node) + def test_deprecated_function(self) -> None: # Tests detecting deprecated function node = astroid.extract_node( diff --git a/tests/functional/d/deprecated/deprecated_attribute_py312.py b/tests/functional/d/deprecated/deprecated_attribute_py312.py new file mode 100644 index 0000000000..f8613573e6 --- /dev/null +++ b/tests/functional/d/deprecated/deprecated_attribute_py312.py @@ -0,0 +1,4 @@ +"""Test deprecated-attribute""" + +import calendar +print(calendar.January) # [deprecated-attribute] diff --git a/tests/functional/d/deprecated/deprecated_attribute_py312.rc b/tests/functional/d/deprecated/deprecated_attribute_py312.rc new file mode 100644 index 0000000000..9c966d4bda --- /dev/null +++ b/tests/functional/d/deprecated/deprecated_attribute_py312.rc @@ -0,0 +1,2 @@ +[testoptions] +min_pyver=3.12 diff --git a/tests/functional/d/deprecated/deprecated_attribute_py312.txt b/tests/functional/d/deprecated/deprecated_attribute_py312.txt new file mode 100644 index 0000000000..f86fde7c7e --- /dev/null +++ b/tests/functional/d/deprecated/deprecated_attribute_py312.txt @@ -0,0 +1 @@ +deprecated-attribute:4:6:4:22::Using deprecated attribute 'calendar.January':INFERENCE diff --git a/tests/pyreverse/functional/class_diagrams/colorized_output/colorized.puml b/tests/pyreverse/functional/class_diagrams/colorized_output/colorized.puml index dc9b7034ba..1c3d49d806 100644 --- a/tests/pyreverse/functional/class_diagrams/colorized_output/colorized.puml +++ b/tests/pyreverse/functional/class_diagrams/colorized_output/colorized.puml @@ -26,6 +26,7 @@ class "StdlibChecker" as pylint.checkers.stdlib.StdlibChecker #44BB99 { msgs : dict[str, MessageDefinitionTuple] name : str deprecated_arguments(method: str) -> tuple[tuple[int | None, str], ...] + deprecated_attributes() -> Iterable[str] deprecated_classes(module: str) -> Iterable[str] deprecated_decorators() -> Iterable[str] deprecated_methods() -> set[str] diff --git a/tests/pyreverse/functional/class_diagrams/colorized_output/custom_colors.dot b/tests/pyreverse/functional/class_diagrams/colorized_output/custom_colors.dot index 84c8142a35..4f1b5f8b1c 100644 --- a/tests/pyreverse/functional/class_diagrams/colorized_output/custom_colors.dot +++ b/tests/pyreverse/functional/class_diagrams/colorized_output/custom_colors.dot @@ -4,7 +4,7 @@ charset="utf-8" "custom_colors.CheckerCollector" [color="red", fontcolor="black", label=<{CheckerCollector|checker1
checker2
checker3
|}>, shape="record", style="filled"]; "pylint.extensions.check_elif.ElseifUsedChecker" [color="#44BB88", fontcolor="black", label=<{ElseifUsedChecker|msgs : dict
name : str
|leave_module(_: nodes.Module): None
process_tokens(tokens: list[TokenInfo]): None
visit_if(node: nodes.If): None
}>, shape="record", style="filled"]; "pylint.checkers.exceptions.ExceptionsChecker" [color="yellow", fontcolor="black", label=<{ExceptionsChecker|msgs : dict
name : str
options : tuple
|open(): None
visit_binop(node: nodes.BinOp): None
visit_compare(node: nodes.Compare): None
visit_raise(node: nodes.Raise): None
visit_try(node: nodes.Try): None
}>, shape="record", style="filled"]; -"pylint.checkers.stdlib.StdlibChecker" [color="yellow", fontcolor="black", label=<{StdlibChecker|msgs : dict[str, MessageDefinitionTuple]
name : str
|deprecated_arguments(method: str): tuple[tuple[int \| None, str], ...]
deprecated_classes(module: str): Iterable[str]
deprecated_decorators(): Iterable[str]
deprecated_methods(): set[str]
visit_boolop(node: nodes.BoolOp): None
visit_call(node: nodes.Call): None
visit_functiondef(node: nodes.FunctionDef): None
visit_if(node: nodes.If): None
visit_ifexp(node: nodes.IfExp): None
visit_unaryop(node: nodes.UnaryOp): None
}>, shape="record", style="filled"]; +"pylint.checkers.stdlib.StdlibChecker" [color="yellow", fontcolor="black", label=<{StdlibChecker|msgs : dict[str, MessageDefinitionTuple]
name : str
|deprecated_arguments(method: str): tuple[tuple[int \| None, str], ...]
deprecated_attributes(): Iterable[str]
deprecated_classes(module: str): Iterable[str]
deprecated_decorators(): Iterable[str]
deprecated_methods(): set[str]
visit_boolop(node: nodes.BoolOp): None
visit_call(node: nodes.Call): None
visit_functiondef(node: nodes.FunctionDef): None
visit_if(node: nodes.If): None
visit_ifexp(node: nodes.IfExp): None
visit_unaryop(node: nodes.UnaryOp): None
}>, shape="record", style="filled"]; "pylint.checkers.exceptions.ExceptionsChecker" -> "custom_colors.CheckerCollector" [arrowhead="diamond", arrowtail="none", fontcolor="green", label="checker1", style="solid"]; "pylint.checkers.stdlib.StdlibChecker" -> "custom_colors.CheckerCollector" [arrowhead="diamond", arrowtail="none", fontcolor="green", label="checker3", style="solid"]; "pylint.extensions.check_elif.ElseifUsedChecker" -> "custom_colors.CheckerCollector" [arrowhead="diamond", arrowtail="none", fontcolor="green", label="checker2", style="solid"]; diff --git a/tests/pyreverse/functional/class_diagrams/colorized_output/custom_colors.puml b/tests/pyreverse/functional/class_diagrams/colorized_output/custom_colors.puml index 7119900639..a132fb0d3e 100644 --- a/tests/pyreverse/functional/class_diagrams/colorized_output/custom_colors.puml +++ b/tests/pyreverse/functional/class_diagrams/colorized_output/custom_colors.puml @@ -26,6 +26,7 @@ class "StdlibChecker" as pylint.checkers.stdlib.StdlibChecker #yellow { msgs : dict[str, MessageDefinitionTuple] name : str deprecated_arguments(method: str) -> tuple[tuple[int | None, str], ...] + deprecated_attributes() -> Iterable[str] deprecated_classes(module: str) -> Iterable[str] deprecated_decorators() -> Iterable[str] deprecated_methods() -> set[str]