From 87c6d0cc2fe0bb4ac39042abc2394d285450ba45 Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Thu, 4 May 2023 14:44:45 +0200 Subject: [PATCH 1/7] [suggestion-mode] Remove the option and always suggest _is_c_extension already checks if owner is a Module See https://github.com/pylint-dev/pylint/pull/9962#discussion_r1782040559 Caching start to make sense in _similar_name See https://github.com/pylint-dev/pylint/pull/9962#discussion_r1782042663 We had to create a clean_lru_cache file to prevent a circular import. --- doc/user_guide/configuration/all-options.rst | 9 --- doc/whatsnew/fragments/9962.breaking | 4 ++ examples/pylintrc | 4 -- examples/pyproject.toml | 4 -- pylint/checkers/clear_lru_cache.py | 37 ++++++++++++ pylint/checkers/typecheck.py | 59 ++++++-------------- pylint/checkers/utils.py | 18 +----- pylint/lint/base_options.py | 13 ----- pylint/lint/run.py | 2 +- pylint/utils/utils.py | 1 - pylintrc | 4 -- tests/checkers/unittest_typecheck.py | 25 +-------- 12 files changed, 62 insertions(+), 118 deletions(-) create mode 100644 doc/whatsnew/fragments/9962.breaking create mode 100644 pylint/checkers/clear_lru_cache.py diff --git a/doc/user_guide/configuration/all-options.rst b/doc/user_guide/configuration/all-options.rst index 82ade5b522..7c1093024a 100644 --- a/doc/user_guide/configuration/all-options.rst +++ b/doc/user_guide/configuration/all-options.rst @@ -209,13 +209,6 @@ Standard Checkers **Default:** ``()`` ---suggestion-mode -""""""""""""""""" -*When enabled, pylint would attempt to guess common misconfiguration and emit user-friendly hints instead of false-positive error messages.* - -**Default:** ``True`` - - --unsafe-load-any-extension """"""""""""""""""""""""""" *Allow loading of arbitrary C extensions. Extensions are imported into the active Python interpreter and may run arbitrary code.* @@ -290,8 +283,6 @@ Standard Checkers source-roots = [] - suggestion-mode = true - unsafe-load-any-extension = false diff --git a/doc/whatsnew/fragments/9962.breaking b/doc/whatsnew/fragments/9962.breaking new file mode 100644 index 0000000000..f3a5e95572 --- /dev/null +++ b/doc/whatsnew/fragments/9962.breaking @@ -0,0 +1,4 @@ +The ``suggestion-mode`` option was removed, as pylint now always emits user-friendly hints instead +of false-positive error messages. You should remove it from your conf if it's defined. + +Refs #9962 diff --git a/examples/pylintrc b/examples/pylintrc index dd9e3b1770..63ee59c58f 100644 --- a/examples/pylintrc +++ b/examples/pylintrc @@ -104,10 +104,6 @@ recursive=no # source root. source-roots= -# When enabled, pylint would attempt to guess common misconfiguration and emit -# user-friendly hints instead of false-positive error messages. -suggestion-mode=yes - # Allow loading of arbitrary C extensions. Extensions are imported into the # active Python interpreter and may run arbitrary code. unsafe-load-any-extension=no diff --git a/examples/pyproject.toml b/examples/pyproject.toml index d914258006..1399791bf7 100644 --- a/examples/pyproject.toml +++ b/examples/pyproject.toml @@ -94,10 +94,6 @@ py-version = "3.12" # source root. # source-roots = -# When enabled, pylint would attempt to guess common misconfiguration and emit -# user-friendly hints instead of false-positive error messages. -suggestion-mode = true - # Allow loading of arbitrary C extensions. Extensions are imported into the # active Python interpreter and may run arbitrary code. # unsafe-load-any-extension = diff --git a/pylint/checkers/clear_lru_cache.py b/pylint/checkers/clear_lru_cache.py new file mode 100644 index 0000000000..128ee183be --- /dev/null +++ b/pylint/checkers/clear_lru_cache.py @@ -0,0 +1,37 @@ +# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +from pylint.checkers.typecheck import _similar_names +from pylint.checkers.utils import ( + class_is_abstract, + in_for_else_branch, + infer_all, + is_overload_stub, + overridden_method, + safe_infer, + unimplemented_abstract_methods, +) + +if TYPE_CHECKING: + from functools import _lru_cache_wrapper + + +def clear_lru_caches() -> None: + """Clear caches holding references to AST nodes.""" + caches_holding_node_references: list[_lru_cache_wrapper[Any]] = [ + class_is_abstract, + in_for_else_branch, + infer_all, + is_overload_stub, + overridden_method, + unimplemented_abstract_methods, + safe_infer, + _similar_names, + ] + for lru in caches_holding_node_references: + lru.cache_clear() diff --git a/pylint/checkers/typecheck.py b/pylint/checkers/typecheck.py index e1475fa729..7953e640b4 100644 --- a/pylint/checkers/typecheck.py +++ b/pylint/checkers/typecheck.py @@ -13,7 +13,7 @@ import shlex import sys from collections.abc import Callable, Iterable -from functools import cached_property, singledispatch +from functools import cached_property, lru_cache, singledispatch from re import Pattern from typing import TYPE_CHECKING, Any, Literal, Union @@ -172,6 +172,7 @@ def _string_distance(seq1: str, seq2: str, seq1_length: int, seq2_length: int) - return row[seq2_length - 1] +@lru_cache(maxsize=256) def _similar_names( owner: SuccessfulInferenceResult, attrname: str | None, @@ -214,26 +215,6 @@ def _similar_names( return sorted(picked) -def _missing_member_hint( - owner: SuccessfulInferenceResult, - attrname: str | None, - distance_threshold: int, - max_choices: int, -) -> str: - names = _similar_names(owner, attrname, distance_threshold, max_choices) - if not names: - # No similar name. - return "" - - names = [repr(name) for name in names] - if len(names) == 1: - names_hint = ", ".join(names) - else: - names_hint = f"one of {', '.join(names[:-1])} or {names[-1]}" - - return f"; maybe {names_hint}?" - - MSGS: dict[str, MessageDefinitionTuple] = { "E1101": ( "%s %r has no %r member%s", @@ -997,10 +978,6 @@ def open(self) -> None: self._py310_plus = py_version >= (3, 10) self._mixin_class_rgx = self.linter.config.mixin_class_rgx - @cached_property - def _suggestion_mode(self) -> bool: - return self.linter.config.suggestion_mode # type: ignore[no-any-return] - @cached_property def _compiled_generated_members(self) -> tuple[Pattern[str], ...]: # do this lazily since config not fully initialized in __init__ @@ -1211,24 +1188,24 @@ def _get_nomember_msgid_hint( node: nodes.Attribute | nodes.AssignAttr | nodes.DelAttr, owner: SuccessfulInferenceResult, ) -> tuple[Literal["c-extension-no-member", "no-member"], str]: - suggestions_are_possible = self._suggestion_mode and isinstance( - owner, nodes.Module + if _is_c_extension(owner): + return "c-extension-no-member", "" + if not self.linter.config.missing_member_hint: + return "no-member", "" + names = _similar_names( + owner, + node.attrname, + self.linter.config.missing_member_hint_distance, + self.linter.config.missing_member_max_choices, ) - if suggestions_are_possible and _is_c_extension(owner): - msg = "c-extension-no-member" - hint = "" + if not names: + return "no-member", "" + names = [repr(name) for name in names] + if len(names) == 1: + names_hint = names[0] else: - msg = "no-member" - if self.linter.config.missing_member_hint: - hint = _missing_member_hint( - owner, - node.attrname, - self.linter.config.missing_member_hint_distance, - self.linter.config.missing_member_max_choices, - ) - else: - hint = "" - return msg, hint # type: ignore[return-value] + names_hint = f"one of {', '.join(names[:-1])} or {names[-1]}" + return "no-member", f"; maybe {names_hint}?" @only_required_for_messages( "assignment-from-no-return", diff --git a/pylint/checkers/utils.py b/pylint/checkers/utils.py index 1edd58b4c7..d77a392574 100644 --- a/pylint/checkers/utils.py +++ b/pylint/checkers/utils.py @@ -16,7 +16,7 @@ from collections.abc import Callable, Iterable, Iterator from functools import lru_cache, partial from re import Match -from typing import TYPE_CHECKING, Any, TypeVar +from typing import TYPE_CHECKING, TypeVar import astroid.objects from astroid import TooManyLevelsError, nodes, util @@ -28,7 +28,6 @@ from pylint.constants import TYPING_NEVER, TYPING_NORETURN if TYPE_CHECKING: - from functools import _lru_cache_wrapper from pylint.checkers import BaseChecker @@ -2327,21 +2326,6 @@ def overridden_method( return None # pragma: no cover -def clear_lru_caches() -> None: - """Clear caches holding references to AST nodes.""" - caches_holding_node_references: list[_lru_cache_wrapper[Any]] = [ - class_is_abstract, - in_for_else_branch, - infer_all, - is_overload_stub, - overridden_method, - unimplemented_abstract_methods, - safe_infer, - ] - for lru in caches_holding_node_references: - lru.cache_clear() - - def is_enum_member(node: nodes.AssignName) -> bool: """Return `True` if `node` is an Enum member (is an item of the `__members__` container). diff --git a/pylint/lint/base_options.py b/pylint/lint/base_options.py index aa619f26e0..4ec84d2e9e 100644 --- a/pylint/lint/base_options.py +++ b/pylint/lint/base_options.py @@ -306,19 +306,6 @@ def _make_linter_options(linter: PyLinter) -> Options: ), }, ), - ( - "suggestion-mode", - { - "type": "yn", - "metavar": "", - "default": True, - "help": ( - "When enabled, pylint would attempt to guess common " - "misconfiguration and emit user-friendly hints instead " - "of false-positive error messages." - ), - }, - ), ( "exit-zero", { diff --git a/pylint/lint/run.py b/pylint/lint/run.py index dc595fda96..f62be71d24 100644 --- a/pylint/lint/run.py +++ b/pylint/lint/run.py @@ -12,7 +12,7 @@ from typing import ClassVar from pylint import config -from pylint.checkers.utils import clear_lru_caches +from pylint.checkers.clear_lru_cache import clear_lru_caches from pylint.config._pylint_config import ( _handle_pylint_config_commands, _register_generate_config_options, diff --git a/pylint/utils/utils.py b/pylint/utils/utils.py index c17f2c0c27..341a4aec6d 100644 --- a/pylint/utils/utils.py +++ b/pylint/utils/utils.py @@ -40,7 +40,6 @@ # These are types used to overload get_global_option() and refer to the options type GLOBAL_OPTION_BOOL = Literal[ - "suggestion-mode", "analyse-fallback-blocks", "allow-global-unused-variables", "prefer-stubs", diff --git a/pylintrc b/pylintrc index 3a96cdc756..6ccfa56650 100644 --- a/pylintrc +++ b/pylintrc @@ -40,10 +40,6 @@ load-plugins= # number of processors available to use. jobs=1 -# When enabled, pylint would attempt to guess common misconfiguration and emit -# user-friendly hints instead of false-positive error messages. -suggestion-mode=yes - # Allow loading of arbitrary C extensions. Extensions are imported into the # active Python interpreter and may run arbitrary code. unsafe-load-any-extension=no diff --git a/tests/checkers/unittest_typecheck.py b/tests/checkers/unittest_typecheck.py index d3fd5a34c0..6b93fb1724 100644 --- a/tests/checkers/unittest_typecheck.py +++ b/tests/checkers/unittest_typecheck.py @@ -7,7 +7,7 @@ from pylint.checkers import typecheck from pylint.interfaces import INFERENCE, UNDEFINED -from pylint.testutils import CheckerTestCase, MessageTest, set_config +from pylint.testutils import CheckerTestCase, MessageTest try: from coverage import tracer as _ @@ -27,29 +27,6 @@ class TestTypeChecker(CheckerTestCase): CHECKER_CLASS = typecheck.TypeChecker - @set_config(suggestion_mode=False) - @needs_c_extension - def test_nomember_on_c_extension_error_msg(self) -> None: - node = astroid.extract_node( - """ - from coverage import tracer - tracer.CTracer #@ - """ - ) - message = MessageTest( - "no-member", - node=node, - args=("Module", "coverage.tracer", "CTracer", ""), - confidence=INFERENCE, - line=3, - col_offset=0, - end_line=3, - end_col_offset=14, - ) - with self.assertAddsMessages(message): - self.checker.visit_attribute(node) - - @set_config(suggestion_mode=True) @needs_c_extension def test_nomember_on_c_extension_info_msg(self) -> None: node = astroid.extract_node( From 69a8b103e138f3f7a2dcbaa37a3a987d4d85fd5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20Noord?= <13665637+DanielNoord@users.noreply.github.com> Date: Sun, 30 Mar 2025 21:52:47 +0200 Subject: [PATCH 2/7] Show what is wrong --- tests/test_self.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_self.py b/tests/test_self.py index 4eda915dfc..88e9bc9682 100644 --- a/tests/test_self.py +++ b/tests/test_self.py @@ -1536,6 +1536,7 @@ def test_generate_rcfile(tmp_path: Path) -> None: [join(HERE, "regrtest_data", "empty.py"), f"--rcfile={filename}"], exit=False, ) + assert not runner.linter.reporter.messages assert not runner.linter.msg_status os.remove(filename) From ca117b4520bdd87434bd96341dcf603ee7b7ab54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20Noord?= <13665637+DanielNoord@users.noreply.github.com> Date: Sun, 30 Mar 2025 22:05:49 +0200 Subject: [PATCH 3/7] Show all stats --- tests/test_self.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_self.py b/tests/test_self.py index 88e9bc9682..2d9437d23b 100644 --- a/tests/test_self.py +++ b/tests/test_self.py @@ -1536,7 +1536,7 @@ def test_generate_rcfile(tmp_path: Path) -> None: [join(HERE, "regrtest_data", "empty.py"), f"--rcfile={filename}"], exit=False, ) - assert not runner.linter.reporter.messages + assert not runner.linter.stats assert not runner.linter.msg_status os.remove(filename) From 3911092d03b7684bc40768266f8e6fada46f116b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20Noord?= <13665637+DanielNoord@users.noreply.github.com> Date: Sun, 30 Mar 2025 22:42:39 +0200 Subject: [PATCH 4/7] Show less statistics --- tests/test_self.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_self.py b/tests/test_self.py index 2d9437d23b..a6888f4b99 100644 --- a/tests/test_self.py +++ b/tests/test_self.py @@ -1536,7 +1536,7 @@ def test_generate_rcfile(tmp_path: Path) -> None: [join(HERE, "regrtest_data", "empty.py"), f"--rcfile={filename}"], exit=False, ) - assert not runner.linter.stats + assert not runner.linter.stats.by_msg assert not runner.linter.msg_status os.remove(filename) From 3d3f198080f59f9c202cfe5c713ec054c25fb1b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20Noord?= <13665637+DanielNoord@users.noreply.github.com> Date: Sun, 30 Mar 2025 22:49:48 +0200 Subject: [PATCH 5/7] Raise `ValueError` --- pylint/config/config_initialization.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pylint/config/config_initialization.py b/pylint/config/config_initialization.py index 5be28e4326..4b98784d57 100644 --- a/pylint/config/config_initialization.py +++ b/pylint/config/config_initialization.py @@ -107,6 +107,7 @@ def _config_initialization( # pylint: disable=too-many-statements # with all disables, it is safe to emit messages if unrecognized_options_message is not None: linter.set_current_module(str(config_file) if config_file else "") + raise ValueError(unrecognized_options_message) linter.add_message( "unrecognized-option", args=unrecognized_options_message, line=0 ) From 0dcce0a6808b0df384ca28dee64761d0520bda9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20Noord?= <13665637+DanielNoord@users.noreply.github.com> Date: Sun, 30 Mar 2025 22:57:21 +0200 Subject: [PATCH 6/7] Test assumptions --- pylint/config/config_initialization.py | 1 - tests/test_self.py | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pylint/config/config_initialization.py b/pylint/config/config_initialization.py index 4b98784d57..5be28e4326 100644 --- a/pylint/config/config_initialization.py +++ b/pylint/config/config_initialization.py @@ -107,7 +107,6 @@ def _config_initialization( # pylint: disable=too-many-statements # with all disables, it is safe to emit messages if unrecognized_options_message is not None: linter.set_current_module(str(config_file) if config_file else "") - raise ValueError(unrecognized_options_message) linter.add_message( "unrecognized-option", args=unrecognized_options_message, line=0 ) diff --git a/tests/test_self.py b/tests/test_self.py index a6888f4b99..bf45aaa0d0 100644 --- a/tests/test_self.py +++ b/tests/test_self.py @@ -1528,6 +1528,8 @@ def test_generate_rcfile(tmp_path: Path) -> None: ) assert process.stdout == process_two.stdout + assert "suggestion-mode" not in process.stdout + # Check that the generated file is valid with tempfile.NamedTemporaryFile(mode="w", delete=False) as temp: filename = temp.name @@ -1536,7 +1538,6 @@ def test_generate_rcfile(tmp_path: Path) -> None: [join(HERE, "regrtest_data", "empty.py"), f"--rcfile={filename}"], exit=False, ) - assert not runner.linter.stats.by_msg assert not runner.linter.msg_status os.remove(filename) From 8eb2dbf2d597e58e9f6926b1d49a3f3a4f4e87f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20Noord?= <13665637+DanielNoord@users.noreply.github.com> Date: Sun, 30 Mar 2025 14:11:40 -0700 Subject: [PATCH 7/7] Bump CACHE_VERSION --- .github/workflows/tests.yaml | 2 +- tests/test_self.py | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index ca505d1779..5a6ac5f87b 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -13,7 +13,7 @@ on: - "maintenance/**" env: - CACHE_VERSION: 4 + CACHE_VERSION: 5 KEY_PREFIX: venv permissions: diff --git a/tests/test_self.py b/tests/test_self.py index bf45aaa0d0..4eda915dfc 100644 --- a/tests/test_self.py +++ b/tests/test_self.py @@ -1528,8 +1528,6 @@ def test_generate_rcfile(tmp_path: Path) -> None: ) assert process.stdout == process_two.stdout - assert "suggestion-mode" not in process.stdout - # Check that the generated file is valid with tempfile.NamedTemporaryFile(mode="w", delete=False) as temp: filename = temp.name