diff options
-rw-r--r-- | doc/exts/pylint_messages.py | 82 | ||||
-rw-r--r-- | examples/deprecation_checker.py | 10 | ||||
-rw-r--r-- | pylint/checkers/base_checker.py | 4 | ||||
-rw-r--r-- | pylint/checkers/deprecated.py | 37 | ||||
-rw-r--r-- | pylint/checkers/imports.py | 3 | ||||
-rw-r--r-- | pylint/checkers/stdlib.py | 8 | ||||
-rw-r--r-- | pylint/message/message_definition.py | 3 | ||||
-rw-r--r-- | pylint/typing.py | 1 | ||||
-rw-r--r-- | tests/message/unittest_message_definition.py | 2 | ||||
-rw-r--r-- | tests/message/unittest_message_id_store.py | 2 | ||||
-rw-r--r-- | tests/pyreverse/functional/class_diagrams/colorized_output/colorized.puml | 2 |
11 files changed, 115 insertions, 39 deletions
diff --git a/doc/exts/pylint_messages.py b/doc/exts/pylint_messages.py index 3196b0b2c..bb882ba2b 100644 --- a/doc/exts/pylint_messages.py +++ b/doc/exts/pylint_messages.py @@ -7,7 +7,7 @@ import os from collections import defaultdict from inspect import getmodule -from itertools import chain +from itertools import chain, groupby from pathlib import Path from typing import DefaultDict, Dict, List, NamedTuple, Optional, Tuple @@ -43,6 +43,7 @@ class MessageData(NamedTuple): related_links: str checker_module_name: str checker_module_path: str + shared: bool = False MessagesDict = Dict[str, List[MessageData]] @@ -169,6 +170,7 @@ def _get_all_messages( ((checker, msg) for msg in checker.messages) for checker in linter.get_checkers() ) + for checker, message in checker_message_mapping: good_code, bad_code, details, related = _get_message_data( _get_message_data_path(message) @@ -191,15 +193,21 @@ def _get_all_messages( related, checker_module.__name__, checker_module.__file__, + message.shared, ) msg_type = MSG_TYPES_DOC[message.msgid[0]] messages_dict[msg_type].append(message_data) if message.old_names: for old_name in message.old_names: category = MSG_TYPES_DOC[old_name[0][0]] - old_messages[category][(old_name[1], old_name[0])].append( - (message.symbol, msg_type) - ) + # We check if the message is already in old_messages so + # we don't duplicate shared messages. + if (message.symbol, msg_type) not in old_messages[category][ + (old_name[1], old_name[0]) + ]: + old_messages[category][(old_name[1], old_name[0])].append( + (message.symbol, msg_type) + ) return messages_dict, old_messages @@ -234,19 +242,26 @@ def _write_message_page(messages_dict: MessagesDict) -> None: if not category_dir.exists(): category_dir.mkdir(parents=True, exist_ok=True) for message in messages: + if message.shared: + continue if not _message_needs_update(message, category): continue _write_single_message_page(category_dir, message) + for _, shared_messages in groupby( + sorted( + (message for message in messages if message.shared), key=lambda m: m.id + ), + key=lambda m: m.id, + ): + shared_messages_list = list(shared_messages) + if len(shared_messages_list) > 1: + _write_single_shared_message_page(category_dir, shared_messages_list) + else: + _write_single_message_page(category_dir, shared_messages_list[0]) -def _write_single_message_page(category_dir: Path, message: MessageData) -> None: - checker_module_rel_path = os.path.relpath( - message.checker_module_path, PYLINT_BASE_PATH - ) - messages_file = os.path.join(category_dir, f"{message.name}.rst") - with open(messages_file, "w", encoding="utf-8") as stream: - stream.write( - f""".. _{message.name}: +def _generate_single_message_body(message: MessageData) -> str: + body = f""".. _{message.name}: {get_rst_title(f"{message.name} / {message.id}", "=")} **Message emitted:** @@ -262,19 +277,42 @@ def _write_single_message_page(category_dir: Path, message: MessageData) -> None {message.details} {message.related_links} """ - ) - if message.checker_module_name.startswith("pylint.extensions."): - stream.write( - f""" + if message.checker_module_name.startswith("pylint.extensions."): + body += f""" .. note:: This message is emitted by the optional :ref:`'{message.checker}'<{message.checker_module_name}>` checker which requires the ``{message.checker_module_name}`` plugin to be loaded. """ - ) - checker_url = ( - f"https://github.com/PyCQA/pylint/blob/main/{checker_module_rel_path}" + return body + + +def _generate_checker_url(message: MessageData) -> str: + checker_module_rel_path = os.path.relpath( + message.checker_module_path, PYLINT_BASE_PATH + ) + return f"https://github.com/PyCQA/pylint/blob/main/{checker_module_rel_path}" + + +def _write_single_shared_message_page( + category_dir: Path, messages: List[MessageData] +) -> None: + message = messages[0] + with open(category_dir / f"{message.name}.rst", "w", encoding="utf-8") as stream: + stream.write(_generate_single_message_body(message)) + checker_urls = ", ".join( + [ + f"`{message.checker} <{_generate_checker_url(message)}>`__" + for message in messages + ] ) + stream.write(f"Created by the {checker_urls} checkers.") + + +def _write_single_message_page(category_dir: Path, message: MessageData) -> None: + with open(category_dir / f"{message.name}.rst", "w", encoding="utf-8") as stream: + stream.write(_generate_single_message_body(message)) + checker_url = _generate_checker_url(message) stream.write(f"Created by the `{message.checker} <{checker_url}>`__ checker.") @@ -309,7 +347,11 @@ Pylint can emit the following messages: "refactor", "information", ): - messages = sorted(messages_dict[category], key=lambda item: item.name) + # We need to remove all duplicated shared messages + messages = sorted( + {msg.id: msg for msg in messages_dict[category]}.values(), + key=lambda item: item.name, + ) old_messages = sorted(old_messages_dict[category], key=lambda item: item[0]) messages_string = "".join( f" {category}/{message.name}\n" for message in messages diff --git a/examples/deprecation_checker.py b/examples/deprecation_checker.py index 52244edf6..6fb21eedd 100644 --- a/examples/deprecation_checker.py +++ b/examples/deprecation_checker.py @@ -51,14 +51,20 @@ if TYPE_CHECKING: class DeprecationChecker(DeprecatedMixin, BaseChecker): """Class implementing deprecation checker.""" - # DeprecationMixin class is Mixin class implementing logic for searching deprecated methods and functions. - # The list of deprecated methods/functions is defined by implementing class via deprecated_methods callback. + # DeprecatedMixin class is Mixin class implementing logic for searching deprecated methods and functions. + # The list of deprecated methods/functions is defined by the implementing class via deprecated_methods callback. # DeprecatedMixin class is overriding attributes of BaseChecker hence must be specified *before* BaseChecker # in list of base classes. # The name defines a custom section of the config for this checker. name = "deprecated" + # Register messages emitted by the checker. + msgs = { + **DeprecatedMixin.DEPRECATED_METHOD_MESSAGE, + **DeprecatedMixin.DEPRECATED_ARGUMENT_MESSAGE, + } + def deprecated_methods(self) -> set[str]: """Callback method called by DeprecatedMixin for every method/function found in the code. diff --git a/pylint/checkers/base_checker.py b/pylint/checkers/base_checker.py index a2dfe7b17..37ae236d2 100644 --- a/pylint/checkers/base_checker.py +++ b/pylint/checkers/base_checker.py @@ -178,6 +178,10 @@ class BaseChecker(_ArgumentsProvider): checker_id = None existing_ids = [] for message in self.messages: + # Id's for shared messages such as the 'deprecated-*' messages + # can be inconsistent with their checker id. + if message.shared: + continue if checker_id is not None and checker_id != message.msgid[1:3]: error_msg = "Inconsistent checker part in message id " error_msg += f"'{message.msgid}' (expected 'x{checker_id}xx' " diff --git a/pylint/checkers/deprecated.py b/pylint/checkers/deprecated.py index ea33ff6bc..78d73586b 100644 --- a/pylint/checkers/deprecated.py +++ b/pylint/checkers/deprecated.py @@ -31,31 +31,48 @@ class DeprecatedMixin(BaseChecker): A class implementing mixin must define "deprecated-method" Message. """ - msgs: dict[str, MessageDefinitionTuple] = { - "W1505": ( + DEPRECATED_MODULE_MESSAGE: dict[str, MessageDefinitionTuple] = { + "W4901": ( + "Deprecated module %r", + "deprecated-module", + "A module marked as deprecated is imported.", + {"old_names": [("W0402", "old-deprecated-module")], "shared": True}, + ), + } + + DEPRECATED_METHOD_MESSAGE: dict[str, MessageDefinitionTuple] = { + "W4902": ( "Using deprecated method %s()", "deprecated-method", "The method is marked as deprecated and will be removed in the future.", + {"old_names": [("W1505", "old-deprecated-method")], "shared": True}, ), - "W1511": ( + } + + DEPRECATED_ARGUMENT_MESSAGE: dict[str, MessageDefinitionTuple] = { + "W4903": ( "Using deprecated argument %s of method %s()", "deprecated-argument", "The argument is marked as deprecated and will be removed in the future.", + {"old_names": [("W1511", "old-deprecated-argument")], "shared": True}, ), - "W0402": ( - "Deprecated module %r", - "deprecated-module", - "A module marked as deprecated is imported.", - ), - "W1512": ( + } + + DEPRECATED_CLASS_MESSAGE: dict[str, MessageDefinitionTuple] = { + "W4904": ( "Using deprecated class %s of module %s", "deprecated-class", "The class is marked as deprecated and will be removed in the future.", + {"old_names": [("W1512", "old-deprecated-class")], "shared": True}, ), - "W1513": ( + } + + DEPRECATED_DECORATOR_MESSAGE: dict[str, MessageDefinitionTuple] = { + "W4905": ( "Using deprecated decorator %s()", "deprecated-decorator", "The decorator is marked as deprecated and will be removed in the future.", + {"old_names": [("W1513", "old-deprecated-decorator")], "shared": True}, ), } diff --git a/pylint/checkers/imports.py b/pylint/checkers/imports.py index 0119c9bcf..c4015e685 100644 --- a/pylint/checkers/imports.py +++ b/pylint/checkers/imports.py @@ -197,7 +197,6 @@ def _make_graph( # the import checker itself ################################################### MSGS: dict[str, MessageDefinitionTuple] = { - **{k: v for k, v in DeprecatedMixin.msgs.items() if k[1:3] == "04"}, "E0401": ( "Unable to import %s", "import-error", @@ -302,7 +301,7 @@ class ImportsChecker(DeprecatedMixin, BaseChecker): """ name = "imports" - msgs = MSGS + msgs = {**DeprecatedMixin.DEPRECATED_MODULE_MESSAGE, **MSGS} default_deprecated_modules = () options = ( diff --git a/pylint/checkers/stdlib.py b/pylint/checkers/stdlib.py index 8aa5fc8f6..2e6f8c182 100644 --- a/pylint/checkers/stdlib.py +++ b/pylint/checkers/stdlib.py @@ -15,6 +15,7 @@ from astroid import nodes from pylint import interfaces from pylint.checkers import BaseChecker, DeprecatedMixin, utils +from pylint.typing import MessageDefinitionTuple if TYPE_CHECKING: from pylint.lint import PyLinter @@ -332,8 +333,11 @@ def _check_mode_str(mode): class StdlibChecker(DeprecatedMixin, BaseChecker): name = "stdlib" - msgs = { - **{k: v for k, v in DeprecatedMixin.msgs.items() if k[1:3] == "15"}, + msgs: dict[str, MessageDefinitionTuple] = { + **DeprecatedMixin.DEPRECATED_METHOD_MESSAGE, + **DeprecatedMixin.DEPRECATED_ARGUMENT_MESSAGE, + **DeprecatedMixin.DEPRECATED_CLASS_MESSAGE, + **DeprecatedMixin.DEPRECATED_DECORATOR_MESSAGE, "W1501": ( '"%s" is not a valid mode for open.', "bad-open-mode", diff --git a/pylint/message/message_definition.py b/pylint/message/message_definition.py index 714ce7efa..3b403b008 100644 --- a/pylint/message/message_definition.py +++ b/pylint/message/message_definition.py @@ -18,6 +18,7 @@ if TYPE_CHECKING: class MessageDefinition: + # pylint: disable-next=too-many-arguments def __init__( self, checker: BaseChecker, @@ -29,6 +30,7 @@ class MessageDefinition: minversion: tuple[int, int] | None = None, maxversion: tuple[int, int] | None = None, old_names: list[tuple[str, str]] | None = None, + shared: bool = False, ) -> None: self.checker_name = checker.name self.check_msgid(msgid) @@ -39,6 +41,7 @@ class MessageDefinition: self.scope = scope self.minversion = minversion self.maxversion = maxversion + self.shared = shared self.old_names: list[tuple[str, str]] = [] if old_names: for old_msgid, old_symbol in old_names: diff --git a/pylint/typing.py b/pylint/typing.py index 7ceb33f91..224e0bd6b 100644 --- a/pylint/typing.py +++ b/pylint/typing.py @@ -123,6 +123,7 @@ class ExtraMessageOptions(TypedDict, total=False): old_names: list[tuple[str, str]] maxversion: tuple[int, int] minversion: tuple[int, int] + shared: bool MessageDefinitionTuple = Union[ diff --git a/tests/message/unittest_message_definition.py b/tests/message/unittest_message_definition.py index 2a3fd7a6a..d42a249e3 100644 --- a/tests/message/unittest_message_definition.py +++ b/tests/message/unittest_message_definition.py @@ -64,7 +64,6 @@ class TestMessagesDefinition: @staticmethod def get_message_definition() -> MessageDefinition: - kwargs = {"minversion": None, "maxversion": None} return MessageDefinition( FalseChecker(), "W1234", @@ -72,7 +71,6 @@ class TestMessagesDefinition: "description", "msg-symbol", WarningScope.NODE, - **kwargs, ) def test_may_be_emitted(self) -> None: diff --git a/tests/message/unittest_message_id_store.py b/tests/message/unittest_message_id_store.py index f300b4f19..d2f0e5ca1 100644 --- a/tests/message/unittest_message_id_store.py +++ b/tests/message/unittest_message_id_store.py @@ -131,6 +131,8 @@ def test_exclusivity_of_msgids() -> None: } for msgid, definition in runner.linter.msgs_store._messages_definitions.items(): + if definition.shared: + continue if msgid[1:3] in checker_id_pairs: assert ( definition.checker_name in checker_id_pairs[msgid[1:3]] diff --git a/tests/pyreverse/functional/class_diagrams/colorized_output/colorized.puml b/tests/pyreverse/functional/class_diagrams/colorized_output/colorized.puml index 6a4e356ab..7e8b127c7 100644 --- a/tests/pyreverse/functional/class_diagrams/colorized_output/colorized.puml +++ b/tests/pyreverse/functional/class_diagrams/colorized_output/colorized.puml @@ -23,7 +23,7 @@ class "ExceptionsChecker" as pylint.checkers.exceptions.ExceptionsChecker #aquam visit_tryexcept(node: nodes.TryExcept) -> None } class "StdlibChecker" as pylint.checkers.stdlib.StdlibChecker #aquamarine { - msgs + msgs : dict[str, MessageDefinitionTuple] name : str deprecated_arguments(method: str) deprecated_classes(module: str) |