From 9ed37207e9c26e92669d95c9cd2936f628d84a81 Mon Sep 17 00:00:00 2001 From: Mark Byrne <31762852+mbyrnepr2@users.noreply.github.com> Date: Sat, 7 May 2022 21:23:26 +0200 Subject: [PATCH 1/8] Add an exception for `IndexError` inside `uninferable_final_decorator` (#6532) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Pierre Sassoulas Co-authored-by: Daniël van Noord <13665637+DanielNoord@users.noreply.github.com> --- ChangeLog | 3 ++ doc/whatsnew/2.13.rst | 4 +++ pylint/checkers/utils.py | 30 +++++++++++-------- .../regression_6531_crash_index_error.py | 30 +++++++++++++++++++ 4 files changed, 55 insertions(+), 12 deletions(-) create mode 100644 tests/functional/r/regression/regression_6531_crash_index_error.py diff --git a/ChangeLog b/ChangeLog index 7d8d60e857..07bf60acb0 100644 --- a/ChangeLog +++ b/ChangeLog @@ -20,6 +20,9 @@ What's New in Pylint 2.13.9? ============================ Release date: TBA +* Fix ``IndexError`` crash in ``uninferable_final_decorators`` method. + + Relates to #6531 What's New in Pylint 2.13.8? diff --git a/doc/whatsnew/2.13.rst b/doc/whatsnew/2.13.rst index 56914559fe..e851af65df 100644 --- a/doc/whatsnew/2.13.rst +++ b/doc/whatsnew/2.13.rst @@ -639,3 +639,7 @@ Other Changes ``open`` Closes #6414 + +* Fix ``IndexError`` crash in ``uninferable_final_decorators`` method. + + Relates to #6531 diff --git a/pylint/checkers/utils.py b/pylint/checkers/utils.py index ec5f2cddc5..9cba6e0579 100644 --- a/pylint/checkers/utils.py +++ b/pylint/checkers/utils.py @@ -820,28 +820,34 @@ def uninferable_final_decorators( """ decorators = [] for decorator in getattr(node, "nodes", []): + import_nodes: tuple[nodes.Import | nodes.ImportFrom] | None = None + + # Get the `Import` node. The decorator is of the form: @module.name if isinstance(decorator, nodes.Attribute): - try: - import_node = decorator.expr.lookup(decorator.expr.name)[1][0] - except AttributeError: - continue + inferred = safe_infer(decorator.expr) + if isinstance(inferred, nodes.Module) and inferred.qname() == "typing": + _, import_nodes = decorator.expr.lookup(decorator.expr.name) + + # Get the `ImportFrom` node. The decorator is of the form: @name elif isinstance(decorator, nodes.Name): - lookup_values = decorator.lookup(decorator.name) - if lookup_values[1]: - import_node = lookup_values[1][0] - else: - continue # pragma: no cover # Covered on Python < 3.8 - else: + _, import_nodes = decorator.lookup(decorator.name) + + # The `final` decorator is expected to be found in the + # import_nodes. Continue if we don't find any `Import` or `ImportFrom` + # nodes for this decorator. + if not import_nodes: continue + import_node = import_nodes[0] if not isinstance(import_node, (astroid.Import, astroid.ImportFrom)): continue import_names = dict(import_node.names) - # from typing import final + # Check if the import is of the form: `from typing import final` is_from_import = ("final" in import_names) and import_node.modname == "typing" - # import typing + + # Check if the import is of the form: `import typing` is_import = ("typing" in import_names) and getattr( decorator, "attrname", None ) == "final" diff --git a/tests/functional/r/regression/regression_6531_crash_index_error.py b/tests/functional/r/regression/regression_6531_crash_index_error.py new file mode 100644 index 0000000000..6cdc96617b --- /dev/null +++ b/tests/functional/r/regression/regression_6531_crash_index_error.py @@ -0,0 +1,30 @@ +"""Regression test for https://github.com/PyCQA/pylint/issues/6531.""" + +# pylint: disable=missing-docstring, redefined-outer-name + +import pytest + + +class Wallet: + def __init__(self): + self.balance = 0 + + def add_cash(self, earned): + self.balance += earned + + def spend_cash(self, spent): + self.balance -= spent + +@pytest.fixture +def my_wallet(): + '''Returns a Wallet instance with a zero balance''' + return Wallet() + +@pytest.mark.parametrize("earned,spent,expected", [ + (30, 10, 20), + (20, 2, 18), +]) +def test_transactions(my_wallet, earned, spent, expected): + my_wallet.add_cash(earned) + my_wallet.spend_cash(spent) + assert my_wallet.balance == expected From cc57f483bf7b07f4a0698b4bb4a285976491072d Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Mon, 9 May 2022 13:46:26 +0200 Subject: [PATCH 2/8] Upgrade astroid version to 2.11.5 --- requirements_test_min.txt | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements_test_min.txt b/requirements_test_min.txt index 91eb8d4295..1c1bd3e0ce 100644 --- a/requirements_test_min.txt +++ b/requirements_test_min.txt @@ -1,6 +1,6 @@ -e .[testutil] # astroid dependency is also defined in setup.cfg -astroid==2.11.3 # Pinned to a specific version for tests +astroid==2.11.5 # Pinned to a specific version for tests typing-extensions~=4.1 pytest~=7.0 pytest-benchmark~=3.4 diff --git a/setup.cfg b/setup.cfg index 7b0eac7e7e..af5e831d27 100644 --- a/setup.cfg +++ b/setup.cfg @@ -48,7 +48,7 @@ install_requires = # Also upgrade requirements_test_min.txt if you are bumping astroid. # Pinned to dev of next minor update to allow editable installs, # see https://github.com/PyCQA/astroid/issues/1341 - astroid>=2.11.3,<=2.12.0-dev0 + astroid>=2.11.5,<=2.12.0-dev0 isort>=4.2.5,<6 mccabe>=0.6,<0.8 tomli>=1.1.0;python_version<"3.11" From 15634b83dc97da674dd037b922c274d243a47b20 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Sun, 8 May 2022 21:38:08 -0400 Subject: [PATCH 3/8] Add regression test for #6539 --- ChangeLog | 4 ++++ doc/whatsnew/2.13.rst | 4 ++++ .../i/invalid/invalid_name/invalid_name_module_level.py | 3 +++ 3 files changed, 11 insertions(+) diff --git a/ChangeLog b/ChangeLog index 07bf60acb0..6fefa3c5c1 100644 --- a/ChangeLog +++ b/ChangeLog @@ -24,6 +24,10 @@ Release date: TBA Relates to #6531 +* Fix a crash when accessing ``__code__`` and assigning it to a variable. + + Closes #6539 + What's New in Pylint 2.13.8? ============================ diff --git a/doc/whatsnew/2.13.rst b/doc/whatsnew/2.13.rst index e851af65df..08c9db2ee8 100644 --- a/doc/whatsnew/2.13.rst +++ b/doc/whatsnew/2.13.rst @@ -635,6 +635,10 @@ Other Changes Closes #3979 +* Fix a crash when accessing ``__code__`` and assigning it to a variable. + + Closes #6539 + * Fix a crash when linting a file that passes an integer ``mode=`` to ``open`` diff --git a/tests/functional/i/invalid/invalid_name/invalid_name_module_level.py b/tests/functional/i/invalid/invalid_name/invalid_name_module_level.py index 355969f39a..ec2b2769be 100644 --- a/tests/functional/i/invalid/invalid_name/invalid_name_module_level.py +++ b/tests/functional/i/invalid/invalid_name/invalid_name_module_level.py @@ -21,3 +21,6 @@ def A(): # [invalid-name] CONSTD = A() CONST = "12 34 ".rstrip().split() + + +assignment_that_crashed_pylint = type(float.__new__.__code__) From 870ed76900cbca16b9331dd02fda679ea40294ee Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Mon, 9 May 2022 14:44:05 -0400 Subject: [PATCH 4/8] Add regression test for #6497 (#6498) --- ChangeLog | 5 +++++ doc/whatsnew/2.13.rst | 6 ++++++ tests/functional/n/no/no_name_in_module.py | 6 ++++++ 3 files changed, 17 insertions(+) diff --git a/ChangeLog b/ChangeLog index 6fefa3c5c1..f48f42c2e8 100644 --- a/ChangeLog +++ b/ChangeLog @@ -20,6 +20,11 @@ What's New in Pylint 2.13.9? ============================ Release date: TBA + +* Fix false positives for ``no-name-in-module`` and ``import-error`` for ``numpy.distutils`` and ``pydantic``. + + Closes #6497 + * Fix ``IndexError`` crash in ``uninferable_final_decorators`` method. Relates to #6531 diff --git a/doc/whatsnew/2.13.rst b/doc/whatsnew/2.13.rst index 08c9db2ee8..5c1da5a0f5 100644 --- a/doc/whatsnew/2.13.rst +++ b/doc/whatsnew/2.13.rst @@ -644,6 +644,12 @@ Other Changes Closes #6414 + +* Fix false positives for ``no-name-in-module`` and ``import-error`` for ``numpy.distutils`` + and ``pydantic``. + + Closes #6497 + * Fix ``IndexError`` crash in ``uninferable_final_decorators`` method. Relates to #6531 diff --git a/tests/functional/n/no/no_name_in_module.py b/tests/functional/n/no/no_name_in_module.py index 34e8af569f..2964d78497 100644 --- a/tests/functional/n/no/no_name_in_module.py +++ b/tests/functional/n/no/no_name_in_module.py @@ -78,3 +78,9 @@ # Check ignored-modules setting from argparse import THIS_does_not_EXIST + + +# This captures the original failure in https://github.com/PyCQA/pylint/issues/6497 +# only if numpy is installed. We are not installing numpy on CI (for now) +from numpy.distutils.misc_util import is_sequence +from pydantic import BaseModel From 43c2122289df5524e3cb133f5d70ff79e590ecea Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Wed, 11 May 2022 10:20:17 -0400 Subject: [PATCH 5/8] Fix a crash in `unnecessary-dict-index-lookup` when subscripting an attribute (#6579) --- ChangeLog | 4 ++++ doc/whatsnew/2.13.rst | 4 ++++ pylint/checkers/refactoring/refactoring_checker.py | 1 + .../u/unnecessary/unnecessary_dict_index_lookup.py | 7 +++++++ 4 files changed, 16 insertions(+) diff --git a/ChangeLog b/ChangeLog index f48f42c2e8..ccce125a1b 100644 --- a/ChangeLog +++ b/ChangeLog @@ -29,6 +29,10 @@ Release date: TBA Relates to #6531 +* Fix a crash in ``unnecessary-dict-index-lookup`` when subscripting an attribute. + + Closes #6557 + * Fix a crash when accessing ``__code__`` and assigning it to a variable. Closes #6539 diff --git a/doc/whatsnew/2.13.rst b/doc/whatsnew/2.13.rst index 5c1da5a0f5..1ae1ca7f9a 100644 --- a/doc/whatsnew/2.13.rst +++ b/doc/whatsnew/2.13.rst @@ -653,3 +653,7 @@ Other Changes * Fix ``IndexError`` crash in ``uninferable_final_decorators`` method. Relates to #6531 + +* Fix a crash in ``unnecessary-dict-index-lookup`` when subscripting an attribute. + + Closes #6557 diff --git a/pylint/checkers/refactoring/refactoring_checker.py b/pylint/checkers/refactoring/refactoring_checker.py index 8dc65f129d..730f1ae578 100644 --- a/pylint/checkers/refactoring/refactoring_checker.py +++ b/pylint/checkers/refactoring/refactoring_checker.py @@ -1938,6 +1938,7 @@ def _check_unnecessary_dict_index_lookup( elif isinstance(value, nodes.Subscript): if ( not isinstance(node.target, nodes.AssignName) + or not isinstance(value.value, nodes.Name) or node.target.name != value.value.name or iterating_object_name != subscript.value.as_string() ): diff --git a/tests/functional/u/unnecessary/unnecessary_dict_index_lookup.py b/tests/functional/u/unnecessary/unnecessary_dict_index_lookup.py index f594d9e0f8..7ae5488ce7 100644 --- a/tests/functional/u/unnecessary/unnecessary_dict_index_lookup.py +++ b/tests/functional/u/unnecessary/unnecessary_dict_index_lookup.py @@ -112,3 +112,10 @@ class Foo: d = {} for key, in d.items(): print(d[key]) + +# Test subscripting an attribute +# https://github.com/PyCQA/pylint/issues/6557 +f = Foo() +for input_output in d.items(): + f.input_output = input_output # pylint: disable=attribute-defined-outside-init + print(d[f.input_output[0]]) From 42c0ad5c07e4b2c0ed188b487b5b91f618ee82c3 Mon Sep 17 00:00:00 2001 From: Matus Valo Date: Fri, 13 May 2022 08:06:04 +0200 Subject: [PATCH 6/8] Respect ignore configuration options when --recursive=y. (#6528) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Ignore specified files/directories in recursive mode Co-authored-by: Pierre Sassoulas Co-authored-by: Daniël van Noord <13665637+DanielNoord@users.noreply.github.com> --- ChangeLog | 4 + pylint/lint/expand_modules.py | 20 ++++- pylint/lint/pylinter.py | 17 ++++- tests/lint/unittest_lint.py | 73 +++++++++++++++++- .../directory/ignored_subdirectory/failing.py | 1 + tests/test_self.py | 76 ++++++++++++++++++- 6 files changed, 181 insertions(+), 10 deletions(-) create mode 100644 tests/regrtest_data/directory/ignored_subdirectory/failing.py diff --git a/ChangeLog b/ChangeLog index ccce125a1b..a49bb9e300 100644 --- a/ChangeLog +++ b/ChangeLog @@ -21,6 +21,10 @@ What's New in Pylint 2.13.9? Release date: TBA +* Respect ignore configuration options with ``--recursive=y``. + + Closes #6471 + * Fix false positives for ``no-name-in-module`` and ``import-error`` for ``numpy.distutils`` and ``pydantic``. Closes #6497 diff --git a/pylint/lint/expand_modules.py b/pylint/lint/expand_modules.py index 184316e9bd..07b92895a3 100644 --- a/pylint/lint/expand_modules.py +++ b/pylint/lint/expand_modules.py @@ -43,6 +43,20 @@ def _is_in_ignore_list_re(element: str, ignore_list_re: List[Pattern]) -> bool: return any(file_pattern.match(element) for file_pattern in ignore_list_re) +def _is_ignored_file( + element: str, + ignore_list: list[str], + ignore_list_re: list[Pattern[str]], + ignore_list_paths_re: list[Pattern[str]], +) -> bool: + basename = os.path.basename(element) + return ( + basename in ignore_list + or _is_in_ignore_list_re(basename, ignore_list_re) + or _is_in_ignore_list_re(element, ignore_list_paths_re) + ) + + def expand_modules( files_or_modules: List[str], ignore_list: List[str], @@ -58,10 +72,8 @@ def expand_modules( for something in files_or_modules: basename = os.path.basename(something) - if ( - basename in ignore_list - or _is_in_ignore_list_re(os.path.basename(something), ignore_list_re) - or _is_in_ignore_list_re(something, ignore_list_paths_re) + if _is_ignored_file( + something, ignore_list, ignore_list_re, ignore_list_paths_re ): continue module_path = get_python_path(something) diff --git a/pylint/lint/pylinter.py b/pylint/lint/pylinter.py index 0bf8961c1e..878c1190eb 100644 --- a/pylint/lint/pylinter.py +++ b/pylint/lint/pylinter.py @@ -40,7 +40,7 @@ MSG_TYPES_LONG, MSG_TYPES_STATUS, ) -from pylint.lint.expand_modules import expand_modules +from pylint.lint.expand_modules import _is_ignored_file, expand_modules from pylint.lint.parallel import check_parallel from pylint.lint.report_functions import ( report_messages_by_module_stats, @@ -1013,9 +1013,8 @@ def initialize(self): if not msg.may_be_emitted(): self._msgs_state[msg.msgid] = False - @staticmethod - def _discover_files(files_or_modules: Sequence[str]) -> Iterator[str]: - """Discover python modules and packages in subdirectory. + def _discover_files(self, files_or_modules: Sequence[str]) -> Iterator[str]: + """Discover python modules and packages in sub-directory. Returns iterator of paths to discovered modules and packages. """ @@ -1028,6 +1027,16 @@ def _discover_files(files_or_modules: Sequence[str]) -> Iterator[str]: if any(root.startswith(s) for s in skip_subtrees): # Skip subtree of already discovered package. continue + + if _is_ignored_file( + root, + self.config.ignore, + self.config.ignore_patterns, + self.config.ignore_paths, + ): + skip_subtrees.append(root) + continue + if "__init__.py" in files: skip_subtrees.append(root) yield root diff --git a/tests/lint/unittest_lint.py b/tests/lint/unittest_lint.py index 91df7dc1f6..f124e4251b 100644 --- a/tests/lint/unittest_lint.py +++ b/tests/lint/unittest_lint.py @@ -13,6 +13,7 @@ from io import StringIO from os import chdir, getcwd from os.path import abspath, basename, dirname, isdir, join, sep +from pathlib import Path from shutil import rmtree from typing import Iterable, Iterator, List, Optional, Tuple @@ -29,7 +30,13 @@ OLD_DEFAULT_PYLINT_HOME, ) from pylint.exceptions import InvalidMessageError -from pylint.lint import ArgumentPreprocessingError, PyLinter, Run, preprocess_options +from pylint.lint import ( + ArgumentPreprocessingError, + PyLinter, + Run, + fix_import_path, + preprocess_options, +) from pylint.message import Message from pylint.reporters import text from pylint.typing import MessageLocationTuple @@ -868,3 +875,67 @@ def test_by_module_statement_value(initialized_linter: PyLinter) -> None: # Check that the by_module "statement" is equal to the global "statement" # computed for that module assert module_stats["statement"] == linter2.stats.statement + + +@pytest.mark.parametrize( + "ignore_parameter,ignore_parameter_value", + [ + ("--ignore", "failing.py"), + ("--ignore", "ignored_subdirectory"), + ("--ignore-patterns", "failing.*"), + ("--ignore-patterns", "ignored_*"), + ("--ignore-paths", ".*directory/ignored.*"), + ("--ignore-paths", ".*ignored.*/failing.*"), + ], +) +def test_recursive_ignore(ignore_parameter, ignore_parameter_value) -> None: + run = Run( + [ + "--recursive", + "y", + ignore_parameter, + ignore_parameter_value, + join(REGRTEST_DATA_DIR, "directory"), + ], + exit=False, + ) + + linted_files = run.linter._iterate_file_descrs( + tuple(run.linter._discover_files([join(REGRTEST_DATA_DIR, "directory")])) + ) + linted_file_paths = [file_item.filepath for file_item in linted_files] + + ignored_file = os.path.abspath( + join(REGRTEST_DATA_DIR, "directory", "ignored_subdirectory", "failing.py") + ) + assert ignored_file not in linted_file_paths + + for regrtest_data_module in ( + ("directory", "subdirectory", "subsubdirectory", "module.py"), + ("directory", "subdirectory", "module.py"), + ("directory", "package", "module.py"), + ("directory", "package", "subpackage", "module.py"), + ): + module = os.path.abspath(join(REGRTEST_DATA_DIR, *regrtest_data_module)) + assert module in linted_file_paths + + +def test_import_sibling_module_from_namespace(initialized_linter: PyLinter) -> None: + """If the parent directory above `namespace` is on sys.path, ensure that + modules under `namespace` can import each other without raising `import-error`.""" + linter = initialized_linter + with tempdir() as tmpdir: + create_files(["namespace/submodule1.py", "namespace/submodule2.py"]) + second_path = Path("namespace/submodule2.py") + with open(second_path, "w", encoding="utf-8") as f: + f.write( + """\"\"\"This module imports submodule1.\"\"\" +import submodule1 +print(submodule1) +""" + ) + os.chdir("namespace") + # Add the parent directory to sys.path + with fix_import_path([tmpdir]): + linter.check(["submodule2.py"]) + assert not linter.stats.by_msg diff --git a/tests/regrtest_data/directory/ignored_subdirectory/failing.py b/tests/regrtest_data/directory/ignored_subdirectory/failing.py new file mode 100644 index 0000000000..b199df5420 --- /dev/null +++ b/tests/regrtest_data/directory/ignored_subdirectory/failing.py @@ -0,0 +1 @@ +import re diff --git a/tests/test_self.py b/tests/test_self.py index 36084b717a..27b1e7bede 100644 --- a/tests/test_self.py +++ b/tests/test_self.py @@ -1276,17 +1276,91 @@ def test_max_inferred_for_complicated_class_hierarchy() -> None: assert not ex.value.code % 2 def test_regression_recursive(self): + """Tests if error is raised when linter is executed over directory not using --recursive=y""" self._test_output( [join(HERE, "regrtest_data", "directory", "subdirectory"), "--recursive=n"], expected_output="No such file or directory", ) def test_recursive(self): + """Tests if running linter over directory using --recursive=y""" self._runtest( [join(HERE, "regrtest_data", "directory", "subdirectory"), "--recursive=y"], code=0, ) + def test_ignore_recursive(self): + """Tests recursive run of linter ignoring directory using --ignore parameter. + + Ignored directory contains files yielding lint errors. If directory is not ignored + test would fail due these errors. + """ + self._runtest( + [ + join(HERE, "regrtest_data", "directory"), + "--recursive=y", + "--ignore=ignored_subdirectory", + ], + code=0, + ) + + self._runtest( + [ + join(HERE, "regrtest_data", "directory"), + "--recursive=y", + "--ignore=failing.py", + ], + code=0, + ) + + def test_ignore_pattern_recursive(self): + """Tests recursive run of linter ignoring directory using --ignore-parameter parameter. + + Ignored directory contains files yielding lint errors. If directory is not ignored + test would fail due these errors. + """ + self._runtest( + [ + join(HERE, "regrtest_data", "directory"), + "--recursive=y", + "--ignore-pattern=ignored_.*", + ], + code=0, + ) + + self._runtest( + [ + join(HERE, "regrtest_data", "directory"), + "--recursive=y", + "--ignore-pattern=failing.*", + ], + code=0, + ) + + def test_ignore_path_recursive(self): + """Tests recursive run of linter ignoring directory using --ignore-path parameter. + + Ignored directory contains files yielding lint errors. If directory is not ignored + test would fail due these errors. + """ + self._runtest( + [ + join(HERE, "regrtest_data", "directory"), + "--recursive=y", + "--ignore-path=.*ignored.*", + ], + code=0, + ) + + self._runtest( + [ + join(HERE, "regrtest_data", "directory"), + "--recursive=y", + "--ignore-path=.*failing.*", + ], + code=0, + ) + def test_recursive_current_dir(self): with _test_sys_path(): # pytest is including directory HERE/regrtest_data to sys.path which causes @@ -1297,7 +1371,7 @@ def test_recursive_current_dir(self): if not os.path.basename(path) == "regrtest_data" ] with _test_cwd(): - os.chdir(join(HERE, "regrtest_data", "directory")) + os.chdir(join(HERE, "regrtest_data", "directory", "subdirectory")) self._runtest( [".", "--recursive=y"], code=0, From 2d10b8b530862517eefe5e89f7a84cc208eb76f3 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Fri, 13 May 2022 10:07:05 -0400 Subject: [PATCH 7/8] Fix false positive for `undefined-loop-variable` with `enumerate()` (#6602) Co-authored-by: Pierre Sassoulas --- ChangeLog | 4 ++++ doc/whatsnew/2.13.rst | 4 ++++ pylint/checkers/variables.py | 7 +++++++ tests/functional/u/undefined/undefined_loop_variable.py | 7 +++++++ 4 files changed, 22 insertions(+) diff --git a/ChangeLog b/ChangeLog index a49bb9e300..12e3c5599d 100644 --- a/ChangeLog +++ b/ChangeLog @@ -41,6 +41,10 @@ Release date: TBA Closes #6539 +* Fix a false positive for ``undefined-loop-variable`` when using ``enumerate()``. + + Closes #6593 + What's New in Pylint 2.13.8? ============================ diff --git a/doc/whatsnew/2.13.rst b/doc/whatsnew/2.13.rst index 1ae1ca7f9a..e5a628d8ab 100644 --- a/doc/whatsnew/2.13.rst +++ b/doc/whatsnew/2.13.rst @@ -657,3 +657,7 @@ Other Changes * Fix a crash in ``unnecessary-dict-index-lookup`` when subscripting an attribute. Closes #6557 + +* Fix a false positive for ``undefined-loop-variable`` when using ``enumerate()``. + + Closes #6593 diff --git a/pylint/checkers/variables.py b/pylint/checkers/variables.py index 807e031e60..fb50f4501b 100644 --- a/pylint/checkers/variables.py +++ b/pylint/checkers/variables.py @@ -2265,6 +2265,13 @@ def _loopvar_name(self, node: astroid.Name) -> None: # For functions we can do more by inferring the length of the itered object try: inferred = next(assign.iter.infer()) + # Prefer the target of enumerate() rather than the enumerate object itself + if ( + isinstance(inferred, astroid.Instance) + and inferred.qname() == "builtins.enumerate" + and assign.iter.args + ): + inferred = next(assign.iter.args[0].infer()) except astroid.InferenceError: self.add_message("undefined-loop-variable", args=node.name, node=node) else: diff --git a/tests/functional/u/undefined/undefined_loop_variable.py b/tests/functional/u/undefined/undefined_loop_variable.py index 6bcf6103d9..d66bd5a05a 100644 --- a/tests/functional/u/undefined/undefined_loop_variable.py +++ b/tests/functional/u/undefined/undefined_loop_variable.py @@ -139,3 +139,10 @@ def variable_name_assigned_in_body_of_second_loop(): alias = True if alias: print(alias) + + +def use_enumerate(): + """https://github.com/PyCQA/pylint/issues/6593""" + for i, num in enumerate(range(3)): + pass + print(i, num) From b3a15db705630c69782fe12988518ca93c8f126d Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Mon, 9 May 2022 21:16:34 +0200 Subject: [PATCH 8/8] Bump pylint to 2.13.9, update changelog --- CONTRIBUTORS.txt | 2 +- ChangeLog | 2 +- pylint/__pkginfo__.py | 2 +- pylint/checkers/utils.py | 2 +- pylint/lint/expand_modules.py | 6 +++--- pylint/lint/pylinter.py | 7 ++++--- tbump.toml | 2 +- tests/test_self.py | 8 ++++---- 8 files changed, 16 insertions(+), 15 deletions(-) diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index fd6f179687..42163d9610 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -89,8 +89,8 @@ contributors: - Ville Skyttä - Matus Valo - Pierre-Yves David -- David Shea : invalid sequence and slice index - Mark Byrne <31762852+mbyrnepr2@users.noreply.github.com> +- David Shea : invalid sequence and slice index - Derek Gustafson - Cezar Elnazli : deprecated-method - Nicolas Chauvat diff --git a/ChangeLog b/ChangeLog index 12e3c5599d..8c49493f72 100644 --- a/ChangeLog +++ b/ChangeLog @@ -18,7 +18,7 @@ Release date: TBA What's New in Pylint 2.13.9? ============================ -Release date: TBA +Release date: 2022-05-13 * Respect ignore configuration options with ``--recursive=y``. diff --git a/pylint/__pkginfo__.py b/pylint/__pkginfo__.py index 9ac08af5a0..91585ad9e1 100644 --- a/pylint/__pkginfo__.py +++ b/pylint/__pkginfo__.py @@ -4,7 +4,7 @@ from typing import Tuple -__version__ = "2.13.8" +__version__ = "2.13.9" def get_numversion_from_version(v: str) -> Tuple: diff --git a/pylint/checkers/utils.py b/pylint/checkers/utils.py index 9cba6e0579..5f00734e9b 100644 --- a/pylint/checkers/utils.py +++ b/pylint/checkers/utils.py @@ -820,7 +820,7 @@ def uninferable_final_decorators( """ decorators = [] for decorator in getattr(node, "nodes", []): - import_nodes: tuple[nodes.Import | nodes.ImportFrom] | None = None + import_nodes = None # Get the `Import` node. The decorator is of the form: @module.name if isinstance(decorator, nodes.Attribute): diff --git a/pylint/lint/expand_modules.py b/pylint/lint/expand_modules.py index 07b92895a3..302eea9fb3 100644 --- a/pylint/lint/expand_modules.py +++ b/pylint/lint/expand_modules.py @@ -45,9 +45,9 @@ def _is_in_ignore_list_re(element: str, ignore_list_re: List[Pattern]) -> bool: def _is_ignored_file( element: str, - ignore_list: list[str], - ignore_list_re: list[Pattern[str]], - ignore_list_paths_re: list[Pattern[str]], + ignore_list, + ignore_list_re, + ignore_list_paths_re, ) -> bool: basename = os.path.basename(element) return ( diff --git a/pylint/lint/pylinter.py b/pylint/lint/pylinter.py index 878c1190eb..a0218937df 100644 --- a/pylint/lint/pylinter.py +++ b/pylint/lint/pylinter.py @@ -251,6 +251,7 @@ def make_options() -> Tuple[Tuple[str, OptionDict], ...]: "type": "regexp_paths_csv", "metavar": "[,...]", "default": [], + "dest": "ignore_paths", "help": "Add files or directories matching the regex patterns to the " "ignore-list. The regex matches against paths and can be in " "Posix or Windows format.", @@ -1030,8 +1031,8 @@ def _discover_files(self, files_or_modules: Sequence[str]) -> Iterator[str]: if _is_ignored_file( root, - self.config.ignore, - self.config.ignore_patterns, + self.config.black_list, + self.config.black_list_re, self.config.ignore_paths, ): skip_subtrees.append(root) @@ -1193,7 +1194,7 @@ def _expand_files(self, modules) -> List[ModuleDescriptionDict]: modules, self.config.black_list, self.config.black_list_re, - self._ignore_paths, + self.config.ignore_paths, ) for error in errors: message = modname = error["mod"] diff --git a/tbump.toml b/tbump.toml index 0bc188dd32..f9d6a7e566 100644 --- a/tbump.toml +++ b/tbump.toml @@ -1,7 +1,7 @@ github_url = "https://github.com/PyCQA/pylint" [version] -current = "2.13.8" +current = "2.13.9" regex = ''' ^(?P0|[1-9]\d*) \. diff --git a/tests/test_self.py b/tests/test_self.py index 27b1e7bede..09011d91ef 100644 --- a/tests/test_self.py +++ b/tests/test_self.py @@ -1323,7 +1323,7 @@ def test_ignore_pattern_recursive(self): [ join(HERE, "regrtest_data", "directory"), "--recursive=y", - "--ignore-pattern=ignored_.*", + "--ignore-patterns=ignored_.*", ], code=0, ) @@ -1332,7 +1332,7 @@ def test_ignore_pattern_recursive(self): [ join(HERE, "regrtest_data", "directory"), "--recursive=y", - "--ignore-pattern=failing.*", + "--ignore-patterns=failing.*", ], code=0, ) @@ -1347,7 +1347,7 @@ def test_ignore_path_recursive(self): [ join(HERE, "regrtest_data", "directory"), "--recursive=y", - "--ignore-path=.*ignored.*", + "--ignore-paths=.*ignored.*", ], code=0, ) @@ -1356,7 +1356,7 @@ def test_ignore_path_recursive(self): [ join(HERE, "regrtest_data", "directory"), "--recursive=y", - "--ignore-path=.*failing.*", + "--ignore-paths=.*failing.*", ], code=0, )