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]