From b1b067e8c540070cd1814f6cc3c8fcdf68e4807f Mon Sep 17 00:00:00 2001 From: Rogdham Date: Sat, 27 Aug 2022 17:07:25 +0200 Subject: [PATCH 01/13] Fix `used-before-assignment` for functions/classes defined in type checking guard Close #7368 --- doc/whatsnew/fragments/7368.false_positive | 3 +++ pylint/checkers/variables.py | 10 +++++++--- .../u/undefined/undefined_variable.py | 9 +++++++++ .../u/undefined/undefined_variable.txt | 18 +++++++++--------- 4 files changed, 28 insertions(+), 12 deletions(-) create mode 100644 doc/whatsnew/fragments/7368.false_positive diff --git a/doc/whatsnew/fragments/7368.false_positive b/doc/whatsnew/fragments/7368.false_positive new file mode 100644 index 0000000000..4e9551a321 --- /dev/null +++ b/doc/whatsnew/fragments/7368.false_positive @@ -0,0 +1,3 @@ +Fix ``used-before-assignment`` for functions/classes defined in type checking guard. + +Closes #7368 diff --git a/pylint/checkers/variables.py b/pylint/checkers/variables.py index 3249a4b361..ce6a384207 100644 --- a/pylint/checkers/variables.py +++ b/pylint/checkers/variables.py @@ -1841,7 +1841,6 @@ def _is_variable_violation( base_scope_type, is_recursive_klass, ) -> tuple[bool, bool, bool]: - # pylint: disable=too-many-nested-blocks maybe_before_assign = True annotation_return = False use_outer_definition = False @@ -2014,8 +2013,13 @@ def _is_variable_violation( for target in definition.targets if isinstance(target, nodes.AssignName) ) - if defined_in_or_else: - break + elif isinstance( + definition, (nodes.ClassDef, nodes.FunctionDef) + ): + defined_in_or_else = definition.name == node.name + + if defined_in_or_else: + break if not used_in_branch and not defined_in_or_else: maybe_before_assign = True diff --git a/tests/functional/u/undefined/undefined_variable.py b/tests/functional/u/undefined/undefined_variable.py index 8c85b8582a..78a32dacc4 100644 --- a/tests/functional/u/undefined/undefined_variable.py +++ b/tests/functional/u/undefined/undefined_variable.py @@ -267,15 +267,24 @@ def func_should_fail(_dt: datetime): # [used-before-assignment] if TYPE_CHECKING: from collections import Counter from collections import OrderedDict + from collections import defaultdict + from collections import UserDict else: Counter = object OrderedDict = object + def defaultdict(): + return {} + class UserDict(dict): + pass def tick(counter: Counter, name: str, dictionary: OrderedDict) -> OrderedDict: counter[name] += 1 return dictionary +defaultdict() + +UserDict() # pylint: disable=unused-argument def not_using_loop_variable_accordingly(iterator): diff --git a/tests/functional/u/undefined/undefined_variable.txt b/tests/functional/u/undefined/undefined_variable.txt index 85df230355..ab1a004209 100644 --- a/tests/functional/u/undefined/undefined_variable.txt +++ b/tests/functional/u/undefined/undefined_variable.txt @@ -28,12 +28,12 @@ undefined-variable:171:4:171:13::Undefined variable 'unicode_3':UNDEFINED undefined-variable:226:25:226:37:LambdaClass4.:Undefined variable 'LambdaClass4':UNDEFINED undefined-variable:234:25:234:37:LambdaClass5.:Undefined variable 'LambdaClass5':UNDEFINED used-before-assignment:255:26:255:34:func_should_fail:Using variable 'datetime' before assignment:HIGH -undefined-variable:282:18:282:24:not_using_loop_variable_accordingly:Undefined variable 'iteree':UNDEFINED -undefined-variable:299:27:299:28:undefined_annotation:Undefined variable 'x':UNDEFINED -used-before-assignment:300:7:300:8:undefined_annotation:Using variable 'x' before assignment:HIGH -undefined-variable:330:11:330:12:decorated3:Undefined variable 'x':UNDEFINED -undefined-variable:335:19:335:20:decorated4:Undefined variable 'y':UNDEFINED -undefined-variable:356:10:356:20:global_var_mixed_assignment:Undefined variable 'GLOBAL_VAR':HIGH -undefined-variable:368:19:368:44:RepeatedReturnAnnotations.x:Undefined variable 'RepeatedReturnAnnotations':UNDEFINED -undefined-variable:370:19:370:44:RepeatedReturnAnnotations.y:Undefined variable 'RepeatedReturnAnnotations':UNDEFINED -undefined-variable:372:19:372:44:RepeatedReturnAnnotations.z:Undefined variable 'RepeatedReturnAnnotations':UNDEFINED +undefined-variable:291:18:291:24:not_using_loop_variable_accordingly:Undefined variable 'iteree':UNDEFINED +undefined-variable:308:27:308:28:undefined_annotation:Undefined variable 'x':UNDEFINED +used-before-assignment:309:7:309:8:undefined_annotation:Using variable 'x' before assignment:HIGH +undefined-variable:339:11:339:12:decorated3:Undefined variable 'x':UNDEFINED +undefined-variable:344:19:344:20:decorated4:Undefined variable 'y':UNDEFINED +undefined-variable:365:10:365:20:global_var_mixed_assignment:Undefined variable 'GLOBAL_VAR':HIGH +undefined-variable:377:19:377:44:RepeatedReturnAnnotations.x:Undefined variable 'RepeatedReturnAnnotations':UNDEFINED +undefined-variable:379:19:379:44:RepeatedReturnAnnotations.y:Undefined variable 'RepeatedReturnAnnotations':UNDEFINED +undefined-variable:381:19:381:44:RepeatedReturnAnnotations.z:Undefined variable 'RepeatedReturnAnnotations':UNDEFINED From a10406ab31725993b9c6b44154d71166f10e248d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20Noord?= <13665637+DanielNoord@users.noreply.github.com> Date: Wed, 31 Aug 2022 22:06:40 +0200 Subject: [PATCH 02/13] Fix crash while iteraring over a class attribute (#7386) Co-authored-by: orSolocate <38433858+orSolocate@users.noreply.github.com> --- doc/whatsnew/fragments/7380.bugfix | 3 +++ pylint/checkers/modified_iterating_checker.py | 8 ++++++-- tests/functional/m/modified_iterating.py | 16 ++++++++++++++-- tests/functional/m/modified_iterating.txt | 1 + 4 files changed, 24 insertions(+), 4 deletions(-) create mode 100644 doc/whatsnew/fragments/7380.bugfix diff --git a/doc/whatsnew/fragments/7380.bugfix b/doc/whatsnew/fragments/7380.bugfix new file mode 100644 index 0000000000..dc5ea5fa6b --- /dev/null +++ b/doc/whatsnew/fragments/7380.bugfix @@ -0,0 +1,3 @@ +Update ``modified_iterating`` checker to fix a crash with ``for`` loops on empty list. + +Closes #7380 diff --git a/pylint/checkers/modified_iterating_checker.py b/pylint/checkers/modified_iterating_checker.py index bdabc26503..62e887a954 100644 --- a/pylint/checkers/modified_iterating_checker.py +++ b/pylint/checkers/modified_iterating_checker.py @@ -81,7 +81,7 @@ def _modified_iterating_check( msg_id = "modified-iterating-dict" elif isinstance(inferred, nodes.Set): msg_id = "modified-iterating-set" - elif not isinstance(iter_obj, nodes.Name): + elif not isinstance(iter_obj, (nodes.Name, nodes.Attribute)): pass elif self._modified_iterating_list_cond(node, iter_obj): msg_id = "modified-iterating-list" @@ -90,10 +90,14 @@ def _modified_iterating_check( elif self._modified_iterating_set_cond(node, iter_obj): msg_id = "modified-iterating-set" if msg_id: + if isinstance(iter_obj, nodes.Attribute): + obj_name = iter_obj.attrname + else: + obj_name = iter_obj.name self.add_message( msg_id, node=node, - args=(iter_obj.name,), + args=(obj_name,), confidence=interfaces.INFERENCE, ) diff --git a/tests/functional/m/modified_iterating.py b/tests/functional/m/modified_iterating.py index 166bf4f6c4..527ce358cb 100644 --- a/tests/functional/m/modified_iterating.py +++ b/tests/functional/m/modified_iterating.py @@ -1,5 +1,5 @@ """Tests for iterating-modified messages""" -# pylint: disable=not-callable,unnecessary-comprehension +# pylint: disable=not-callable,unnecessary-comprehension,too-few-public-methods import copy @@ -26,7 +26,7 @@ i = 1 for item in my_dict: item_list[0] = i # for coverage, see reference at /pull/5628#discussion_r792181642 - my_dict[i] = 1 # [modified-iterating-dict] + my_dict[i] = 1 # [modified-iterating-dict] i += 1 i = 1 @@ -93,3 +93,15 @@ def update_existing_key(): for key in my_dict: new_key = key.lower() my_dict[new_key] = 1 # [modified-iterating-dict] + + +class MyClass: + """Regression test for https://github.com/PyCQA/pylint/issues/7380""" + + def __init__(self) -> None: + self.attribute = [1, 2, 3] + + def my_method(self): + """This should raise as we are deleting.""" + for var in self.attribute: + del var # [modified-iterating-list] diff --git a/tests/functional/m/modified_iterating.txt b/tests/functional/m/modified_iterating.txt index c63095556f..7d5e3014d7 100644 --- a/tests/functional/m/modified_iterating.txt +++ b/tests/functional/m/modified_iterating.txt @@ -13,3 +13,4 @@ modified-iterating-list:64:4:64:23::Iterated list 'item_list' is being modified modified-iterating-list:67:12:67:31::Iterated list 'item_list' is being modified inside for loop body, consider iterating through a copy of it instead.:INFERENCE modified-iterating-list:69:16:69:35::Iterated list 'item_list' is being modified inside for loop body, consider iterating through a copy of it instead.:INFERENCE modified-iterating-dict:95:8:95:28:update_existing_key:Iterated dict 'my_dict' is being modified inside for loop body, iterate through a copy of it instead.:INFERENCE +modified-iterating-list:107:12:107:19:MyClass.my_method:Iterated list 'attribute' is being modified inside for loop body, consider iterating through a copy of it instead.:INFERENCE From 7a1fbb9f9aa8d1938ad768ff3d0403aa7d0c4bfb Mon Sep 17 00:00:00 2001 From: Mark Byrne <31762852+mbyrnepr2@users.noreply.github.com> Date: Fri, 2 Sep 2022 09:35:16 +0200 Subject: [PATCH 03/13] Fix false positive for ``too-many-function-args`` when a function call is assigned to a class attribute inside the class where the function is defined. (#7395) Closes #6592 --- doc/whatsnew/fragments/6592.false_positive | 3 +++ pylint/checkers/typecheck.py | 13 +++++++++++++ tests/functional/a/arguments.py | 19 ++++++++++++++++++- 3 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 doc/whatsnew/fragments/6592.false_positive diff --git a/doc/whatsnew/fragments/6592.false_positive b/doc/whatsnew/fragments/6592.false_positive new file mode 100644 index 0000000000..846ddce961 --- /dev/null +++ b/doc/whatsnew/fragments/6592.false_positive @@ -0,0 +1,3 @@ +Fix false positive for ``too-many-function-args`` when a function call is assigned to a class attribute inside the class where the function is defined. + +Closes #6592 diff --git a/pylint/checkers/typecheck.py b/pylint/checkers/typecheck.py index 9732ede783..f2518beae6 100644 --- a/pylint/checkers/typecheck.py +++ b/pylint/checkers/typecheck.py @@ -1458,6 +1458,19 @@ def visit_call(self, node: nodes.Call) -> None: keyword_args += list(already_filled_keywords) num_positional_args += implicit_args + already_filled_positionals + # Decrement `num_positional_args` by 1 when a function call is assigned to a class attribute + # inside the class where the function is defined. + # This avoids emitting `too-many-function-args` since `num_positional_args` + # includes an implicit `self` argument which is not present in `called.args`. + if ( + isinstance(node.frame(), nodes.ClassDef) + and isinstance(node.parent, (nodes.Assign, nodes.AnnAssign)) + and isinstance(called, nodes.FunctionDef) + and called in node.frame().body + and num_positional_args > 0 + ): + num_positional_args -= 1 + # Analyze the list of formal parameters. args = list(itertools.chain(called.args.posonlyargs or (), called.args.args)) num_mandatory_parameters = len(args) - len(called.args.defaults) diff --git a/tests/functional/a/arguments.py b/tests/functional/a/arguments.py index a065e517e0..6929b98500 100644 --- a/tests/functional/a/arguments.py +++ b/tests/functional/a/arguments.py @@ -1,6 +1,6 @@ # pylint: disable=too-few-public-methods, missing-docstring,import-error,wrong-import-position # pylint: disable=wrong-import-order, unnecessary-lambda, consider-using-f-string -# pylint: disable=unnecessary-lambda-assignment +# pylint: disable=unnecessary-lambda-assignment, no-self-argument, unused-argument def decorator(fun): """Decorator""" @@ -261,3 +261,20 @@ def func(one, two, three): CALL = lambda *args: func(*args) + + +# Ensure `too-many-function-args` is not emitted when a function call is assigned +# to a class attribute inside the class where the function is defined. +# Reference: https://github.com/PyCQA/pylint/issues/6592 +class FruitPicker: + def _pick_fruit(fruit): + def _print_selection(self): + print(f"Selected: {fruit}!") + return _print_selection + + pick_apple = _pick_fruit("apple") + pick_pear = _pick_fruit("pear") + +picker = FruitPicker() +picker.pick_apple() +picker.pick_pear() From d476a8bd2557ec1a075e4a8ac630469700ba753a Mon Sep 17 00:00:00 2001 From: Christoph Blessing <33834216+cblessing24@users.noreply.github.com> Date: Sat, 3 Sep 2022 20:44:32 +0200 Subject: [PATCH 04/13] Do not lint ignored file on stdin (#7220) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously pylint would lint a file passed on stdin even if the user meant to ignore the file. This commit fixes that issue. Co-authored-by: Daniël van Noord <13665637+DanielNoord@users.noreply.github.com> --- doc/whatsnew/fragments/4354.bugfix | 3 +++ pylint/lint/pylinter.py | 17 +++++++++++------ tests/test_self.py | 14 ++++++++++++++ 3 files changed, 28 insertions(+), 6 deletions(-) create mode 100644 doc/whatsnew/fragments/4354.bugfix diff --git a/doc/whatsnew/fragments/4354.bugfix b/doc/whatsnew/fragments/4354.bugfix new file mode 100644 index 0000000000..09caf8d139 --- /dev/null +++ b/doc/whatsnew/fragments/4354.bugfix @@ -0,0 +1,3 @@ +Fix ignored files being linted when passed on stdin. + +Closes #4354 diff --git a/pylint/lint/pylinter.py b/pylint/lint/pylinter.py index 472b3b0302..8fa47ffe83 100644 --- a/pylint/lint/pylinter.py +++ b/pylint/lint/pylinter.py @@ -660,9 +660,7 @@ def check(self, files_or_modules: Sequence[str] | str) -> None: # 3) Get all FileItems with fix_import_path(files_or_modules): if self.config.from_stdin: - fileitems = iter( - (self._get_file_descr_from_stdin(files_or_modules[0]),) - ) + fileitems = self._get_file_descr_from_stdin(files_or_modules[0]) data: str | None = _read_stdin() else: fileitems = self._iterate_file_descrs(files_or_modules) @@ -817,14 +815,21 @@ def _check_file( for msgid, line, args in spurious_messages: self.add_message(msgid, line, None, args) - @staticmethod - def _get_file_descr_from_stdin(filepath: str) -> FileItem: + def _get_file_descr_from_stdin(self, filepath: str) -> Iterator[FileItem]: """Return file description (tuple of module name, file path, base name) from given file path. This method is used for creating suitable file description for _check_files when the source is standard input. """ + if _is_ignored_file( + filepath, + self.config.ignore, + self.config.ignore_patterns, + self.config.ignore_paths, + ): + return + try: # Note that this function does not really perform an # __import__ but may raise an ImportError exception, which @@ -833,7 +838,7 @@ def _get_file_descr_from_stdin(filepath: str) -> FileItem: except ImportError: modname = os.path.splitext(os.path.basename(filepath))[0] - return FileItem(modname, filepath, filepath) + yield FileItem(modname, filepath, filepath) def _iterate_file_descrs( self, files_or_modules: Sequence[str] diff --git a/tests/test_self.py b/tests/test_self.py index 53c9fb11bc..f2587c5c73 100644 --- a/tests/test_self.py +++ b/tests/test_self.py @@ -1128,6 +1128,20 @@ def test_ignore_pattern_recursive(self, ignore_pattern_value: str) -> None: code=0, ) + def test_ignore_pattern_from_stdin(self) -> None: + """Test if linter ignores standard input if the filename matches the ignore pattern.""" + with mock.patch("pylint.lint.pylinter._read_stdin", return_value="import os\n"): + self._runtest( + [ + "--from-stdin", + "mymodule.py", + "--disable=all", + "--enable=unused-import", + "--ignore-patterns=mymodule.py", + ], + code=0, + ) + @pytest.mark.parametrize("ignore_path_value", [".*ignored.*", ".*failing.*"]) def test_ignore_path_recursive(self, ignore_path_value: str) -> None: """Tests recursive run of linter ignoring directory using --ignore-path parameter. From 40a53aa4954e643baa60006f0c6341df15fdb9c4 Mon Sep 17 00:00:00 2001 From: Levi Gruspe Date: Sun, 4 Sep 2022 19:10:41 +0800 Subject: [PATCH 05/13] Fix #3299 false positives with names in string literal type annotations (#7400) Don't emit 'unused-variable' or 'unused-import' on names in string literal type annotations (#3299) Don't treat strings inside typing.Literal as names --- doc/whatsnew/fragments/3299.false_positive | 3 ++ pylint/checkers/utils.py | 22 ++++++++++ pylint/checkers/variables.py | 41 ++++++++++++++++++- tests/checkers/unittest_utils.py | 26 ++++++++++++ ..._name_in_string_literal_type_annotation.py | 26 ++++++++++++ ...in_string_literal_type_annotation_py310.py | 9 ++++ ...in_string_literal_type_annotation_py310.rc | 2 + ..._in_string_literal_type_annotation_py38.py | 20 +++++++++ ..._in_string_literal_type_annotation_py38.rc | 2 + ...in_string_literal_type_annotation_py38.txt | 4 ++ ..._in_string_literal_type_annotation_py39.py | 11 +++++ ..._in_string_literal_type_annotation_py39.rc | 2 + 12 files changed, 167 insertions(+), 1 deletion(-) create mode 100644 doc/whatsnew/fragments/3299.false_positive create mode 100644 tests/functional/u/unused/unused_name_in_string_literal_type_annotation.py create mode 100644 tests/functional/u/unused/unused_name_in_string_literal_type_annotation_py310.py create mode 100644 tests/functional/u/unused/unused_name_in_string_literal_type_annotation_py310.rc create mode 100644 tests/functional/u/unused/unused_name_in_string_literal_type_annotation_py38.py create mode 100644 tests/functional/u/unused/unused_name_in_string_literal_type_annotation_py38.rc create mode 100644 tests/functional/u/unused/unused_name_in_string_literal_type_annotation_py38.txt create mode 100644 tests/functional/u/unused/unused_name_in_string_literal_type_annotation_py39.py create mode 100644 tests/functional/u/unused/unused_name_in_string_literal_type_annotation_py39.rc diff --git a/doc/whatsnew/fragments/3299.false_positive b/doc/whatsnew/fragments/3299.false_positive new file mode 100644 index 0000000000..b1e61c9313 --- /dev/null +++ b/doc/whatsnew/fragments/3299.false_positive @@ -0,0 +1,3 @@ +Fix false positive for ``unused-variable`` and ``unused-import`` when a name is only used in a string literal type annotation. + +Closes #3299 diff --git a/pylint/checkers/utils.py b/pylint/checkers/utils.py index 08676c2e80..68452db0e4 100644 --- a/pylint/checkers/utils.py +++ b/pylint/checkers/utils.py @@ -1891,6 +1891,28 @@ def in_type_checking_block(node: nodes.NodeNG) -> bool: return False +def is_typing_literal(node: nodes.NodeNG) -> bool: + """Check if a node refers to typing.Literal.""" + if isinstance(node, nodes.Name): + try: + import_from = node.lookup(node.name)[1][0] + except IndexError: + return False + if isinstance(import_from, nodes.ImportFrom): + return ( + import_from.modname == "typing" + and import_from.real_name(node.name) == "Literal" + ) + elif isinstance(node, nodes.Attribute): + inferred_module = safe_infer(node.expr) + return ( + isinstance(inferred_module, nodes.Module) + and inferred_module.name == "typing" + and node.attrname == "Literal" + ) + return False + + @lru_cache() def in_for_else_branch(parent: nodes.NodeNG, stmt: nodes.Statement) -> bool: """Returns True if stmt is inside the else branch for a parent For stmt.""" diff --git a/pylint/checkers/variables.py b/pylint/checkers/variables.py index ce6a384207..74b8c4c706 100644 --- a/pylint/checkers/variables.py +++ b/pylint/checkers/variables.py @@ -19,7 +19,7 @@ from typing import TYPE_CHECKING, Any, NamedTuple import astroid -from astroid import nodes +from astroid import extract_node, nodes from astroid.typing import InferenceResult from pylint.checkers import BaseChecker, utils @@ -2341,6 +2341,10 @@ def _check_is_unused( if name in comprehension_target_names: return + # Ignore names in string literal type annotation. + if name in self._type_annotation_names: + return + argnames = node.argnames() # Care about functions with unknown argument (builtins) if name in argnames: @@ -2904,6 +2908,41 @@ def _check_potential_index_error( ) return + @utils.only_required_for_messages( + "unused-import", + "unused-variable", + ) + def visit_const(self, node: nodes.Const) -> None: + """Take note of names that appear inside string literal type annotations + unless the string is a parameter to typing.Literal. + """ + if node.pytype() != "builtins.str": + return + if not utils.is_node_in_type_annotation_context(node): + return + if not node.value.isidentifier(): + try: + annotation = extract_node(node.value) + self._store_type_annotation_node(annotation) + except ValueError: + # e.g. node.value is white space + return + except astroid.AstroidSyntaxError: + # e.g. "?" or ":" in typing.Literal["?", ":"] + return + + # Check if parent's or grandparent's first child is typing.Literal + parent = node.parent + if isinstance(parent, nodes.Tuple): + parent = parent.parent + + if isinstance(parent, nodes.Subscript): + origin = next(parent.get_children(), None) + if origin is not None and utils.is_typing_literal(origin): + return + + self._type_annotation_names.append(node.value) + def register(linter: PyLinter) -> None: linter.register_checker(VariablesChecker(linter)) diff --git a/tests/checkers/unittest_utils.py b/tests/checkers/unittest_utils.py index 8b91898920..f68a48dbbc 100644 --- a/tests/checkers/unittest_utils.py +++ b/tests/checkers/unittest_utils.py @@ -489,3 +489,29 @@ def visit_assname(self, node: nodes.NodeNG) -> None: records[0].message.args[0] == "utils.check_messages will be removed in favour of calling utils.only_required_for_messages in pylint 3.0" ) + + +def test_is_typing_literal() -> None: + code = astroid.extract_node( + """ + from typing import Literal as Lit, Set as Literal + import typing as t + + Literal #@ + Lit #@ + t.Literal #@ + """ + ) + + assert not utils.is_typing_literal(code[0]) + assert utils.is_typing_literal(code[1]) + assert utils.is_typing_literal(code[2]) + + code = astroid.extract_node( + """ + Literal #@ + typing.Literal #@ + """ + ) + assert not utils.is_typing_literal(code[0]) + assert not utils.is_typing_literal(code[1]) diff --git a/tests/functional/u/unused/unused_name_in_string_literal_type_annotation.py b/tests/functional/u/unused/unused_name_in_string_literal_type_annotation.py new file mode 100644 index 0000000000..389647657b --- /dev/null +++ b/tests/functional/u/unused/unused_name_in_string_literal_type_annotation.py @@ -0,0 +1,26 @@ +"""Test if pylint sees names inside string literal type annotations. #3299""" +# pylint: disable=too-few-public-methods + +from argparse import ArgumentParser, Namespace +import os +from os import PathLike +from pathlib import Path +from typing import NoReturn, Set + +# unused-import shouldn't be emitted for Path +example1: Set["Path"] = set() + +def example2(_: "ArgumentParser") -> "NoReturn": + """unused-import shouldn't be emitted for ArgumentParser or NoReturn.""" + while True: + pass + +def example3(_: "os.PathLike[str]") -> None: + """unused-import shouldn't be emitted for os.""" + +def example4(_: "PathLike[str]") -> None: + """unused-import shouldn't be emitted for PathLike.""" + +class Class: + """unused-import shouldn't be emitted for Namespace""" + cls: "Namespace" diff --git a/tests/functional/u/unused/unused_name_in_string_literal_type_annotation_py310.py b/tests/functional/u/unused/unused_name_in_string_literal_type_annotation_py310.py new file mode 100644 index 0000000000..00bf5799fc --- /dev/null +++ b/tests/functional/u/unused/unused_name_in_string_literal_type_annotation_py310.py @@ -0,0 +1,9 @@ +# pylint: disable=missing-docstring + +from typing import TypeAlias + +def unused_variable_should_not_be_emitted(): + """unused-variable shouldn't be emitted for Example.""" + Example: TypeAlias = int + result: set["Example"] = set() + return result diff --git a/tests/functional/u/unused/unused_name_in_string_literal_type_annotation_py310.rc b/tests/functional/u/unused/unused_name_in_string_literal_type_annotation_py310.rc new file mode 100644 index 0000000000..68a8c8ef15 --- /dev/null +++ b/tests/functional/u/unused/unused_name_in_string_literal_type_annotation_py310.rc @@ -0,0 +1,2 @@ +[testoptions] +min_pyver=3.10 diff --git a/tests/functional/u/unused/unused_name_in_string_literal_type_annotation_py38.py b/tests/functional/u/unused/unused_name_in_string_literal_type_annotation_py38.py new file mode 100644 index 0000000000..497f64937b --- /dev/null +++ b/tests/functional/u/unused/unused_name_in_string_literal_type_annotation_py38.py @@ -0,0 +1,20 @@ +# pylint: disable=missing-docstring + +from argparse import ArgumentParser # [unused-import] +from argparse import Namespace # [unused-import] +from typing import Literal as Lit +import typing as t + +# str inside Literal shouldn't be treated as names +example1: t.Literal["ArgumentParser", Lit["Namespace", "ArgumentParser"]] + + +def unused_variable_example(): + hello = "hello" # [unused-variable] + world = "world" # [unused-variable] + example2: Lit["hello", "world"] = "hello" + return example2 + + +# pylint shouldn't crash with the following strings in a type annotation context +example3: Lit["", " ", "?"] = "?" diff --git a/tests/functional/u/unused/unused_name_in_string_literal_type_annotation_py38.rc b/tests/functional/u/unused/unused_name_in_string_literal_type_annotation_py38.rc new file mode 100644 index 0000000000..85fc502b37 --- /dev/null +++ b/tests/functional/u/unused/unused_name_in_string_literal_type_annotation_py38.rc @@ -0,0 +1,2 @@ +[testoptions] +min_pyver=3.8 diff --git a/tests/functional/u/unused/unused_name_in_string_literal_type_annotation_py38.txt b/tests/functional/u/unused/unused_name_in_string_literal_type_annotation_py38.txt new file mode 100644 index 0000000000..082595bfff --- /dev/null +++ b/tests/functional/u/unused/unused_name_in_string_literal_type_annotation_py38.txt @@ -0,0 +1,4 @@ +unused-import:3:0:3:35::Unused ArgumentParser imported from argparse:UNDEFINED +unused-import:4:0:4:30::Unused Namespace imported from argparse:UNDEFINED +unused-variable:13:4:13:9:unused_variable_example:Unused variable 'hello':UNDEFINED +unused-variable:14:4:14:9:unused_variable_example:Unused variable 'world':UNDEFINED diff --git a/tests/functional/u/unused/unused_name_in_string_literal_type_annotation_py39.py b/tests/functional/u/unused/unused_name_in_string_literal_type_annotation_py39.py new file mode 100644 index 0000000000..1258844cd2 --- /dev/null +++ b/tests/functional/u/unused/unused_name_in_string_literal_type_annotation_py39.py @@ -0,0 +1,11 @@ +# pylint: disable=missing-docstring + +import graphlib +from graphlib import TopologicalSorter + +def example( + sorter1: "graphlib.TopologicalSorter[int]", + sorter2: "TopologicalSorter[str]", +) -> None: + """unused-import shouldn't be emitted for graphlib or TopologicalSorter.""" + print(sorter1, sorter2) diff --git a/tests/functional/u/unused/unused_name_in_string_literal_type_annotation_py39.rc b/tests/functional/u/unused/unused_name_in_string_literal_type_annotation_py39.rc new file mode 100644 index 0000000000..16b75eea75 --- /dev/null +++ b/tests/functional/u/unused/unused_name_in_string_literal_type_annotation_py39.rc @@ -0,0 +1,2 @@ +[testoptions] +min_pyver=3.9 From fb3a641c0cb41bcf10b02e5aab243565b4f2ccdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20Noord?= <13665637+DanielNoord@users.noreply.github.com> Date: Sun, 4 Sep 2022 21:35:11 +0200 Subject: [PATCH 06/13] Make ``missing-return-doc`` respect the ``no-docstring-rgx`` option (#7410) --- doc/whatsnew/fragments/4743.bugfix | 3 ++ pylint/extensions/docparams.py | 5 +++ tests/functional/ext/docparams/docparams.py | 26 +++++++++----- tests/functional/ext/docparams/docparams.rc | 1 + tests/functional/ext/docparams/docparams.txt | 34 +++++++++++-------- .../return/missing_return_doc_required.py | 7 ++++ 6 files changed, 54 insertions(+), 22 deletions(-) create mode 100644 doc/whatsnew/fragments/4743.bugfix diff --git a/doc/whatsnew/fragments/4743.bugfix b/doc/whatsnew/fragments/4743.bugfix new file mode 100644 index 0000000000..20dbe071dd --- /dev/null +++ b/doc/whatsnew/fragments/4743.bugfix @@ -0,0 +1,3 @@ +``missing-return-doc`` now respects the ``no-docstring-rgx`` option. + +Closes #4743 diff --git a/pylint/extensions/docparams.py b/pylint/extensions/docparams.py index 805e126aaf..01e85b491c 100644 --- a/pylint/extensions/docparams.py +++ b/pylint/extensions/docparams.py @@ -327,6 +327,11 @@ def visit_return(self, node: nodes.Return) -> None: if not isinstance(func_node, astroid.FunctionDef): return + # skip functions that match the 'no-docstring-rgx' config option + no_docstring_rgx = self.linter.config.no_docstring_rgx + if no_docstring_rgx and re.match(no_docstring_rgx, func_node.name): + return + doc = utils.docstringify( func_node.doc_node, self.linter.config.default_docstring_type ) diff --git a/tests/functional/ext/docparams/docparams.py b/tests/functional/ext/docparams/docparams.py index 295cf430d6..3798ee3ac2 100644 --- a/tests/functional/ext/docparams/docparams.py +++ b/tests/functional/ext/docparams/docparams.py @@ -1,19 +1,23 @@ """Fixture for testing missing documentation in docparams.""" -def _private_func1(param1): # [missing-return-doc, missing-return-type-doc] +def _private_func1( # [missing-return-doc, missing-return-type-doc, missing-any-param-doc] + param1, +): """This is a test docstring without returns""" return param1 -def _private_func2(param1): # [missing-yield-doc, missing-yield-type-doc] +def _private_func2( # [missing-yield-doc, missing-yield-type-doc, missing-any-param-doc] + param1, +): """This is a test docstring without yields""" yield param1 -def _private_func3(param1): # [missing-raises-doc] +def _private_func3(param1): # [missing-raises-doc, missing-any-param-doc] """This is a test docstring without raises""" - raise Exception('Example') + raise Exception("Example") def public_func1(param1): # [missing-any-param-doc] @@ -21,19 +25,25 @@ def public_func1(param1): # [missing-any-param-doc] print(param1) -async def _async_private_func1(param1): # [missing-return-doc, missing-return-type-doc] +# pylint: disable-next=line-too-long +async def _async_private_func1( # [missing-return-doc, missing-return-type-doc, missing-any-param-doc] + param1, +): """This is a test docstring without returns""" return param1 -async def _async_private_func2(param1): # [missing-yield-doc, missing-yield-type-doc] +# pylint: disable-next=line-too-long +async def _async_private_func2( # [missing-yield-doc, missing-yield-type-doc, missing-any-param-doc] + param1, +): """This is a test docstring without yields""" yield param1 -async def _async_private_func3(param1): # [missing-raises-doc] +async def _async_private_func3(param1): # [missing-raises-doc, missing-any-param-doc] """This is a test docstring without raises""" - raise Exception('Example') + raise Exception("Example") async def async_public_func1(param1): # [missing-any-param-doc] diff --git a/tests/functional/ext/docparams/docparams.rc b/tests/functional/ext/docparams/docparams.rc index 24fa52ef43..2a09f2f6d5 100644 --- a/tests/functional/ext/docparams/docparams.rc +++ b/tests/functional/ext/docparams/docparams.rc @@ -1,5 +1,6 @@ [MAIN] load-plugins = pylint.extensions.docparams +no-docstring-rgx = ONLYVERYSPECIFICFUNCTIONS [BASIC] accept-no-param-doc = no diff --git a/tests/functional/ext/docparams/docparams.txt b/tests/functional/ext/docparams/docparams.txt index b896404878..2504e2b630 100644 --- a/tests/functional/ext/docparams/docparams.txt +++ b/tests/functional/ext/docparams/docparams.txt @@ -1,16 +1,22 @@ +missing-any-param-doc:4:0:4:18:_private_func1:"Missing any documentation in ""_private_func1""":HIGH missing-return-doc:4:0:4:18:_private_func1:Missing return documentation:HIGH missing-return-type-doc:4:0:4:18:_private_func1:Missing return type documentation:HIGH -missing-yield-doc:9:0:9:18:_private_func2:Missing yield documentation:HIGH -missing-yield-type-doc:9:0:9:18:_private_func2:Missing yield type documentation:HIGH -missing-raises-doc:14:0:14:18:_private_func3:"""Exception"" not documented as being raised":HIGH -missing-any-param-doc:19:0:19:16:public_func1:"Missing any documentation in ""public_func1""":HIGH -missing-return-doc:24:0:24:30:_async_private_func1:Missing return documentation:HIGH -missing-return-type-doc:24:0:24:30:_async_private_func1:Missing return type documentation:HIGH -missing-yield-doc:29:0:29:30:_async_private_func2:Missing yield documentation:HIGH -missing-yield-type-doc:29:0:29:30:_async_private_func2:Missing yield type documentation:HIGH -missing-raises-doc:34:0:34:30:_async_private_func3:"""Exception"" not documented as being raised":HIGH -missing-any-param-doc:39:0:39:28:async_public_func1:"Missing any documentation in ""async_public_func1""":HIGH -differing-param-doc:44:0:44:23:differing_param_doc:"""param"" differing in parameter documentation":HIGH -differing-param-doc:55:0:55:35:differing_param_doc_kwords_only:"""param"" differing in parameter documentation":HIGH -missing-type-doc:66:0:66:20:missing_type_doc:"""par1"" missing in parameter type documentation":HIGH -missing-type-doc:76:0:76:32:missing_type_doc_kwords_only:"""par1"" missing in parameter type documentation":HIGH +missing-any-param-doc:11:0:11:18:_private_func2:"Missing any documentation in ""_private_func2""":HIGH +missing-yield-doc:11:0:11:18:_private_func2:Missing yield documentation:HIGH +missing-yield-type-doc:11:0:11:18:_private_func2:Missing yield type documentation:HIGH +missing-any-param-doc:18:0:18:18:_private_func3:"Missing any documentation in ""_private_func3""":HIGH +missing-raises-doc:18:0:18:18:_private_func3:"""Exception"" not documented as being raised":HIGH +missing-any-param-doc:23:0:23:16:public_func1:"Missing any documentation in ""public_func1""":HIGH +missing-any-param-doc:29:0:29:30:_async_private_func1:"Missing any documentation in ""_async_private_func1""":HIGH +missing-return-doc:29:0:29:30:_async_private_func1:Missing return documentation:HIGH +missing-return-type-doc:29:0:29:30:_async_private_func1:Missing return type documentation:HIGH +missing-any-param-doc:37:0:37:30:_async_private_func2:"Missing any documentation in ""_async_private_func2""":HIGH +missing-yield-doc:37:0:37:30:_async_private_func2:Missing yield documentation:HIGH +missing-yield-type-doc:37:0:37:30:_async_private_func2:Missing yield type documentation:HIGH +missing-any-param-doc:44:0:44:30:_async_private_func3:"Missing any documentation in ""_async_private_func3""":HIGH +missing-raises-doc:44:0:44:30:_async_private_func3:"""Exception"" not documented as being raised":HIGH +missing-any-param-doc:49:0:49:28:async_public_func1:"Missing any documentation in ""async_public_func1""":HIGH +differing-param-doc:54:0:54:23:differing_param_doc:"""param"" differing in parameter documentation":HIGH +differing-param-doc:65:0:65:35:differing_param_doc_kwords_only:"""param"" differing in parameter documentation":HIGH +missing-type-doc:76:0:76:20:missing_type_doc:"""par1"" missing in parameter type documentation":HIGH +missing-type-doc:86:0:86:32:missing_type_doc_kwords_only:"""par1"" missing in parameter type documentation":HIGH diff --git a/tests/functional/ext/docparams/return/missing_return_doc_required.py b/tests/functional/ext/docparams/return/missing_return_doc_required.py index c34b35777f..bd56e7e070 100644 --- a/tests/functional/ext/docparams/return/missing_return_doc_required.py +++ b/tests/functional/ext/docparams/return/missing_return_doc_required.py @@ -5,3 +5,10 @@ def warns_no_docstring(self): # [missing-return-doc, missing-return-type-doc] return False + + +# this function doesn't require a docstring, because its name starts +# with an '_' (no-docstring-rgx): +def _function(some_arg: int) -> int: + _ = some_arg + return 0 From b04722031eef4e9c05c4bdb5a253a8131d2d5fa8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20Noord?= <13665637+DanielNoord@users.noreply.github.com> Date: Mon, 5 Sep 2022 13:44:38 +0200 Subject: [PATCH 07/13] Make ``disable-next`` only consider the succeeding line (#7411) --- doc/whatsnew/fragments/7401.bugfix | 3 ++ pylint/checkers/variables.py | 3 +- pylint/lint/message_state_handler.py | 10 ++-- pylint/utils/file_state.py | 50 ++++++++++++++------ tests/functional/d/disable_msg_next_line.py | 7 +++ tests/functional/d/disable_msg_next_line.txt | 3 ++ 6 files changed, 54 insertions(+), 22 deletions(-) create mode 100644 doc/whatsnew/fragments/7401.bugfix diff --git a/doc/whatsnew/fragments/7401.bugfix b/doc/whatsnew/fragments/7401.bugfix new file mode 100644 index 0000000000..8b0f0e2a84 --- /dev/null +++ b/doc/whatsnew/fragments/7401.bugfix @@ -0,0 +1,3 @@ +``disable-next`` is now correctly scoped to only the succeeding line. + +Closes #7401 diff --git a/pylint/checkers/variables.py b/pylint/checkers/variables.py index 74b8c4c706..0644b4d07d 100644 --- a/pylint/checkers/variables.py +++ b/pylint/checkers/variables.py @@ -2209,9 +2209,8 @@ def _loopvar_name(self, node: astroid.Name) -> None: # scope lookup rules would need to be changed to return the initial # assignment (which does not exist in code per se) as well as any later # modifications. - # pylint: disable-next=too-many-boolean-expressions if ( - not astmts + not astmts # pylint: disable=too-many-boolean-expressions or ( astmts[0].parent == astmts[0].root() and astmts[0].parent.parent_of(node) diff --git a/pylint/lint/message_state_handler.py b/pylint/lint/message_state_handler.py index 8415c88544..4cd40e276d 100644 --- a/pylint/lint/message_state_handler.py +++ b/pylint/lint/message_state_handler.py @@ -70,10 +70,10 @@ def _set_one_msg_status( self, scope: str, msg: MessageDefinition, line: int | None, enable: bool ) -> None: """Set the status of an individual message.""" - if scope == "module": + if scope in {"module", "line"}: assert isinstance(line, int) # should always be int inside module scope - self.linter.file_state.set_msg_status(msg, line, enable) + self.linter.file_state.set_msg_status(msg, line, enable, scope) if not enable and msg.symbol != "locally-disabled": self.linter.add_message( "locally-disabled", line=line, args=(msg.symbol, msg.msgid) @@ -143,7 +143,7 @@ def _set_msg_status( ignore_unknown: bool = False, ) -> None: """Do some tests and then iterate over message definitions to set state.""" - assert scope in {"package", "module"} + assert scope in {"package", "module", "line"} message_definitions = self._get_messages_to_set(msgid, enable, ignore_unknown) @@ -197,7 +197,7 @@ def disable( def disable_next( self, msgid: str, - scope: str = "package", + _: str = "package", line: int | None = None, ignore_unknown: bool = False, ) -> None: @@ -207,7 +207,7 @@ def disable_next( self._set_msg_status( msgid, enable=False, - scope=scope, + scope="line", line=line + 1, ignore_unknown=ignore_unknown, ) diff --git a/pylint/utils/file_state.py b/pylint/utils/file_state.py index acd59d6488..9624174ad8 100644 --- a/pylint/utils/file_state.py +++ b/pylint/utils/file_state.py @@ -194,30 +194,50 @@ def _set_message_state_in_block( state = lines[line] original_lineno = line - # Update suppression mapping - if not state: - self._suppression_mapping[(msg.msgid, line)] = original_lineno - else: - self._suppression_mapping.pop((msg.msgid, line), None) + self._set_message_state_on_line(msg, line, state, original_lineno) - # Update message state for respective line - try: - self._module_msgs_state[msg.msgid][line] = state - except KeyError: - self._module_msgs_state[msg.msgid] = {line: state} del lines[lineno] - def set_msg_status(self, msg: MessageDefinition, line: int, status: bool) -> None: + def _set_message_state_on_line( + self, + msg: MessageDefinition, + line: int, + state: bool, + original_lineno: int, + ) -> None: + """Set the state of a message on a line.""" + # Update suppression mapping + if not state: + self._suppression_mapping[(msg.msgid, line)] = original_lineno + else: + self._suppression_mapping.pop((msg.msgid, line), None) + + # Update message state for respective line + try: + self._module_msgs_state[msg.msgid][line] = state + except KeyError: + self._module_msgs_state[msg.msgid] = {line: state} + + def set_msg_status( + self, + msg: MessageDefinition, + line: int, + status: bool, + scope: str = "package", + ) -> None: """Set status (enabled/disable) for a given message at a given line.""" assert line > 0 assert self._module # TODO: 3.0: Remove unnecessary assertion assert self._msgs_store - # Expand the status to cover all relevant block lines - self._set_state_on_block_lines( - self._msgs_store, self._module, msg, {line: status} - ) + if scope != "line": + # Expand the status to cover all relevant block lines + self._set_state_on_block_lines( + self._msgs_store, self._module, msg, {line: status} + ) + else: + self._set_message_state_on_line(msg, line, status, line) # Store the raw value try: diff --git a/tests/functional/d/disable_msg_next_line.py b/tests/functional/d/disable_msg_next_line.py index f500feb1ea..ea283a4276 100644 --- a/tests/functional/d/disable_msg_next_line.py +++ b/tests/functional/d/disable_msg_next_line.py @@ -18,3 +18,10 @@ def function_C(): def function_D(arg1, arg2): # [unused-argument, invalid-name] return arg1 + + +def function_E(): # [invalid-name] + # pylint: disable-next=unused-variable + + test = 43 # [unused-variable] + blah = 123 # [unused-variable] diff --git a/tests/functional/d/disable_msg_next_line.txt b/tests/functional/d/disable_msg_next_line.txt index 36ba9527d2..794cfbb98d 100644 --- a/tests/functional/d/disable_msg_next_line.txt +++ b/tests/functional/d/disable_msg_next_line.txt @@ -3,3 +3,6 @@ unused-variable:15:4:15:5:function_C:Unused variable 'x':UNDEFINED f-string-without-interpolation:16:11:16:44:function_C:Using an f-string that does not have any interpolated variables:UNDEFINED invalid-name:19:0:19:14:function_D:"Function name ""function_D"" doesn't conform to snake_case naming style":HIGH unused-argument:19:21:19:25:function_D:Unused argument 'arg2':HIGH +invalid-name:23:0:23:14:function_E:"Function name ""function_E"" doesn't conform to snake_case naming style":HIGH +unused-variable:26:4:26:8:function_E:Unused variable 'test':UNDEFINED +unused-variable:27:4:27:8:function_E:Unused variable 'blah':UNDEFINED From a21af6a7971f70a8cbd5e89acce213c17c75e813 Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Tue, 6 Sep 2022 12:44:37 +0200 Subject: [PATCH 08/13] Upgrade astroid version following 2.12.8 release --- .github/workflows/changelog.yml | 2 +- .github/workflows/checks.yaml | 2 +- .github/workflows/primer-test.yaml | 2 +- .github/workflows/primer_comment.yaml | 2 +- .github/workflows/primer_run_main.yaml | 2 +- .github/workflows/primer_run_pr.yaml | 2 +- .github/workflows/tests.yaml | 2 +- pyproject.toml | 2 +- requirements_test_min.txt | 4 ++-- 9 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml index 1cdfee9599..8139f6d6e7 100644 --- a/.github/workflows/changelog.yml +++ b/.github/workflows/changelog.yml @@ -6,7 +6,7 @@ on: env: # Also change CACHE_VERSION in the other workflows - CACHE_VERSION: 22 + CACHE_VERSION: 25 DEFAULT_PYTHON: "3.10" jobs: diff --git a/.github/workflows/checks.yaml b/.github/workflows/checks.yaml index 1fb6dde842..f3e5233e83 100644 --- a/.github/workflows/checks.yaml +++ b/.github/workflows/checks.yaml @@ -8,7 +8,7 @@ on: pull_request: ~ env: - CACHE_VERSION: 22 + CACHE_VERSION: 25 DEFAULT_PYTHON: "3.10" PRE_COMMIT_CACHE: ~/.cache/pre-commit diff --git a/.github/workflows/primer-test.yaml b/.github/workflows/primer-test.yaml index 145fd70ca1..ae30d45d02 100644 --- a/.github/workflows/primer-test.yaml +++ b/.github/workflows/primer-test.yaml @@ -13,7 +13,7 @@ on: - ".github/workflows/primer-test.yaml" env: - CACHE_VERSION: 22 + CACHE_VERSION: 25 concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} diff --git a/.github/workflows/primer_comment.yaml b/.github/workflows/primer_comment.yaml index 21a6bf6bfc..f4a145c23a 100644 --- a/.github/workflows/primer_comment.yaml +++ b/.github/workflows/primer_comment.yaml @@ -14,7 +14,7 @@ on: env: # This needs to be the SAME as in the Main and PR job - CACHE_VERSION: 22 + CACHE_VERSION: 25 permissions: contents: read diff --git a/.github/workflows/primer_run_main.yaml b/.github/workflows/primer_run_main.yaml index bf719ec677..4e5169d3b6 100644 --- a/.github/workflows/primer_run_main.yaml +++ b/.github/workflows/primer_run_main.yaml @@ -16,7 +16,7 @@ concurrency: env: # This needs to be the SAME as in the PR and comment job - CACHE_VERSION: 22 + CACHE_VERSION: 25 jobs: run-primer: diff --git a/.github/workflows/primer_run_pr.yaml b/.github/workflows/primer_run_pr.yaml index b55c57c005..0fb31919c4 100644 --- a/.github/workflows/primer_run_pr.yaml +++ b/.github/workflows/primer_run_pr.yaml @@ -25,7 +25,7 @@ concurrency: env: # This needs to be the SAME as in the Main and comment job - CACHE_VERSION: 22 + CACHE_VERSION: 25 jobs: run-primer: diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index e35a642aac..dc55796460 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -10,7 +10,7 @@ on: - doc/data/messages/** env: - CACHE_VERSION: 22 + CACHE_VERSION: 25 jobs: tests-linux: diff --git a/pyproject.toml b/pyproject.toml index 4870201f53..7650c7f118 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,7 +37,7 @@ dependencies = [ # Also upgrade requirements_test_min.txt if you are bumping astroid. # Pinned to dev of second minor update to allow editable installs and fix primer issues, # see https://github.com/PyCQA/astroid/issues/1341 - "astroid>=2.12.4,<=2.14.0-dev0", + "astroid>=2.12.8,<=2.14.0-dev0", "isort>=4.2.5,<6", "mccabe>=0.6,<0.8", "tomli>=1.1.0;python_version<'3.11'", diff --git a/requirements_test_min.txt b/requirements_test_min.txt index 4c8946c783..e3f671d73b 100644 --- a/requirements_test_min.txt +++ b/requirements_test_min.txt @@ -1,9 +1,9 @@ -e .[testutils,spelling] # astroid dependency is also defined in pyproject.toml -astroid==2.12.4 # Pinned to a specific version for tests +astroid==2.12.8 # Pinned to a specific version for tests typing-extensions~=4.3 pytest~=7.1 pytest-benchmark~=3.4 pytest-timeout~=2.1 -towncrier~=21.9 +towncrier~=22.8 requests From 262723aaf93798560010fceb254b07fb0899ed0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20Noord?= <13665637+DanielNoord@users.noreply.github.com> Date: Tue, 6 Sep 2022 12:37:13 +0200 Subject: [PATCH 09/13] Make ``missing-yield/raises-doc`` respect ``no-docstring-rgx`` option --- doc/whatsnew/fragments/4743.bugfix | 3 ++- pylint/extensions/docparams.py | 10 ++++++++++ .../ext/docparams/raise/missing_raises_doc_required.py | 7 +++++++ .../ext/docparams/yield/missing_yield_doc_required.py | 8 ++++++++ .../ext/docparams/yield/missing_yield_doc_required.txt | 4 ++-- 5 files changed, 29 insertions(+), 3 deletions(-) diff --git a/doc/whatsnew/fragments/4743.bugfix b/doc/whatsnew/fragments/4743.bugfix index 20dbe071dd..1f8c30f1a7 100644 --- a/doc/whatsnew/fragments/4743.bugfix +++ b/doc/whatsnew/fragments/4743.bugfix @@ -1,3 +1,4 @@ -``missing-return-doc`` now respects the ``no-docstring-rgx`` option. +``missing-return-doc``, ``missing-raises-doc`` and ``missing-yields-doc`` now respect +the ``no-docstring-rgx`` option. Closes #4743 diff --git a/pylint/extensions/docparams.py b/pylint/extensions/docparams.py index 01e85b491c..19d9cf8904 100644 --- a/pylint/extensions/docparams.py +++ b/pylint/extensions/docparams.py @@ -278,6 +278,11 @@ def visit_raise(self, node: nodes.Raise) -> None: if not isinstance(func_node, astroid.FunctionDef): return + # skip functions that match the 'no-docstring-rgx' config option + no_docstring_rgx = self.linter.config.no_docstring_rgx + if no_docstring_rgx and re.match(no_docstring_rgx, func_node.name): + return + expected_excs = utils.possible_exc_types(node) if not expected_excs: @@ -355,6 +360,11 @@ def visit_yield(self, node: nodes.Yield | nodes.YieldFrom) -> None: if not isinstance(func_node, astroid.FunctionDef): return + # skip functions that match the 'no-docstring-rgx' config option + no_docstring_rgx = self.linter.config.no_docstring_rgx + if no_docstring_rgx and re.match(no_docstring_rgx, func_node.name): + return + doc = utils.docstringify( func_node.doc_node, self.linter.config.default_docstring_type ) diff --git a/tests/functional/ext/docparams/raise/missing_raises_doc_required.py b/tests/functional/ext/docparams/raise/missing_raises_doc_required.py index 860a3ecf55..96f2297a28 100644 --- a/tests/functional/ext/docparams/raise/missing_raises_doc_required.py +++ b/tests/functional/ext/docparams/raise/missing_raises_doc_required.py @@ -6,3 +6,10 @@ def test_warns_unknown_style(self): # [missing-raises-doc] """This is a docstring.""" raise RuntimeError("hi") + + +# This function doesn't require a docstring, because its name starts +# with an '_' (no-docstring-rgx): +def _function(some_arg: int): + """This is a docstring.""" + raise ValueError diff --git a/tests/functional/ext/docparams/yield/missing_yield_doc_required.py b/tests/functional/ext/docparams/yield/missing_yield_doc_required.py index e307063b36..bfaae4f03c 100644 --- a/tests/functional/ext/docparams/yield/missing_yield_doc_required.py +++ b/tests/functional/ext/docparams/yield/missing_yield_doc_required.py @@ -1,7 +1,15 @@ """Tests for missing-yield-doc and missing-yield-type-doc with accept-no-yields-doc = no""" # pylint: disable=missing-function-docstring, unused-argument, function-redefined +from typing import Iterator + # Test missing docstring def my_func(self): # [missing-yield-doc, missing-yield-type-doc] yield False + + +# This function doesn't require a docstring, because its name starts +# with an '_' (no-docstring-rgx): +def _function(some_arg: int) -> Iterator[int]: + yield some_arg diff --git a/tests/functional/ext/docparams/yield/missing_yield_doc_required.txt b/tests/functional/ext/docparams/yield/missing_yield_doc_required.txt index cf39e4031c..c7bd4b0333 100644 --- a/tests/functional/ext/docparams/yield/missing_yield_doc_required.txt +++ b/tests/functional/ext/docparams/yield/missing_yield_doc_required.txt @@ -1,2 +1,2 @@ -missing-yield-doc:6:0:6:11:my_func:Missing yield documentation:HIGH -missing-yield-type-doc:6:0:6:11:my_func:Missing yield type documentation:HIGH +missing-yield-doc:8:0:8:11:my_func:Missing yield documentation:HIGH +missing-yield-type-doc:8:0:8:11:my_func:Missing yield type documentation:HIGH From 5b85ecc72663edff1d5f3b5d7e1478a041859c11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20Noord?= <13665637+DanielNoord@users.noreply.github.com> Date: Tue, 6 Sep 2022 22:03:32 +0200 Subject: [PATCH 10/13] Suppress ``OSError`` in config file discovery (#7423) --- doc/whatsnew/fragments/7169.bugfix | 3 ++ pylint/config/find_default_config_files.py | 52 +++++++++++++++---- .../config/test_find_default_config_files.py | 9 ++++ 3 files changed, 54 insertions(+), 10 deletions(-) create mode 100644 doc/whatsnew/fragments/7169.bugfix diff --git a/doc/whatsnew/fragments/7169.bugfix b/doc/whatsnew/fragments/7169.bugfix new file mode 100644 index 0000000000..6ddf1a498e --- /dev/null +++ b/doc/whatsnew/fragments/7169.bugfix @@ -0,0 +1,3 @@ +Don't crash on ``OSError`` in config file discovery. + +Closes #7169 diff --git a/pylint/config/find_default_config_files.py b/pylint/config/find_default_config_files.py index 36917a380c..314e70e110 100644 --- a/pylint/config/find_default_config_files.py +++ b/pylint/config/find_default_config_files.py @@ -39,17 +39,26 @@ def _cfg_has_config(path: Path | str) -> bool: return any(section.startswith("pylint.") for section in parser.sections()) -def find_default_config_files() -> Iterator[Path]: - """Find all possible config files.""" +def _yield_default_files() -> Iterator[Path]: + """Iterate over the default config file names and see if they exist.""" for config_name in CONFIG_NAMES: - if config_name.is_file(): - if config_name.suffix == ".toml" and not _toml_has_config(config_name): - continue - if config_name.suffix == ".cfg" and not _cfg_has_config(config_name): - continue + try: + if config_name.is_file(): + if config_name.suffix == ".toml" and not _toml_has_config(config_name): + continue + if config_name.suffix == ".cfg" and not _cfg_has_config(config_name): + continue + + yield config_name.resolve() + except OSError: + pass + - yield config_name.resolve() +def _find_project_config() -> Iterator[Path]: + """Traverse up the directory tree to find a config file. + Stop if no '__init__' is found and thus we are no longer in a package. + """ if Path("__init__.py").is_file(): curdir = Path(os.getcwd()).resolve() while (curdir / "__init__.py").is_file(): @@ -59,6 +68,9 @@ def find_default_config_files() -> Iterator[Path]: if rc_path.is_file(): yield rc_path.resolve() + +def _find_config_in_home_or_environment() -> Iterator[Path]: + """Find a config file in the specified environment var or the home directory.""" if "PYLINTRC" in os.environ and Path(os.environ["PYLINTRC"]).exists(): if Path(os.environ["PYLINTRC"]).is_file(): yield Path(os.environ["PYLINTRC"]).resolve() @@ -68,16 +80,36 @@ def find_default_config_files() -> Iterator[Path]: except RuntimeError: # If the home directory does not exist a RuntimeError will be raised user_home = None + if user_home is not None and str(user_home) not in ("~", "/root"): home_rc = user_home / ".pylintrc" if home_rc.is_file(): yield home_rc.resolve() + home_rc = user_home / ".config" / "pylintrc" if home_rc.is_file(): yield home_rc.resolve() - if os.path.isfile("/etc/pylintrc"): - yield Path("/etc/pylintrc").resolve() + +def find_default_config_files() -> Iterator[Path]: + """Find all possible config files.""" + yield from _yield_default_files() + + try: + yield from _find_project_config() + except OSError: + pass + + try: + yield from _find_config_in_home_or_environment() + except OSError: + pass + + try: + if os.path.isfile("/etc/pylintrc"): + yield Path("/etc/pylintrc").resolve() + except OSError: + pass def find_pylintrc() -> str | None: diff --git a/tests/config/test_find_default_config_files.py b/tests/config/test_find_default_config_files.py index 9addb6db2e..10484be1d6 100644 --- a/tests/config/test_find_default_config_files.py +++ b/tests/config/test_find_default_config_files.py @@ -253,3 +253,12 @@ def test_non_existent_home() -> None: assert not list(config.find_default_config_files()) os.chdir(current_dir) + + +def test_permission_error() -> None: + """Test that we handle PermissionError correctly in find_default_config_files. + + Reported in https://github.com/PyCQA/pylint/issues/7169. + """ + with mock.patch("pathlib.Path.is_file", side_effect=PermissionError): + list(config.find_default_config_files()) From e63a35207e447a1e714893a5b803706a218e1f99 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 27 Aug 2022 14:30:10 +0200 Subject: [PATCH 11/13] Fix 2.15 changelog (#7369) --- doc/whatsnew/2/2.15/index.rst | 4 ++++ doc/whatsnew/fragments/7322.false-positive | 4 ---- 2 files changed, 4 insertions(+), 4 deletions(-) delete mode 100644 doc/whatsnew/fragments/7322.false-positive diff --git a/doc/whatsnew/2/2.15/index.rst b/doc/whatsnew/2/2.15/index.rst index 6f9b0dbff4..420258c5ab 100644 --- a/doc/whatsnew/2/2.15/index.rst +++ b/doc/whatsnew/2/2.15/index.rst @@ -72,6 +72,10 @@ False Positives Fixed - Fix `undefined-loop-variable` with `break` and `continue` statements in `else` blocks. Refs #7311 (`#7311 `_) +- Improve default TypeVar name regex. Disallow names prefixed with ``T``. + E.g. use ``AnyStrT`` instead of ``TAnyStr``. + + Refs #7322 (`#7322 `_`) False Negatives Fixed diff --git a/doc/whatsnew/fragments/7322.false-positive b/doc/whatsnew/fragments/7322.false-positive deleted file mode 100644 index 0c196ada0d..0000000000 --- a/doc/whatsnew/fragments/7322.false-positive +++ /dev/null @@ -1,4 +0,0 @@ -Improve default TypeVar name regex. Disallow names prefixed with ``T``. -E.g. use ``AnyStrT`` instead of ``TAnyStr``. - -Refs #7322 From de613c2a08af8e83ac2c9d93acba224d461eb837 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20Noord?= <13665637+DanielNoord@users.noreply.github.com> Date: Sun, 4 Sep 2022 22:04:18 +0200 Subject: [PATCH 12/13] Fix and refactors for ``docparams`` extension (#7398) * Fix and refactors for ``docparams`` extension The ``re_only_desc`` regex did not match for white and characters after ``\n``, so some description-only lines weren't getting matched. In addition, lookaheads were added to ``re_param_line`` (i.e. make sure the type group is not followed by a new line (``\n``)). Lastly, named groups (ala Perl regular expressions) were added for slightly improved clarity. Co-authored-by: Hendry, Adam --- doc/whatsnew/fragments/7398.other | 4 ++ pylint/extensions/_check_docs_utils.py | 40 +++++++++++++------ .../ext/docparams/missing_param_doc.py | 6 +-- .../ext/docparams/missing_param_doc.txt | 9 ++--- .../missing_param_doc_required_Numpy.py | 16 ++++++++ 5 files changed, 53 insertions(+), 22 deletions(-) create mode 100644 doc/whatsnew/fragments/7398.other diff --git a/doc/whatsnew/fragments/7398.other b/doc/whatsnew/fragments/7398.other new file mode 100644 index 0000000000..e83974ccfc --- /dev/null +++ b/doc/whatsnew/fragments/7398.other @@ -0,0 +1,4 @@ +The ``docparams`` extension now considers typing in Numpy style docstrings +as "documentation" for the ``missing-param-doc`` message. + +Refs #7398 diff --git a/pylint/extensions/_check_docs_utils.py b/pylint/extensions/_check_docs_utils.py index 747fcb4ea0..d8f797b6c5 100644 --- a/pylint/extensions/_check_docs_utils.py +++ b/pylint/extensions/_check_docs_utils.py @@ -247,7 +247,7 @@ class SphinxDocstring(Docstring): re_multiple_simple_type = r""" (?:{container_type}|{type}) - (?:(?:\s+(?:of|or)\s+|\s*,\s*)(?:{container_type}|{type}))* + (?:(?:\s+(?:of|or)\s+|\s*,\s*|\s+\|\s+)(?:{container_type}|{type}))* """.format( type=re_type, container_type=re_simple_container_type ) @@ -449,7 +449,7 @@ class GoogleDocstring(Docstring): re_multiple_type = r""" (?:{container_type}|{type}|{xref}) - (?:(?:\s+(?:of|or)\s+|\s*,\s*)(?:{container_type}|{type}|{xref}))* + (?:(?:\s+(?:of|or)\s+|\s*,\s*|\s+\|\s+)(?:{container_type}|{type}|{xref}))* """.format( type=re_type, xref=re_xref, container_type=re_container_type ) @@ -728,13 +728,13 @@ class NumpyDocstring(GoogleDocstring): re.X | re.S | re.M, ) - re_default_value = r"""((['"]\w+\s*['"])|(True)|(False)|(None))""" + re_default_value = r"""((['"]\w+\s*['"])|(\d+)|(True)|(False)|(None))""" re_param_line = re.compile( rf""" - \s* (\*{{0,2}}\w+)(\s?(:|\n)) # identifier with potential asterisks + \s* (?P\*{{0,2}}\w+)(\s?(:|\n)) # identifier with potential asterisks \s* - ( + (?P ( ({GoogleDocstring.re_multiple_type}) # default type declaration (,\s+optional)? # optional 'optional' indication @@ -742,8 +742,11 @@ class NumpyDocstring(GoogleDocstring): ( {{({re_default_value},?\s*)+}} # set of default values )? - \n)? - \s* (.*) # optional description + (?:$|\n) + )? + ( + \s* (?P.*) # optional description + )? """, re.X | re.S, ) @@ -794,15 +797,26 @@ def match_param_docs(self) -> tuple[set[str], set[str]]: continue # check if parameter has description only - re_only_desc = re.match(r"\s* (\*{0,2}\w+)\s*:?\n", entry) + re_only_desc = re.match(r"\s*(\*{0,2}\w+)\s*:?\n\s*\w*$", entry) if re_only_desc: - param_name = match.group(1) - param_desc = match.group(2) + param_name = match.group("param_name") + param_desc = match.group("param_type") param_type = None else: - param_name = match.group(1) - param_type = match.group(3) - param_desc = match.group(4) + param_name = match.group("param_name") + param_type = match.group("param_type") + param_desc = match.group("param_desc") + # The re_param_line pattern needs to match multi-line which removes the ability + # to match a single line description like 'arg : a number type.' + # We are not trying to determine whether 'a number type' is correct typing + # but we do accept it as typing as it is in the place where typing + # should be + if param_type is None and re.match(r"\s*(\*{0,2}\w+)\s*:.+$", entry): + param_type = param_desc + # If the description is "" but we have a type description + # we consider the description to be the type + if not param_desc and param_type: + param_desc = param_type if param_type: params_with_type.add(param_name) diff --git a/tests/functional/ext/docparams/missing_param_doc.py b/tests/functional/ext/docparams/missing_param_doc.py index 75ca394129..e72507a785 100644 --- a/tests/functional/ext/docparams/missing_param_doc.py +++ b/tests/functional/ext/docparams/missing_param_doc.py @@ -30,7 +30,7 @@ def foobar4(arg1, arg2): #[missing-param-doc, missing-type-doc] """ print(arg1, arg2) -def foobar5(arg1, arg2): #[missing-param-doc, missing-type-doc] +def foobar5(arg1, arg2): #[missing-type-doc] """function foobar ... Parameters ---------- @@ -63,7 +63,7 @@ def foobar8(arg1): #[missing-any-param-doc] print(arg1) -def foobar9(arg1, arg2, arg3): #[missing-param-doc] +def foobar9(arg1, arg2, arg3): """function foobar ... Parameters ---------- @@ -73,7 +73,7 @@ def foobar9(arg1, arg2, arg3): #[missing-param-doc] """ print(arg1, arg2, arg3) -def foobar10(arg1, arg2, arg3): #[missing-param-doc, missing-type-doc] +def foobar10(arg1, arg2, arg3): #[missing-type-doc] """function foobar ... Parameters ---------- diff --git a/tests/functional/ext/docparams/missing_param_doc.txt b/tests/functional/ext/docparams/missing_param_doc.txt index 124392d059..fdf4da93f4 100644 --- a/tests/functional/ext/docparams/missing_param_doc.txt +++ b/tests/functional/ext/docparams/missing_param_doc.txt @@ -1,18 +1,15 @@ missing-any-param-doc:3:0:3:11:foobar1:"Missing any documentation in ""foobar1""":HIGH missing-any-param-doc:8:0:8:11:foobar2:"Missing any documentation in ""foobar2""":HIGH -missing-param-doc:15:0:15:11:foobar3:"""arg1, arg2, arg3"" missing in parameter documentation":HIGH +missing-param-doc:15:0:15:11:foobar3:"""arg2"" missing in parameter documentation":HIGH missing-type-doc:15:0:15:11:foobar3:"""arg2"" missing in parameter type documentation":HIGH missing-param-doc:24:0:24:11:foobar4:"""arg2"" missing in parameter documentation":HIGH missing-type-doc:24:0:24:11:foobar4:"""arg2"" missing in parameter type documentation":HIGH -missing-param-doc:33:0:33:11:foobar5:"""arg2"" missing in parameter documentation":HIGH missing-type-doc:33:0:33:11:foobar5:"""arg1"" missing in parameter type documentation":HIGH -missing-param-doc:43:0:43:11:foobar6:"""arg2, arg3"" missing in parameter documentation":HIGH +missing-param-doc:43:0:43:11:foobar6:"""arg3"" missing in parameter documentation":HIGH missing-type-doc:43:0:43:11:foobar6:"""arg3"" missing in parameter type documentation":HIGH missing-any-param-doc:53:0:53:11:foobar7:"Missing any documentation in ""foobar7""":HIGH missing-any-param-doc:61:0:61:11:foobar8:"Missing any documentation in ""foobar8""":HIGH -missing-param-doc:66:0:66:11:foobar9:"""arg1, arg2, arg3"" missing in parameter documentation":HIGH -missing-param-doc:76:0:76:12:foobar10:"""arg2"" missing in parameter documentation":HIGH missing-type-doc:76:0:76:12:foobar10:"""arg1, arg3"" missing in parameter type documentation":HIGH missing-any-param-doc:88:0:88:12:foobar11:"Missing any documentation in ""foobar11""":HIGH -missing-param-doc:97:0:97:12:foobar12:"""arg1, arg3"" missing in parameter documentation":HIGH +missing-param-doc:97:0:97:12:foobar12:"""arg3"" missing in parameter documentation":HIGH missing-type-doc:97:0:97:12:foobar12:"""arg2, arg3"" missing in parameter type documentation":HIGH diff --git a/tests/functional/ext/docparams/parameter/missing_param_doc_required_Numpy.py b/tests/functional/ext/docparams/parameter/missing_param_doc_required_Numpy.py index 6e725980fb..5626ad385b 100644 --- a/tests/functional/ext/docparams/parameter/missing_param_doc_required_Numpy.py +++ b/tests/functional/ext/docparams/parameter/missing_param_doc_required_Numpy.py @@ -392,6 +392,7 @@ def test_ignores_optional_specifier_numpy(param, param2="all"): """ return param, param2 + def test_with_list_of_default_values(arg, option, option2): """Reported in https://github.com/PyCQA/pylint/issues/4035. @@ -406,3 +407,18 @@ def test_with_list_of_default_values(arg, option, option2): """ return arg, option, option2 + + +def test_with_descriptions_instead_of_typing(arg, axis, option): + """We choose to accept description in place of typing as well. + + See: https://github.com/PyCQA/pylint/pull/7398. + + Parameters + ---------- + arg : a number type. + axis : int or None + option : {"y", "n"} + Do I do it? + """ + return arg, option From c5aefa2d11850e7ef099d15439916241f1707df8 Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Tue, 6 Sep 2022 22:20:01 +0200 Subject: [PATCH 13/13] Bump pylint to 2.15.1, update changelog --- CONTRIBUTORS.txt | 3 ++ doc/whatsnew/2/2.15/index.rst | 44 ++++++++++++++++++++++ doc/whatsnew/fragments/3299.false_positive | 3 -- doc/whatsnew/fragments/4354.bugfix | 3 -- doc/whatsnew/fragments/4743.bugfix | 4 -- doc/whatsnew/fragments/6592.false_positive | 3 -- doc/whatsnew/fragments/7169.bugfix | 3 -- doc/whatsnew/fragments/7368.false_positive | 3 -- doc/whatsnew/fragments/7380.bugfix | 3 -- doc/whatsnew/fragments/7398.other | 4 -- doc/whatsnew/fragments/7401.bugfix | 3 -- 11 files changed, 47 insertions(+), 29 deletions(-) delete mode 100644 doc/whatsnew/fragments/3299.false_positive delete mode 100644 doc/whatsnew/fragments/4354.bugfix delete mode 100644 doc/whatsnew/fragments/4743.bugfix delete mode 100644 doc/whatsnew/fragments/6592.false_positive delete mode 100644 doc/whatsnew/fragments/7169.bugfix delete mode 100644 doc/whatsnew/fragments/7368.false_positive delete mode 100644 doc/whatsnew/fragments/7380.bugfix delete mode 100644 doc/whatsnew/fragments/7398.other delete mode 100644 doc/whatsnew/fragments/7401.bugfix diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 4fc6434d01..d885f555b6 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -374,6 +374,7 @@ contributors: - Ry4an Brase - Ruro - Roman Ivanov +- Rogdham - Robert Schweizer - Reverb Chu - Renat Galimov @@ -422,6 +423,7 @@ contributors: - Louis Sautier - Lorena Buciu <46202743+lorena-b@users.noreply.github.com> - Logan Miller <14319179+komodo472@users.noreply.github.com> +- Levi Gruspe - Kári Tristan Helgason - Kurian Benoy <70306694+kurianbenoy-aot@users.noreply.github.com> - Krzysztof Czapla @@ -500,6 +502,7 @@ contributors: - Craig Citro - Clément Pit-Claudel - Christopher Zurcher +- Christoph Blessing <33834216+cblessing24@users.noreply.github.com> - Carl Crowder : don't evaluate the value of arguments for 'dangerous-default-value' - Carey Metcalfe : demoted `try-except-raise` from error to warning - Cameron Olechowski diff --git a/doc/whatsnew/2/2.15/index.rst b/doc/whatsnew/2/2.15/index.rst index 420258c5ab..7132e70564 100644 --- a/doc/whatsnew/2/2.15/index.rst +++ b/doc/whatsnew/2/2.15/index.rst @@ -29,6 +29,50 @@ Marc Byrne became a maintainer, welcome to the team ! .. towncrier release notes start + +What's new in Pylint 2.15.1? +---------------------------- +Release date: 2022-09-06 + +- Fix ``used-before-assignment`` for functions/classes defined in type checking guard. + + Closes #7368 (`#7368 `_) +- Update ``modified_iterating`` checker to fix a crash with ``for`` loops on empty list. + + Closes #7380 (`#7380 `_) +- The ``docparams`` extension now considers typing in Numpy style docstrings + as "documentation" for the ``missing-param-doc`` message. + + Refs #7398 (`#7398 `_) +- Fix false positive for ``unused-variable`` and ``unused-import`` when a name is only used in a string literal type annotation. + + Closes #3299 (`#3299 `_) +- Fix false positive for ``too-many-function-args`` when a function call is assigned to a class attribute inside the class where the function is defined. + + Closes #6592 (`#6592 `_) +- Fix ``used-before-assignment`` for functions/classes defined in type checking guard. + + Closes #7368 (`#7368 `_) +- Fix ignored files being linted when passed on stdin. + + Closes #4354 (`#4354 `_) +- ``missing-return-doc``, ``missing-raises-doc`` and ``missing-yields-doc`` now respect + the ``no-docstring-rgx`` option. + + Closes #4743 (`#4743 `_) +- Don't crash on ``OSError`` in config file discovery. + + Closes #7169 (`#7169 `_) +- ``disable-next`` is now correctly scoped to only the succeeding line. + + Closes #7401 (`#7401 `_) +- Update ``modified_iterating`` checker to fix a crash with ``for`` loops on empty list. + + Closes #7380 (`#7380 `_) + +What's new in Pylint 2.15.0? +---------------------------- + New Checks ---------- diff --git a/doc/whatsnew/fragments/3299.false_positive b/doc/whatsnew/fragments/3299.false_positive deleted file mode 100644 index b1e61c9313..0000000000 --- a/doc/whatsnew/fragments/3299.false_positive +++ /dev/null @@ -1,3 +0,0 @@ -Fix false positive for ``unused-variable`` and ``unused-import`` when a name is only used in a string literal type annotation. - -Closes #3299 diff --git a/doc/whatsnew/fragments/4354.bugfix b/doc/whatsnew/fragments/4354.bugfix deleted file mode 100644 index 09caf8d139..0000000000 --- a/doc/whatsnew/fragments/4354.bugfix +++ /dev/null @@ -1,3 +0,0 @@ -Fix ignored files being linted when passed on stdin. - -Closes #4354 diff --git a/doc/whatsnew/fragments/4743.bugfix b/doc/whatsnew/fragments/4743.bugfix deleted file mode 100644 index 1f8c30f1a7..0000000000 --- a/doc/whatsnew/fragments/4743.bugfix +++ /dev/null @@ -1,4 +0,0 @@ -``missing-return-doc``, ``missing-raises-doc`` and ``missing-yields-doc`` now respect -the ``no-docstring-rgx`` option. - -Closes #4743 diff --git a/doc/whatsnew/fragments/6592.false_positive b/doc/whatsnew/fragments/6592.false_positive deleted file mode 100644 index 846ddce961..0000000000 --- a/doc/whatsnew/fragments/6592.false_positive +++ /dev/null @@ -1,3 +0,0 @@ -Fix false positive for ``too-many-function-args`` when a function call is assigned to a class attribute inside the class where the function is defined. - -Closes #6592 diff --git a/doc/whatsnew/fragments/7169.bugfix b/doc/whatsnew/fragments/7169.bugfix deleted file mode 100644 index 6ddf1a498e..0000000000 --- a/doc/whatsnew/fragments/7169.bugfix +++ /dev/null @@ -1,3 +0,0 @@ -Don't crash on ``OSError`` in config file discovery. - -Closes #7169 diff --git a/doc/whatsnew/fragments/7368.false_positive b/doc/whatsnew/fragments/7368.false_positive deleted file mode 100644 index 4e9551a321..0000000000 --- a/doc/whatsnew/fragments/7368.false_positive +++ /dev/null @@ -1,3 +0,0 @@ -Fix ``used-before-assignment`` for functions/classes defined in type checking guard. - -Closes #7368 diff --git a/doc/whatsnew/fragments/7380.bugfix b/doc/whatsnew/fragments/7380.bugfix deleted file mode 100644 index dc5ea5fa6b..0000000000 --- a/doc/whatsnew/fragments/7380.bugfix +++ /dev/null @@ -1,3 +0,0 @@ -Update ``modified_iterating`` checker to fix a crash with ``for`` loops on empty list. - -Closes #7380 diff --git a/doc/whatsnew/fragments/7398.other b/doc/whatsnew/fragments/7398.other deleted file mode 100644 index e83974ccfc..0000000000 --- a/doc/whatsnew/fragments/7398.other +++ /dev/null @@ -1,4 +0,0 @@ -The ``docparams`` extension now considers typing in Numpy style docstrings -as "documentation" for the ``missing-param-doc`` message. - -Refs #7398 diff --git a/doc/whatsnew/fragments/7401.bugfix b/doc/whatsnew/fragments/7401.bugfix deleted file mode 100644 index 8b0f0e2a84..0000000000 --- a/doc/whatsnew/fragments/7401.bugfix +++ /dev/null @@ -1,3 +0,0 @@ -``disable-next`` is now correctly scoped to only the succeeding line. - -Closes #7401