From 53d4541eb52c3f249df8740e5508171a57be31ea Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 29 Oct 2023 19:00:53 +0000 Subject: [PATCH 01/10] Fix `undefined-variable` etc for Python 3.12 generic type syntax (#9195) (#9199) (cherry picked from commit c648f164fbeab378389740469a9782d6d70f7ca1) Co-authored-by: Jacob Walls --- doc/whatsnew/fragments/9193.false_positive | 4 ++++ pylint/checkers/variables.py | 12 +++++++++--- .../u/undefined/undefined_variable_py312.py | 7 +++++++ .../u/undefined/undefined_variable_py312.rc | 2 ++ 4 files changed, 22 insertions(+), 3 deletions(-) create mode 100644 doc/whatsnew/fragments/9193.false_positive create mode 100644 tests/functional/u/undefined/undefined_variable_py312.py create mode 100644 tests/functional/u/undefined/undefined_variable_py312.rc diff --git a/doc/whatsnew/fragments/9193.false_positive b/doc/whatsnew/fragments/9193.false_positive new file mode 100644 index 0000000000..39dc70b81d --- /dev/null +++ b/doc/whatsnew/fragments/9193.false_positive @@ -0,0 +1,4 @@ +Fix false positives for ``undefined-variable`` and ``unused-argument`` for +classes and functions using Python 3.12 generic type syntax. + +Closes #9193 diff --git a/pylint/checkers/variables.py b/pylint/checkers/variables.py index 6cf3924665..c06efc3d85 100644 --- a/pylint/checkers/variables.py +++ b/pylint/checkers/variables.py @@ -1738,6 +1738,9 @@ def _should_node_be_skipped( elif consumer.scope_type == "function" and self._defined_in_function_definition( node, consumer.node ): + if any(node.name == param.name.name for param in consumer.node.type_params): + return False + # If the name node is used as a function default argument's value or as # a decorator, then start from the parent frame of the function instead # of the function frame - and thus open an inner class scope @@ -2262,10 +2265,13 @@ def _is_variable_violation( isinstance(defframe, nodes.FunctionDef) and frame is defframe and defframe.parent_of(node) - and stmt is not defstmt + and ( + defnode in defframe.type_params + # Single statement function, with the statement on the + # same line as the function definition + or stmt is not defstmt + ) ): - # Single statement function, with the statement on the - # same line as the function definition maybe_before_assign = False elif ( isinstance(defstmt, NODES_WITH_VALUE_ATTR) diff --git a/tests/functional/u/undefined/undefined_variable_py312.py b/tests/functional/u/undefined/undefined_variable_py312.py new file mode 100644 index 0000000000..0ca2475eb9 --- /dev/null +++ b/tests/functional/u/undefined/undefined_variable_py312.py @@ -0,0 +1,7 @@ +# pylint: disable=missing-function-docstring,missing-module-docstring,missing-class-docstring,too-few-public-methods + +def f[T](a: T) -> T: + print(a) + +class ChildClass[T, *Ts, **P]: + ... diff --git a/tests/functional/u/undefined/undefined_variable_py312.rc b/tests/functional/u/undefined/undefined_variable_py312.rc new file mode 100644 index 0000000000..9c966d4bda --- /dev/null +++ b/tests/functional/u/undefined/undefined_variable_py312.rc @@ -0,0 +1,2 @@ +[testoptions] +min_pyver=3.12 From 0273db7fb4b77c882e40cd766c61a56a0cb2b8e2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 29 Oct 2023 19:45:28 +0000 Subject: [PATCH 02/10] Fix false positive for ``unnecessary-lambda``. (#9149) (#9200) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This simplifies the check for the call having kwargs but not the lambda. (cherry picked from commit 2289c7156638dc64c49d149e0bdd23f50fc471a4) Co-authored-by: Clément Schreiner --- doc/whatsnew/fragments/9148.false_positive | 3 +++ pylint/checkers/base/basic_checker.py | 16 +++------------- .../u/unnecessary/unnecessary_lambda.py | 4 ++++ 3 files changed, 10 insertions(+), 13 deletions(-) create mode 100644 doc/whatsnew/fragments/9148.false_positive diff --git a/doc/whatsnew/fragments/9148.false_positive b/doc/whatsnew/fragments/9148.false_positive new file mode 100644 index 0000000000..647deb103e --- /dev/null +++ b/doc/whatsnew/fragments/9148.false_positive @@ -0,0 +1,3 @@ +Fixed false positive for ``unnecessary-lambda`` when the call has keyword arguments but not the lambda. + +Closes #9148 diff --git a/pylint/checkers/base/basic_checker.py b/pylint/checkers/base/basic_checker.py index 3505ee7082..8aaa274d65 100644 --- a/pylint/checkers/base/basic_checker.py +++ b/pylint/checkers/base/basic_checker.py @@ -519,7 +519,6 @@ def _has_variadic_argument( ) @utils.only_required_for_messages("unnecessary-lambda") - # pylint: disable-next=too-many-return-statements def visit_lambda(self, node: nodes.Lambda) -> None: """Check whether the lambda is suspicious.""" # if the body of the lambda is a call expression with the same @@ -544,12 +543,13 @@ def visit_lambda(self, node: nodes.Lambda) -> None: # return something else (but we don't check that, yet). return - call_site = astroid.arguments.CallSite.from_call(call) ordinary_args = list(node.args.args) new_call_args = list(self._filter_vararg(node, call.args)) if node.args.kwarg: - if self._has_variadic_argument(call.kwargs, node.args.kwarg): + if self._has_variadic_argument(call.keywords, node.args.kwarg): return + elif call.keywords: + return if node.args.vararg: if self._has_variadic_argument(call.starargs, node.args.vararg): @@ -557,16 +557,6 @@ def visit_lambda(self, node: nodes.Lambda) -> None: elif call.starargs: return - if call.keywords: - # Look for additional keyword arguments that are not part - # of the lambda's signature - lambda_kwargs = {keyword.name for keyword in node.args.defaults} - if len(lambda_kwargs) != len(call_site.keyword_arguments): - # Different lengths, so probably not identical - return - if set(call_site.keyword_arguments).difference(lambda_kwargs): - return - # The "ordinary" arguments must be in a correspondence such that: # ordinary_args[i].name == call.args[i].name. if len(ordinary_args) != len(new_call_args): diff --git a/tests/functional/u/unnecessary/unnecessary_lambda.py b/tests/functional/u/unnecessary/unnecessary_lambda.py index 82571a4444..85c30fef28 100644 --- a/tests/functional/u/unnecessary/unnecessary_lambda.py +++ b/tests/functional/u/unnecessary/unnecessary_lambda.py @@ -52,6 +52,10 @@ _ = lambda: _ANYARGS(*[3], **{'three': 3}) _ = lambda: _ANYARGS(func=42) +# pylint: disable=missing-function-docstring +def f(d): + print(lambda x: str(x, **d)) + # Don't warn about this. _ = lambda: code().analysis() From dc11223e95783bdbf337baea7c2a141e1897d06c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 30 Oct 2023 10:54:38 +0000 Subject: [PATCH 03/10] [todos] Fix the todos version and the warnings' text (#9202) (#9204) (cherry picked from commit 5546fe8430cca36e217fe25d770328ddfaf24454) Co-authored-by: Pierre Sassoulas --- pylint/checkers/similar.py | 2 +- pylint/checkers/variables.py | 2 +- pylint/config/callback_actions.py | 2 +- pylint/config/config_initialization.py | 5 +++-- pylint/testutils/functional/test_file.py | 2 +- pylint/utils/utils.py | 4 ++-- tests/test_check_parallel.py | 2 +- 7 files changed, 10 insertions(+), 9 deletions(-) diff --git a/pylint/checkers/similar.py b/pylint/checkers/similar.py index b4091631ad..4b413924fe 100644 --- a/pylint/checkers/similar.py +++ b/pylint/checkers/similar.py @@ -839,7 +839,7 @@ def process_module(self, node: nodes.Module) -> None: stream must implement the readlines method """ if self.linter.current_name is None: - # TODO: 3.0 Fix current_name + # TODO: 4.0 Fix current_name warnings.warn( ( "In pylint 3.0 the current_name attribute of the linter object should be a string. " diff --git a/pylint/checkers/variables.py b/pylint/checkers/variables.py index c06efc3d85..925088f60d 100644 --- a/pylint/checkers/variables.py +++ b/pylint/checkers/variables.py @@ -2571,7 +2571,7 @@ def _loopvar_name(self, node: astroid.Name) -> None: else_stmt, (nodes.Return, nodes.Raise, nodes.Break, nodes.Continue) ): return - # TODO: 3.0: Consider using RefactoringChecker._is_function_def_never_returning + # TODO: 4.0: Consider using RefactoringChecker._is_function_def_never_returning if isinstance(else_stmt, nodes.Expr) and isinstance( else_stmt.value, nodes.Call ): diff --git a/pylint/config/callback_actions.py b/pylint/config/callback_actions.py index 0044e1fcd2..bf2decd3c6 100644 --- a/pylint/config/callback_actions.py +++ b/pylint/config/callback_actions.py @@ -243,7 +243,7 @@ def __call__( values: str | Sequence[Any] | None, option_string: str | None = "--generate-rcfile", ) -> None: - # TODO: 3.x: Deprecate this after the auto-upgrade functionality of + # TODO: 4.x: Deprecate this after the auto-upgrade functionality of # pylint-config is sufficient. self.run.linter._generate_config(skipsections=("Commands",)) sys.exit(0) diff --git a/pylint/config/config_initialization.py b/pylint/config/config_initialization.py index 514bdcd815..6fa7b6b895 100644 --- a/pylint/config/config_initialization.py +++ b/pylint/config/config_initialization.py @@ -110,13 +110,14 @@ def _config_initialization( "unrecognized-option", args=unrecognized_options_message, line=0 ) - # TODO 3.1: Change this to emit unknown-option-value + # TODO: Change this to be checked only when upgrading the configuration for exc_name in linter.config.overgeneral_exceptions: if "." not in exc_name: warnings.warn_explicit( f"'{exc_name}' is not a proper value for the 'overgeneral-exceptions' option. " f"Use fully qualified name (maybe 'builtins.{exc_name}' ?) instead. " - "This will cease to be checked at runtime in 3.1.0.", + "This will cease to be checked at runtime when the configuration " + "upgrader is released.", category=UserWarning, filename="pylint: Command line or configuration file", lineno=1, diff --git a/pylint/testutils/functional/test_file.py b/pylint/testutils/functional/test_file.py index 16593b5c4f..37ba3a5fc6 100644 --- a/pylint/testutils/functional/test_file.py +++ b/pylint/testutils/functional/test_file.py @@ -56,7 +56,7 @@ class FunctionalTestFile: def __init__(self, directory: str, filename: str) -> None: self._directory = directory self.base = filename.replace(".py", "") - # TODO: 3.0: Deprecate FunctionalTestFile.options and related code + # TODO:4.0: Deprecate FunctionalTestFile.options and related code # We should just parse these options like a normal configuration file. self.options: TestFileOptions = { "min_pyver": (2, 5), diff --git a/pylint/utils/utils.py b/pylint/utils/utils.py index 8f16d2f1f4..91f1cdd8b1 100644 --- a/pylint/utils/utils.py +++ b/pylint/utils/utils.py @@ -315,7 +315,7 @@ def format_section( ) -> None: """Format an option's section using the INI format.""" warnings.warn( - "format_section has been deprecated. It will be removed in pylint 3.0.", + "format_section has been deprecated. It will be removed in pylint 4.0.", DeprecationWarning, stacklevel=2, ) @@ -330,7 +330,7 @@ def format_section( def _ini_format(stream: TextIO, options: list[tuple[str, OptionDict, Any]]) -> None: """Format options using the INI format.""" warnings.warn( - "_ini_format has been deprecated. It will be removed in pylint 3.0.", + "_ini_format has been deprecated. It will be removed in pylint 4.0.", DeprecationWarning, stacklevel=2, ) diff --git a/tests/test_check_parallel.py b/tests/test_check_parallel.py index f212df850b..0ae3b1ae10 100644 --- a/tests/test_check_parallel.py +++ b/tests/test_check_parallel.py @@ -269,7 +269,7 @@ def test_linter_with_unpickleable_plugins_is_pickleable(self) -> None: linter.load_plugin_modules(["pylint.extensions.overlapping_exceptions"]) try: dill.dumps(linter) - # TODO: 3.0: Fix this test by raising this assertion again + # TODO: 4.0: Fix this test by raising this assertion again # raise AssertionError( # "Plugins loaded were pickle-safe! This test needs altering" # ) From a60ddd1a44dc00721163f8390d034d2e1385791f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 17 Nov 2023 13:02:19 +0000 Subject: [PATCH 04/10] Fix incorrect suggestion for unnecessary-comprehension (#9172) (#9242) (cherry picked from commit 6f83d5af96400cbe8cc01f3c8f098f81c9d99c70) Co-authored-by: Sayyed Faisal Ali <80758388+C0DE-SLAYER@users.noreply.github.com> --- doc/whatsnew/fragments/9172.false_positive | 12 +++++++++ .../refactoring/refactoring_checker.py | 16 ++++++------ .../unnecessary/unnecessary_comprehension.py | 2 ++ .../unnecessary/unnecessary_comprehension.txt | 26 ++++++++++--------- 4 files changed, 36 insertions(+), 20 deletions(-) create mode 100644 doc/whatsnew/fragments/9172.false_positive diff --git a/doc/whatsnew/fragments/9172.false_positive b/doc/whatsnew/fragments/9172.false_positive new file mode 100644 index 0000000000..43884402cd --- /dev/null +++ b/doc/whatsnew/fragments/9172.false_positive @@ -0,0 +1,12 @@ +Fixed incorrect suggestion for shallow copy in unnecessary-comprehension + +Example of the suggestion: +#pylint: disable=missing-module-docstring +a = [1, 2, 3] +b = [x for x in a] +b[0] = 0 +print(a) # [1, 2, 3] + +After changing b = [x for x in a] to b = a based on the suggestion, the script now prints [0, 2, 3]. The correct suggestion should be use list(a) to preserve the original behavior. + +Closes #9172 diff --git a/pylint/checkers/refactoring/refactoring_checker.py b/pylint/checkers/refactoring/refactoring_checker.py index d29326693a..0d044c7838 100644 --- a/pylint/checkers/refactoring/refactoring_checker.py +++ b/pylint/checkers/refactoring/refactoring_checker.py @@ -1779,15 +1779,15 @@ def _check_unnecessary_comprehension(self, node: nodes.Comprehension) -> None: if isinstance(node.parent, nodes.DictComp) and isinstance( inferred, astroid.objects.DictItems ): - args = (f"{node.iter.func.expr.as_string()}",) - elif ( - isinstance(node.parent, nodes.ListComp) - and isinstance(inferred, nodes.List) - ) or ( - isinstance(node.parent, nodes.SetComp) - and isinstance(inferred, nodes.Set) + args = (f"dict({node.iter.func.expr.as_string()})",) + elif isinstance(node.parent, nodes.ListComp) and isinstance( + inferred, nodes.List ): - args = (f"{node.iter.as_string()}",) + args = (f"list({node.iter.as_string()})",) + elif isinstance(node.parent, nodes.SetComp) and isinstance( + inferred, nodes.Set + ): + args = (f"set({node.iter.as_string()})",) if args: self.add_message( "unnecessary-comprehension", node=node.parent, args=args diff --git a/tests/functional/u/unnecessary/unnecessary_comprehension.py b/tests/functional/u/unnecessary/unnecessary_comprehension.py index 2647898c90..ac26243071 100644 --- a/tests/functional/u/unnecessary/unnecessary_comprehension.py +++ b/tests/functional/u/unnecessary/unnecessary_comprehension.py @@ -4,7 +4,9 @@ # List comprehensions [x for x in iterable] # [unnecessary-comprehension] [y for x in iterable] # expression != target_list +[x for x in iterable] # [unnecessary-comprehension] use list(iterable) [x for x,y,z in iterable] # expression != target_list +[(x, y) for x, y in iterable] # [unnecessary-comprehension] [(x,y,z) for x,y,z in iterable] # [unnecessary-comprehension] [(x,y,z) for (x,y,z) in iterable] # [unnecessary-comprehension] [x for x, *y in iterable] # expression != target_list diff --git a/tests/functional/u/unnecessary/unnecessary_comprehension.txt b/tests/functional/u/unnecessary/unnecessary_comprehension.txt index d316fcdc84..f9b28543de 100644 --- a/tests/functional/u/unnecessary/unnecessary_comprehension.txt +++ b/tests/functional/u/unnecessary/unnecessary_comprehension.txt @@ -1,13 +1,15 @@ unnecessary-comprehension:5:0:5:21::Unnecessary use of a comprehension, use list(iterable) instead.:UNDEFINED -unnecessary-comprehension:8:0:8:31::Unnecessary use of a comprehension, use list(iterable) instead.:UNDEFINED -unnecessary-comprehension:9:0:9:33::Unnecessary use of a comprehension, use list(iterable) instead.:UNDEFINED -unnecessary-comprehension:17:7:17:42::Unnecessary use of a comprehension, use list(a_dict.items()) instead.:UNDEFINED -unnecessary-comprehension:20:0:20:21::Unnecessary use of a comprehension, use set(iterable) instead.:UNDEFINED -unnecessary-comprehension:23:0:23:31::Unnecessary use of a comprehension, use set(iterable) instead.:UNDEFINED -unnecessary-comprehension:24:7:24:42::Unnecessary use of a comprehension, use set(iterable) instead.:UNDEFINED -unnecessary-comprehension:32:0:32:27::Unnecessary use of a comprehension, use dict(iterable) instead.:UNDEFINED -unnecessary-comprehension:34:0:34:29::Unnecessary use of a comprehension, use dict(iterable) instead.:UNDEFINED -unnecessary-comprehension:46:0:46:26::Unnecessary use of a comprehension, use my_list instead.:UNDEFINED -unnecessary-comprehension:47:8:47:42::Unnecessary use of a comprehension, use my_dict instead.:UNDEFINED -consider-using-dict-items:48:0:None:None::Consider iterating with .items():UNDEFINED -unnecessary-comprehension:49:0:49:25::Unnecessary use of a comprehension, use my_set instead.:UNDEFINED +unnecessary-comprehension:7:0:7:21::Unnecessary use of a comprehension, use list(iterable) instead.:UNDEFINED +unnecessary-comprehension:9:0:9:29::Unnecessary use of a comprehension, use list(iterable) instead.:UNDEFINED +unnecessary-comprehension:10:0:10:31::Unnecessary use of a comprehension, use list(iterable) instead.:UNDEFINED +unnecessary-comprehension:11:0:11:33::Unnecessary use of a comprehension, use list(iterable) instead.:UNDEFINED +unnecessary-comprehension:19:7:19:42::Unnecessary use of a comprehension, use list(a_dict.items()) instead.:UNDEFINED +unnecessary-comprehension:22:0:22:21::Unnecessary use of a comprehension, use set(iterable) instead.:UNDEFINED +unnecessary-comprehension:25:0:25:31::Unnecessary use of a comprehension, use set(iterable) instead.:UNDEFINED +unnecessary-comprehension:26:7:26:42::Unnecessary use of a comprehension, use set(iterable) instead.:UNDEFINED +unnecessary-comprehension:34:0:34:27::Unnecessary use of a comprehension, use dict(iterable) instead.:UNDEFINED +unnecessary-comprehension:36:0:36:29::Unnecessary use of a comprehension, use dict(iterable) instead.:UNDEFINED +unnecessary-comprehension:48:0:48:26::Unnecessary use of a comprehension, use list(my_list) instead.:UNDEFINED +unnecessary-comprehension:49:8:49:42::Unnecessary use of a comprehension, use dict(my_dict) instead.:UNDEFINED +consider-using-dict-items:50:0:None:None::Consider iterating with .items():UNDEFINED +unnecessary-comprehension:51:0:51:25::Unnecessary use of a comprehension, use set(my_set) instead.:UNDEFINED From 7f01d837385c4505eda1777f7b9c2693c9b305b1 Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Sun, 26 Nov 2023 11:31:16 +0100 Subject: [PATCH 05/10] Fix doc generation in implicit-str-concat New sphinx now check that a block of code is valid. --- doc/data/messages/i/implicit-str-concat/details.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/data/messages/i/implicit-str-concat/details.rst b/doc/data/messages/i/implicit-str-concat/details.rst index 07b9fc172c..6b3f2c32f5 100644 --- a/doc/data/messages/i/implicit-str-concat/details.rst +++ b/doc/data/messages/i/implicit-str-concat/details.rst @@ -11,7 +11,7 @@ In order to detect this case, you must enable `check-str-concat-over-line-jumps` .. code-block:: toml [STRING_CONSTANT] - check-str-concat-over-line-jumps = yes + check-str-concat-over-line-jumps = true However, the drawback of this setting is that it will trigger false positive for string parameters passed on multiple lines in function calls: From 81f0f2ed02c2e79f6a44c324d47574cf271f9590 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 7 Dec 2023 13:21:08 +0000 Subject: [PATCH 06/10] [Backport maintenance/3.0.x] [bugfix] Find files with ./ as input with a __init__.py file (#9286) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [bugfix] Find files with ./ as input with a __init__.py file (#9211) Co-authored-by: Pierre Sassoulas Co-authored-by: Daniël van Noord <13665637+DanielNoord@users.noreply.github.com> (cherry picked from commit abdb8742201dc31fbea7e3f632a6d7b10b4067b9) --- doc/whatsnew/fragments/9210.bugfix | 4 ++ pylint/lint/expand_modules.py | 3 +- tests/lint/unittest_expand_modules.py | 70 ++++++++++++++++++++++++++- 3 files changed, 75 insertions(+), 2 deletions(-) create mode 100644 doc/whatsnew/fragments/9210.bugfix diff --git a/doc/whatsnew/fragments/9210.bugfix b/doc/whatsnew/fragments/9210.bugfix new file mode 100644 index 0000000000..c1d0835712 --- /dev/null +++ b/doc/whatsnew/fragments/9210.bugfix @@ -0,0 +1,4 @@ +Fix a bug where pylint was unable to walk recursively through a directory if the +directory has an `__init__.py` file. + +Closes #9210 diff --git a/pylint/lint/expand_modules.py b/pylint/lint/expand_modules.py index 1e8fd032f8..d42c798c9d 100644 --- a/pylint/lint/expand_modules.py +++ b/pylint/lint/expand_modules.py @@ -144,8 +144,9 @@ def expand_modules( ) if has_init or is_namespace or is_directory: for subfilepath in modutils.get_module_files( - os.path.dirname(filepath), ignore_list, list_all=is_namespace + os.path.dirname(filepath) or ".", ignore_list, list_all=is_namespace ): + subfilepath = os.path.normpath(subfilepath) if filepath == subfilepath: continue if _is_in_ignore_list_re( diff --git a/tests/lint/unittest_expand_modules.py b/tests/lint/unittest_expand_modules.py index 7120a17480..34133d759b 100644 --- a/tests/lint/unittest_expand_modules.py +++ b/tests/lint/unittest_expand_modules.py @@ -4,7 +4,11 @@ from __future__ import annotations +import copy +import os import re +from collections.abc import Iterator +from contextlib import contextmanager from pathlib import Path import pytest @@ -28,7 +32,8 @@ def test__is_in_ignore_list_re_match() -> None: TEST_DIRECTORY = Path(__file__).parent.parent INIT_PATH = str(TEST_DIRECTORY / "lint/__init__.py") -EXPAND_MODULES = str(TEST_DIRECTORY / "lint/unittest_expand_modules.py") +EXPAND_MODULES_BASE = "unittest_expand_modules.py" +EXPAND_MODULES = str(TEST_DIRECTORY / "lint" / EXPAND_MODULES_BASE) this_file = { "basename": "lint.unittest_expand_modules", "basepath": EXPAND_MODULES, @@ -37,6 +42,14 @@ def test__is_in_ignore_list_re_match() -> None: "path": EXPAND_MODULES, } +this_file_relative_to_parent = { + "basename": "lint.unittest_expand_modules", + "basepath": EXPAND_MODULES_BASE, + "isarg": True, + "name": "lint.unittest_expand_modules", + "path": EXPAND_MODULES_BASE, +} + this_file_from_init = { "basename": "lint", "basepath": INIT_PATH, @@ -117,6 +130,27 @@ def _list_expected_package_modules( ) +def _list_expected_package_modules_relative() -> tuple[dict[str, object], ...]: + """Generates reusable list of modules for our package with relative path input.""" + abs_result = copy.deepcopy(_list_expected_package_modules()) + for item in abs_result: + assert isinstance(item["basepath"], str) + assert isinstance(item["path"], str) + item["basepath"] = os.path.relpath(item["basepath"], str(Path(__file__).parent)) + item["path"] = os.path.relpath(item["path"], str(Path(__file__).parent)) + return abs_result + + +@contextmanager +def pushd(path: Path) -> Iterator[None]: + prev = os.getcwd() + os.chdir(path) + try: + yield + finally: + os.chdir(prev) + + class TestExpandModules(CheckerTestCase): """Test the expand_modules function while allowing options to be set.""" @@ -159,6 +193,40 @@ def test_expand_modules( assert modules == expected assert not errors + @pytest.mark.parametrize( + "files_or_modules,expected", + [ + ( + [Path(__file__).name], + {this_file_relative_to_parent["path"]: this_file_relative_to_parent}, + ), + ( + ["./"], + { + module["path"]: module # pylint: disable=unsubscriptable-object + for module in _list_expected_package_modules_relative() + }, + ), + ], + ) + @set_config(ignore_paths="") + def test_expand_modules_relative_path( + self, files_or_modules: list[str], expected: dict[str, ModuleDescriptionDict] + ) -> None: + """Test expand_modules with the default value of ignore-paths and relative path as input.""" + ignore_list: list[str] = [] + ignore_list_re: list[re.Pattern[str]] = [] + with pushd(Path(__file__).parent): + modules, errors = expand_modules( + files_or_modules, + [], + ignore_list, + ignore_list_re, + self.linter.config.ignore_paths, + ) + assert modules == expected + assert not errors + @pytest.mark.parametrize( "files_or_modules,expected", [ From d0d5c91e26c80933bfc13b36a0428d3a0c593cc0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 8 Dec 2023 08:22:15 -0500 Subject: [PATCH 07/10] [pointless-string-statement] Ignore docstrings on py3.12 type aliases (#9269) (#9287) (cherry picked from commit 796eae3c46b142d479071fdf39f2e47f627da29e) Co-authored-by: Jacob Walls --- doc/data/messages/p/pointless-string-statement/related.rst | 1 + doc/whatsnew/fragments/9268.false_positive | 4 ++++ pylint/checkers/base/basic_checker.py | 4 +++- tests/functional/s/statement_without_effect_py312.py | 7 +++++++ tests/functional/s/statement_without_effect_py312.rc | 2 ++ 5 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 doc/data/messages/p/pointless-string-statement/related.rst create mode 100644 doc/whatsnew/fragments/9268.false_positive create mode 100644 tests/functional/s/statement_without_effect_py312.py create mode 100644 tests/functional/s/statement_without_effect_py312.rc diff --git a/doc/data/messages/p/pointless-string-statement/related.rst b/doc/data/messages/p/pointless-string-statement/related.rst new file mode 100644 index 0000000000..b03ae2cd9b --- /dev/null +++ b/doc/data/messages/p/pointless-string-statement/related.rst @@ -0,0 +1 @@ +- `Discussion thread re: docstrings on assignments `_ diff --git a/doc/whatsnew/fragments/9268.false_positive b/doc/whatsnew/fragments/9268.false_positive new file mode 100644 index 0000000000..f360ea204e --- /dev/null +++ b/doc/whatsnew/fragments/9268.false_positive @@ -0,0 +1,4 @@ +Fixed ``pointless-string-statement`` false positive for docstrings +on Python 3.12 type aliases. + +Closes #9268 diff --git a/pylint/checkers/base/basic_checker.py b/pylint/checkers/base/basic_checker.py index 8aaa274d65..32e0da016c 100644 --- a/pylint/checkers/base/basic_checker.py +++ b/pylint/checkers/base/basic_checker.py @@ -446,7 +446,9 @@ def visit_expr(self, node: nodes.Expr) -> None: if ( sibling is not None and sibling.scope() is scope - and isinstance(sibling, (nodes.Assign, nodes.AnnAssign)) + and isinstance( + sibling, (nodes.Assign, nodes.AnnAssign, nodes.TypeAlias) + ) ): return self.add_message("pointless-string-statement", node=node) diff --git a/tests/functional/s/statement_without_effect_py312.py b/tests/functional/s/statement_without_effect_py312.py new file mode 100644 index 0000000000..5ea5d17e7b --- /dev/null +++ b/tests/functional/s/statement_without_effect_py312.py @@ -0,0 +1,7 @@ +"""Move this into statement_without_effect.py when python 3.12 is minimum.""" + +type MyTuple = tuple[str, str] +""" +Multiline docstring +for Python3.12 type alias (PEP 695) +""" diff --git a/tests/functional/s/statement_without_effect_py312.rc b/tests/functional/s/statement_without_effect_py312.rc new file mode 100644 index 0000000000..9c966d4bda --- /dev/null +++ b/tests/functional/s/statement_without_effect_py312.rc @@ -0,0 +1,2 @@ +[testoptions] +min_pyver=3.12 From fea5483f2861df71ff5f60d3f6300c2fc93ef21a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 10 Dec 2023 21:28:27 +0000 Subject: [PATCH 08/10] [wrong-exception-operation] Fix FP for tuple concatenation of exception types (#9289) (#9291) (cherry picked from commit 8d4c6c1f300414abad0d4f5cd81e592e8743b6f0) Co-authored-by: Jacob Walls --- doc/whatsnew/fragments/9288.false_positive | 4 ++++ pylint/checkers/exceptions.py | 13 +++++++++++- .../functional/w/wrong_exception_operation.py | 21 +++++++++++++++++++ .../w/wrong_exception_operation.txt | 1 + 4 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 doc/whatsnew/fragments/9288.false_positive diff --git a/doc/whatsnew/fragments/9288.false_positive b/doc/whatsnew/fragments/9288.false_positive new file mode 100644 index 0000000000..470c308c69 --- /dev/null +++ b/doc/whatsnew/fragments/9288.false_positive @@ -0,0 +1,4 @@ +Fix false positive for ``invalid-exception-operation`` when concatenating tuples +of exception types. + +Closes #9288 diff --git a/pylint/checkers/exceptions.py b/pylint/checkers/exceptions.py index ce3f9367f0..2a193a6ce8 100644 --- a/pylint/checkers/exceptions.py +++ b/pylint/checkers/exceptions.py @@ -533,8 +533,19 @@ def gather_exceptions_from_handler( @utils.only_required_for_messages("wrong-exception-operation") def visit_binop(self, node: nodes.BinOp) -> None: if isinstance(node.parent, nodes.ExceptHandler): + both_sides_tuple_or_uninferable = isinstance( + utils.safe_infer(node.left), (nodes.Tuple, util.UninferableBase) + ) and isinstance( + utils.safe_infer(node.right), (nodes.Tuple, util.UninferableBase) + ) + # Tuple concatenation allowed + if both_sides_tuple_or_uninferable: + if node.op == "+": + return + suggestion = f"Did you mean '({node.left.as_string()} + {node.right.as_string()})' instead?" # except (V | A) - suggestion = f"Did you mean '({node.left.as_string()}, {node.right.as_string()})' instead?" + else: + suggestion = f"Did you mean '({node.left.as_string()}, {node.right.as_string()})' instead?" self.add_message("wrong-exception-operation", node=node, args=(suggestion,)) @utils.only_required_for_messages("wrong-exception-operation") diff --git a/tests/functional/w/wrong_exception_operation.py b/tests/functional/w/wrong_exception_operation.py index 8078573c41..141251fed9 100644 --- a/tests/functional/w/wrong_exception_operation.py +++ b/tests/functional/w/wrong_exception_operation.py @@ -16,3 +16,24 @@ 1/0 except (ValueError < TypeError): # [wrong-exception-operation] pass + + +# Concatenation of exception type tuples +DIVISION_BY_ZERO = (ZeroDivisionError,) +VALUE_ERROR = (ValueError,) +UNINFERABLE = DIVISION_BY_ZERO | VALUE_ERROR + +try: + 1/0 +except (ValueError, ) + DIVISION_BY_ZERO: + pass + +try: + 1/0 +except (ValueError, ) | DIVISION_BY_ZERO: # [wrong-exception-operation] + pass + +try: + 1/0 +except (ValueError, ) + UNINFERABLE: + pass diff --git a/tests/functional/w/wrong_exception_operation.txt b/tests/functional/w/wrong_exception_operation.txt index dc3c213462..d7eae8134f 100644 --- a/tests/functional/w/wrong_exception_operation.txt +++ b/tests/functional/w/wrong_exception_operation.txt @@ -2,3 +2,4 @@ catching-non-exception:6:8:6:30::"Catching an exception which doesn't inherit fr wrong-exception-operation:6:8:6:30::Invalid exception operation. Did you mean '(ValueError, TypeError)' instead?:UNDEFINED wrong-exception-operation:11:8:11:30::Invalid exception operation. Did you mean '(ValueError, TypeError)' instead?:UNDEFINED wrong-exception-operation:17:8:17:30::Invalid exception operation. Did you mean '(ValueError, TypeError)' instead?:UNDEFINED +wrong-exception-operation:33:7:33:40::Invalid exception operation. Did you mean '((ValueError, ) + DIVISION_BY_ZERO)' instead?:UNDEFINED From 54687e75b6fb4147e58f1c3204eea6cd915a472b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 10 Dec 2023 23:22:46 +0100 Subject: [PATCH 09/10] Disallow isort 5.13.0 (#9290) (#9292) Refs #9270 (cherry picked from commit 47410adc1b9ffdddc15726803d63518a12a1e998) Co-authored-by: Jacob Walls --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 322e6d1b3d..121a465559 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,7 +42,7 @@ dependencies = [ # Pinned to dev of second minor update to allow editable installs and fix primer issues, # see https://github.com/pylint-dev/astroid/issues/1341 "astroid>=3.0.1,<=3.1.0-dev0", - "isort>=4.2.5,<6", + "isort>=4.2.5,<6,!=5.13.0", "mccabe>=0.6,<0.8", "tomli>=1.1.0;python_version<'3.11'", "tomlkit>=0.10.1", From 1a5ffc1f447b77071ffe18a9c6836c09147ee2ed Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Sun, 10 Dec 2023 20:44:51 -0500 Subject: [PATCH 10/10] Bump pylint to 3.0.3, update changelog --- doc/whatsnew/3/3.0/index.rst | 52 ++++++++++++++++++++++ doc/whatsnew/fragments/9148.false_positive | 3 -- doc/whatsnew/fragments/9172.false_positive | 12 ----- doc/whatsnew/fragments/9193.false_positive | 4 -- doc/whatsnew/fragments/9210.bugfix | 4 -- doc/whatsnew/fragments/9268.false_positive | 4 -- doc/whatsnew/fragments/9288.false_positive | 4 -- pylint/__pkginfo__.py | 2 +- tbump.toml | 2 +- towncrier.toml | 2 +- 10 files changed, 55 insertions(+), 34 deletions(-) delete mode 100644 doc/whatsnew/fragments/9148.false_positive delete mode 100644 doc/whatsnew/fragments/9172.false_positive delete mode 100644 doc/whatsnew/fragments/9193.false_positive delete mode 100644 doc/whatsnew/fragments/9210.bugfix delete mode 100644 doc/whatsnew/fragments/9268.false_positive delete mode 100644 doc/whatsnew/fragments/9288.false_positive diff --git a/doc/whatsnew/3/3.0/index.rst b/doc/whatsnew/3/3.0/index.rst index 3730d4857b..bb7c091107 100644 --- a/doc/whatsnew/3/3.0/index.rst +++ b/doc/whatsnew/3/3.0/index.rst @@ -65,6 +65,58 @@ easier to parse and provides more info, here's a sample output. .. towncrier release notes start +What's new in Pylint 3.0.3? +--------------------------- +Release date: 2023-12-11 + + +False Positives Fixed +--------------------- + +- Fixed false positive for ``unnecessary-lambda`` when the call has keyword arguments but not the lambda. + + Closes #9148 (`#9148 `_) + +- Fixed incorrect suggestion for shallow copy in unnecessary-comprehension + + Example of the suggestion: + #pylint: disable=missing-module-docstring + a = [1, 2, 3] + b = [x for x in a] + b[0] = 0 + print(a) # [1, 2, 3] + + After changing b = [x for x in a] to b = a based on the suggestion, the script now prints [0, 2, 3]. The correct suggestion should be use list(a) to preserve the original behavior. + + Closes #9172 (`#9172 `_) + +- Fix false positives for ``undefined-variable`` and ``unused-argument`` for + classes and functions using Python 3.12 generic type syntax. + + Closes #9193 (`#9193 `_) + +- Fixed ``pointless-string-statement`` false positive for docstrings + on Python 3.12 type aliases. + + Closes #9268 (`#9268 `_) + +- Fix false positive for ``invalid-exception-operation`` when concatenating tuples + of exception types. + + Closes #9288 (`#9288 `_) + + + +Other Bug Fixes +--------------- + +- Fix a bug where pylint was unable to walk recursively through a directory if the + directory has an `__init__.py` file. + + Closes #9210 (`#9210 `_) + + + What's new in Pylint 3.0.2? --------------------------- Release date: 2023-10-22 diff --git a/doc/whatsnew/fragments/9148.false_positive b/doc/whatsnew/fragments/9148.false_positive deleted file mode 100644 index 647deb103e..0000000000 --- a/doc/whatsnew/fragments/9148.false_positive +++ /dev/null @@ -1,3 +0,0 @@ -Fixed false positive for ``unnecessary-lambda`` when the call has keyword arguments but not the lambda. - -Closes #9148 diff --git a/doc/whatsnew/fragments/9172.false_positive b/doc/whatsnew/fragments/9172.false_positive deleted file mode 100644 index 43884402cd..0000000000 --- a/doc/whatsnew/fragments/9172.false_positive +++ /dev/null @@ -1,12 +0,0 @@ -Fixed incorrect suggestion for shallow copy in unnecessary-comprehension - -Example of the suggestion: -#pylint: disable=missing-module-docstring -a = [1, 2, 3] -b = [x for x in a] -b[0] = 0 -print(a) # [1, 2, 3] - -After changing b = [x for x in a] to b = a based on the suggestion, the script now prints [0, 2, 3]. The correct suggestion should be use list(a) to preserve the original behavior. - -Closes #9172 diff --git a/doc/whatsnew/fragments/9193.false_positive b/doc/whatsnew/fragments/9193.false_positive deleted file mode 100644 index 39dc70b81d..0000000000 --- a/doc/whatsnew/fragments/9193.false_positive +++ /dev/null @@ -1,4 +0,0 @@ -Fix false positives for ``undefined-variable`` and ``unused-argument`` for -classes and functions using Python 3.12 generic type syntax. - -Closes #9193 diff --git a/doc/whatsnew/fragments/9210.bugfix b/doc/whatsnew/fragments/9210.bugfix deleted file mode 100644 index c1d0835712..0000000000 --- a/doc/whatsnew/fragments/9210.bugfix +++ /dev/null @@ -1,4 +0,0 @@ -Fix a bug where pylint was unable to walk recursively through a directory if the -directory has an `__init__.py` file. - -Closes #9210 diff --git a/doc/whatsnew/fragments/9268.false_positive b/doc/whatsnew/fragments/9268.false_positive deleted file mode 100644 index f360ea204e..0000000000 --- a/doc/whatsnew/fragments/9268.false_positive +++ /dev/null @@ -1,4 +0,0 @@ -Fixed ``pointless-string-statement`` false positive for docstrings -on Python 3.12 type aliases. - -Closes #9268 diff --git a/doc/whatsnew/fragments/9288.false_positive b/doc/whatsnew/fragments/9288.false_positive deleted file mode 100644 index 470c308c69..0000000000 --- a/doc/whatsnew/fragments/9288.false_positive +++ /dev/null @@ -1,4 +0,0 @@ -Fix false positive for ``invalid-exception-operation`` when concatenating tuples -of exception types. - -Closes #9288 diff --git a/pylint/__pkginfo__.py b/pylint/__pkginfo__.py index 24c1c50518..521ffd04cd 100644 --- a/pylint/__pkginfo__.py +++ b/pylint/__pkginfo__.py @@ -9,7 +9,7 @@ from __future__ import annotations -__version__ = "3.0.2" +__version__ = "3.0.3" def get_numversion_from_version(v: str) -> tuple[int, int, int]: diff --git a/tbump.toml b/tbump.toml index 9f6f8c881d..b7db1431f4 100644 --- a/tbump.toml +++ b/tbump.toml @@ -1,7 +1,7 @@ github_url = "https://github.com/pylint-dev/pylint" [version] -current = "3.0.2" +current = "3.0.3" regex = ''' ^(?P0|[1-9]\d*) \. diff --git a/towncrier.toml b/towncrier.toml index dec869630d..46ceaf08c2 100644 --- a/towncrier.toml +++ b/towncrier.toml @@ -1,5 +1,5 @@ [tool.towncrier] -version = "3.0.2" +version = "3.0.3" directory = "doc/whatsnew/fragments" filename = "doc/whatsnew/3/3.0/index.rst" template = "doc/whatsnew/fragments/_template.rst"