From 224fe934f30ac0cbeb3bae81260008cc302402a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20Noord?= <13665637+DanielNoord@users.noreply.github.com> Date: Fri, 25 Mar 2022 14:34:41 +0100 Subject: [PATCH 01/45] Add basic typing to ``pylint/utils.py`` (#5977) --- pylint/checkers/__init__.py | 2 +- pylint/utils/utils.py | 18 ++++++++---------- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/pylint/checkers/__init__.py b/pylint/checkers/__init__.py index 2855d702938..fa14f5eea5e 100644 --- a/pylint/checkers/__init__.py +++ b/pylint/checkers/__init__.py @@ -67,7 +67,7 @@ def table_lines_from_stats( """ lines: List[str] = [] if stat_type == "duplicated_lines": - new: List[Tuple[str, Union[str, int, float]]] = [ + new: List[Tuple[str, Union[int, float]]] = [ ("nb_duplicated_lines", stats.duplicated_lines["nb_duplicated_lines"]), ( "percent_duplicated_lines", diff --git a/pylint/utils/utils.py b/pylint/utils/utils.py index 6a60cd8e961..bf2cb632df0 100644 --- a/pylint/utils/utils.py +++ b/pylint/utils/utils.py @@ -41,6 +41,7 @@ if TYPE_CHECKING: from pylint.checkers.base_checker import BaseChecker + from pylint.lint import PyLinter DEFAULT_LINE_LENGTH = 79 @@ -93,11 +94,11 @@ def normalize_text(text, line_len=DEFAULT_LINE_LENGTH, indent=""): # py3k has no more cmp builtin -def cmp(a, b): +def cmp(a: Union[int, float], b: Union[int, float]) -> int: return (a > b) - (a < b) -def diff_string(old, new): +def diff_string(old: Union[int, float], new: Union[int, float]) -> str: """Given an old and new int value, return a string representing the difference """ @@ -106,7 +107,7 @@ def diff_string(old, new): return diff_str -def get_module_and_frameid(node): +def get_module_and_frameid(node: nodes.NodeNG) -> Tuple[str, str]: """Return the module name and the frame id in the module.""" frame = node.frame(future=True) module, obj = "", [] @@ -166,7 +167,7 @@ def tokenize_module(node: nodes.Module) -> List[tokenize.TokenInfo]: return list(tokenize.tokenize(readline)) -def register_plugins(linter, directory): +def register_plugins(linter: "PyLinter", directory: str) -> None: """Load all module and package in the given directory, looking for a 'register' function in each one, used to register pylint checkers """ @@ -298,13 +299,10 @@ def _splitstrip(string, sep=","): return [word.strip() for word in string.split(sep) if word.strip()] -def _unquote(string): +def _unquote(string: str) -> str: """Remove optional quotes (simple or double) from the string. - :type string: str or unicode :param string: an optionally quoted string - - :rtype: str or unicode :return: the unquoted string (or the input string if it wasn't quoted) """ if not string: @@ -383,7 +381,7 @@ def _ini_format(stream: TextIO, options: List[Tuple]) -> None: class IsortDriver: """A wrapper around isort API that changed between versions 4 and 5.""" - def __init__(self, config): + def __init__(self, config) -> None: if HAS_ISORT_5: self.isort5_config = isort.api.Config( # There is no typo here. EXTRA_standard_library is @@ -400,7 +398,7 @@ def __init__(self, config): known_third_party=config.known_third_party, ) - def place_module(self, package): + def place_module(self, package: str) -> str: if HAS_ISORT_5: return isort.api.place_module(package, self.isort5_config) return self.isort4_obj.place_module(package) From 3366e0dba0c04eebf2f4376f82d91fc88e46e67e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20Noord?= <13665637+DanielNoord@users.noreply.github.com> Date: Fri, 25 Mar 2022 15:28:20 +0100 Subject: [PATCH 02/45] Add basic typing to ``pylint/reporters`` (#5978) --- pylint/reporters/multi_reporter.py | 18 ++++++++---------- pylint/reporters/reports_handler_mix_in.py | 5 +++-- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/pylint/reporters/multi_reporter.py b/pylint/reporters/multi_reporter.py index 43822f05c2b..855ab3c33d7 100644 --- a/pylint/reporters/multi_reporter.py +++ b/pylint/reporters/multi_reporter.py @@ -3,7 +3,7 @@ # Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt import os -from typing import IO, TYPE_CHECKING, Any, AnyStr, Callable, List, Optional +from typing import TYPE_CHECKING, Callable, List, Optional, TextIO from pylint.interfaces import IReporter from pylint.message import Message @@ -11,11 +11,9 @@ from pylint.utils import LinterStats if TYPE_CHECKING: + from pylint.lint import PyLinter from pylint.reporters.ureports.nodes import Section -AnyFile = IO[AnyStr] -PyLinter = Any - class MultiReporter: """Reports messages and layouts in plain text.""" @@ -34,21 +32,21 @@ def __init__( self, sub_reporters: List[BaseReporter], close_output_files: Callable[[], None], - output: Optional[AnyFile] = None, + output: Optional[TextIO] = None, ): self._sub_reporters = sub_reporters self.close_output_files = close_output_files self._path_strip_prefix = os.getcwd() + os.sep - self._linter: Optional[PyLinter] = None + self._linter: Optional["PyLinter"] = None self.out = output self.messages: List[Message] = [] @property - def out(self): + def out(self) -> Optional[TextIO]: return self.__out @out.setter - def out(self, output: Optional[AnyFile] = None): + def out(self, output: Optional[TextIO] = None) -> None: """MultiReporter doesn't have its own output. This method is only provided for API parity with BaseReporter @@ -66,11 +64,11 @@ def path_strip_prefix(self) -> str: return self._path_strip_prefix @property - def linter(self) -> Optional[PyLinter]: + def linter(self) -> Optional["PyLinter"]: return self._linter @linter.setter - def linter(self, value: PyLinter) -> None: + def linter(self, value: "PyLinter") -> None: self._linter = value for rep in self._sub_reporters: rep.linter = value diff --git a/pylint/reporters/reports_handler_mix_in.py b/pylint/reporters/reports_handler_mix_in.py index 890d58660d8..180513a7894 100644 --- a/pylint/reporters/reports_handler_mix_in.py +++ b/pylint/reporters/reports_handler_mix_in.py @@ -22,7 +22,8 @@ from pylint.checkers import BaseChecker from pylint.lint.pylinter import PyLinter -ReportsDict = DefaultDict["BaseChecker", List[Tuple[str, str, Callable]]] +ReportsCallable = Callable[[Section, LinterStats, Optional[LinterStats]], None] +ReportsDict = DefaultDict["BaseChecker", List[Tuple[str, str, ReportsCallable]]] class ReportsHandlerMixIn: @@ -39,7 +40,7 @@ def report_order(self) -> MutableSequence["BaseChecker"]: return list(self._reports) def register_report( - self, reportid: str, r_title: str, r_cb: Callable, checker: "BaseChecker" + self, reportid: str, r_title: str, r_cb: ReportsCallable, checker: "BaseChecker" ) -> None: """Register a report. From 737d45f78a3f2029bea08ce4ef5bc9473e665382 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20Noord?= <13665637+DanielNoord@users.noreply.github.com> Date: Fri, 25 Mar 2022 16:15:58 +0100 Subject: [PATCH 03/45] Final typing for ``pylint/reporters/ureports`` (#5979) Co-authored-by: Pierre Sassoulas --- pylint/reporters/ureports/base_writer.py | 12 +++++++++--- pylint/reporters/ureports/nodes.py | 20 ++++++++++++++------ pylint/reporters/ureports/text_writer.py | 2 +- 3 files changed, 24 insertions(+), 10 deletions(-) diff --git a/pylint/reporters/ureports/base_writer.py b/pylint/reporters/ureports/base_writer.py index 4f0ce55c819..0f0f6ee8a85 100644 --- a/pylint/reporters/ureports/base_writer.py +++ b/pylint/reporters/ureports/base_writer.py @@ -9,10 +9,11 @@ """ import sys from io import StringIO -from typing import TYPE_CHECKING, Iterator, List, TextIO, Union +from typing import TYPE_CHECKING, Iterator, List, Optional, TextIO, Union if TYPE_CHECKING: from pylint.reporters.ureports.nodes import ( + BaseLayout, EvaluationSection, Paragraph, Section, @@ -23,7 +24,12 @@ class BaseWriter: """Base class for ureport writers.""" - def format(self, layout, stream: TextIO = sys.stdout, encoding=None) -> None: + def format( + self, + layout: "BaseLayout", + stream: TextIO = sys.stdout, + encoding: Optional[str] = None, + ) -> None: """Format and write the given layout into the stream object. unicode policy: unicode strings may be found in the layout; @@ -79,7 +85,7 @@ def get_table_content(self, table: "Table") -> List[List[str]]: result[-1] += [""] * (cols - len(result[-1])) return result - def compute_content(self, layout) -> Iterator[str]: + def compute_content(self, layout: "BaseLayout") -> Iterator[str]: """Trick to compute the formatting of children layout before actually writing it diff --git a/pylint/reporters/ureports/nodes.py b/pylint/reporters/ureports/nodes.py index 543082ae9a9..f816f7a6262 100644 --- a/pylint/reporters/ureports/nodes.py +++ b/pylint/reporters/ureports/nodes.py @@ -6,9 +6,13 @@ A micro report is a tree of layout and content objects. """ -from typing import Any, Iterable, Iterator, List, Optional, Union +from typing import Any, Callable, Iterable, Iterator, List, Optional, TypeVar, Union -from pylint.reporters.ureports.text_writer import TextWriter +from pylint.reporters.ureports.base_writer import BaseWriter + +T = TypeVar("T") +VNodeT = TypeVar("VNodeT", bound="VNode") +VisitLeaveFunction = Callable[[T, Any, Any], None] class VNode: @@ -20,12 +24,16 @@ def __init__(self) -> None: def __iter__(self) -> Iterator["VNode"]: return iter(self.children) - def accept(self, visitor: TextWriter, *args: Any, **kwargs: Any) -> None: - func = getattr(visitor, f"visit_{self.visitor_name}") + def accept(self: VNodeT, visitor: BaseWriter, *args: Any, **kwargs: Any) -> None: + func: VisitLeaveFunction[VNodeT] = getattr( + visitor, f"visit_{self.visitor_name}" + ) return func(self, *args, **kwargs) - def leave(self, visitor, *args, **kwargs): - func = getattr(visitor, f"leave_{self.visitor_name}") + def leave(self: VNodeT, visitor: BaseWriter, *args: Any, **kwargs: Any) -> None: + func: VisitLeaveFunction[VNodeT] = getattr( + visitor, f"leave_{self.visitor_name}" + ) return func(self, *args, **kwargs) diff --git a/pylint/reporters/ureports/text_writer.py b/pylint/reporters/ureports/text_writer.py index fc4ce67df96..c6b35ee8545 100644 --- a/pylint/reporters/ureports/text_writer.py +++ b/pylint/reporters/ureports/text_writer.py @@ -28,7 +28,7 @@ class TextWriter(BaseWriter): (ReStructured inspiration but not totally handled yet) """ - def __init__(self): + def __init__(self) -> None: super().__init__() self.list_level = 0 From 8d21ab212789b5e11417bbb3486546f06c33cb0c Mon Sep 17 00:00:00 2001 From: Tim Martin Date: Fri, 25 Mar 2022 21:09:53 +0000 Subject: [PATCH 04/45] New checker `unnecessary-list-index-lookup` (#4525) (#5834) --- ChangeLog | 4 + .../u/unnecessary-list-index-lookup/bad.py | 4 + .../u/unnecessary-list-index-lookup/good.py | 4 + doc/whatsnew/2.14.rst | 5 ++ .../refactoring/refactoring_checker.py | 86 ++++++++++++++++++- .../c/consider/consider_using_enumerate.py | 2 +- .../unnecessary_list_index_lookup.py | 46 ++++++++++ .../unnecessary_list_index_lookup.txt | 3 + .../unnecessary_list_index_lookup_py38.py | 9 ++ .../unnecessary_list_index_lookup_py38.rc | 2 + 10 files changed, 163 insertions(+), 2 deletions(-) create mode 100644 doc/data/messages/u/unnecessary-list-index-lookup/bad.py create mode 100644 doc/data/messages/u/unnecessary-list-index-lookup/good.py create mode 100644 tests/functional/u/unnecessary/unnecessary_list_index_lookup.py create mode 100644 tests/functional/u/unnecessary/unnecessary_list_index_lookup.txt create mode 100644 tests/functional/u/unnecessary/unnecessary_list_index_lookup_py38.py create mode 100644 tests/functional/u/unnecessary/unnecessary_list_index_lookup_py38.rc diff --git a/ChangeLog b/ChangeLog index 9656ea62c1d..895325445e7 100644 --- a/ChangeLog +++ b/ChangeLog @@ -16,6 +16,10 @@ Release date: TBA Closes #578 +* Added new checker ``unnecessary-list-index-lookup`` for indexing into a list while + iterating over ``enumerate()``. + + Closes #4525 * Fix false negative for ``no-member`` when attempting to assign an instance attribute to itself without any prior assignment. diff --git a/doc/data/messages/u/unnecessary-list-index-lookup/bad.py b/doc/data/messages/u/unnecessary-list-index-lookup/bad.py new file mode 100644 index 00000000000..d312d36a034 --- /dev/null +++ b/doc/data/messages/u/unnecessary-list-index-lookup/bad.py @@ -0,0 +1,4 @@ +letters = ['a', 'b', 'c'] + +for index, letter in enumerate(letters): + print(letters[index]) # [unnecessary-list-index-lookup] diff --git a/doc/data/messages/u/unnecessary-list-index-lookup/good.py b/doc/data/messages/u/unnecessary-list-index-lookup/good.py new file mode 100644 index 00000000000..5d3370c0e2b --- /dev/null +++ b/doc/data/messages/u/unnecessary-list-index-lookup/good.py @@ -0,0 +1,4 @@ +letters = ['a', 'b', 'c'] + +for index, letter in enumerate(letters): + print(letter) diff --git a/doc/whatsnew/2.14.rst b/doc/whatsnew/2.14.rst index 610e06cb447..ad38200e7f1 100644 --- a/doc/whatsnew/2.14.rst +++ b/doc/whatsnew/2.14.rst @@ -18,6 +18,11 @@ New checkers Closes #578 +* Added new checker ``unnecessary-list-index-lookup`` for indexing into a list while + iterating over ``enumerate()``. + + Closes #4525 + Removed checkers ================ diff --git a/pylint/checkers/refactoring/refactoring_checker.py b/pylint/checkers/refactoring/refactoring_checker.py index 8dc65f129d4..e1bb443ed93 100644 --- a/pylint/checkers/refactoring/refactoring_checker.py +++ b/pylint/checkers/refactoring/refactoring_checker.py @@ -18,6 +18,7 @@ from pylint import utils as lint_utils from pylint.checkers import utils from pylint.checkers.utils import node_frame_class +from pylint.interfaces import HIGH if sys.version_info >= (3, 8): from functools import cached_property @@ -437,6 +438,13 @@ class RefactoringChecker(checkers.BaseTokenChecker): "Emitted when using dict() to create an empty dictionary instead of the literal {}. " "The literal is faster as it avoids an additional function call.", ), + "R1736": ( + "Unnecessary list index lookup, use '%s' instead", + "unnecessary-list-index-lookup", + "Emitted when iterating over an enumeration and accessing the " + "value by index lookup. " + "The value can be accessed directly instead.", + ), } options = ( ( @@ -635,10 +643,12 @@ def _check_redefined_argument_from_local(self, name_node): "redefined-argument-from-local", "too-many-nested-blocks", "unnecessary-dict-index-lookup", + "unnecessary-list-index-lookup", ) def visit_for(self, node: nodes.For) -> None: self._check_nested_blocks(node) self._check_unnecessary_dict_index_lookup(node) + self._check_unnecessary_list_index_lookup(node) for name in node.target.nodes_of_class(nodes.AssignName): self._check_redefined_argument_from_local(name) @@ -1556,10 +1566,15 @@ def _check_consider_using_join(self, aug_assign): def visit_augassign(self, node: nodes.AugAssign) -> None: self._check_consider_using_join(node) - @utils.check_messages("unnecessary-comprehension", "unnecessary-dict-index-lookup") + @utils.check_messages( + "unnecessary-comprehension", + "unnecessary-dict-index-lookup", + "unnecessary-list-index-lookup", + ) def visit_comprehension(self, node: nodes.Comprehension) -> None: self._check_unnecessary_comprehension(node) self._check_unnecessary_dict_index_lookup(node) + self._check_unnecessary_list_index_lookup(node) def _check_unnecessary_comprehension(self, node: nodes.Comprehension) -> None: if ( @@ -1963,3 +1978,72 @@ def _check_unnecessary_dict_index_lookup( node=subscript, args=("1".join(value.as_string().rsplit("0", maxsplit=1)),), ) + + def _check_unnecessary_list_index_lookup( + self, node: Union[nodes.For, nodes.Comprehension] + ) -> None: + if ( + not isinstance(node.iter, nodes.Call) + or not isinstance(node.iter.func, nodes.Name) + or not node.iter.func.name == "enumerate" + or not isinstance(node.iter.args[0], nodes.Name) + ): + return + + if not isinstance(node.target, nodes.Tuple) or len(node.target.elts) < 2: + # enumerate() result is being assigned without destructuring + return + + iterating_object_name = node.iter.args[0].name + value_variable = node.target.elts[1] + + children = ( + node.body if isinstance(node, nodes.For) else node.parent.get_children() + ) + for child in children: + for subscript in child.nodes_of_class(nodes.Subscript): + if isinstance(node, nodes.For) and ( + isinstance(subscript.parent, nodes.Assign) + and subscript in subscript.parent.targets + or isinstance(subscript.parent, nodes.AugAssign) + and subscript == subscript.parent.target + ): + # Ignore this subscript if it is the target of an assignment + # Early termination; after reassignment index lookup will be necessary + return + + if isinstance(subscript.parent, nodes.Delete): + # Ignore this subscript if it's used with the delete keyword + return + + index = subscript.slice + if isinstance(index, nodes.Name): + if ( + index.name != node.target.elts[0].name + or iterating_object_name != subscript.value.as_string() + ): + continue + + if ( + isinstance(node, nodes.For) + and index.lookup(index.name)[1][-1].lineno > node.lineno + ): + # Ignore this subscript if it has been redefined after + # the for loop. + continue + + if ( + isinstance(node, nodes.For) + and index.lookup(value_variable.name)[1][-1].lineno + > node.lineno + ): + # The variable holding the value from iteration has been + # reassigned on a later line, so it can't be used. + continue + + self.add_message( + "unnecessary-list-index-lookup", + node=subscript, + args=(node.target.elts[1].name,), + confidence=HIGH, + ) diff --git a/tests/functional/c/consider/consider_using_enumerate.py b/tests/functional/c/consider/consider_using_enumerate.py index bf29d688540..359bb29fa30 100644 --- a/tests/functional/c/consider/consider_using_enumerate.py +++ b/tests/functional/c/consider/consider_using_enumerate.py @@ -1,6 +1,6 @@ """Emit a message for iteration through range and len is encountered.""" -# pylint: disable=missing-docstring, import-error, useless-object-inheritance, unsubscriptable-object, too-few-public-methods +# pylint: disable=missing-docstring, import-error, useless-object-inheritance, unsubscriptable-object, too-few-public-methods, unnecessary-list-index-lookup def bad(): iterable = [1, 2, 3] diff --git a/tests/functional/u/unnecessary/unnecessary_list_index_lookup.py b/tests/functional/u/unnecessary/unnecessary_list_index_lookup.py new file mode 100644 index 00000000000..0fbd7995c5c --- /dev/null +++ b/tests/functional/u/unnecessary/unnecessary_list_index_lookup.py @@ -0,0 +1,46 @@ +"""Tests for unnecessary-list-index-lookup.""" + +# pylint: disable=missing-docstring, too-few-public-methods, expression-not-assigned, line-too-long, unused-variable + +my_list = ['a', 'b'] + +for idx, val in enumerate(my_list): + print(my_list[idx]) # [unnecessary-list-index-lookup] + +for idx, _ in enumerate(my_list): + print(my_list[0]) + if idx > 0: + print(my_list[idx - 1]) + +for idx, val in enumerate(my_list): + del my_list[idx] + +for idx, val in enumerate(my_list): + my_list[idx] = 42 + +for vals in enumerate(my_list): + # This could be refactored, but too complex to infer + print(my_list[vals[0]]) + +def process_list(data): + for index, value in enumerate(data): + index = 1 + print(data[index]) + +def process_list_again(data): + for index, value in enumerate(data): + value = 1 + print(data[index]) # Can't use value here, it's been redefined + +other_list = [1, 2] +for idx, val in enumerate(my_list): + print(other_list[idx]) + +OTHER_INDEX = 0 +for idx, val in enumerate(my_list): + print(my_list[OTHER_INDEX]) + +result = [val for idx, val in enumerate(my_list) if my_list[idx] == 'a'] # [unnecessary-list-index-lookup] +result = [val for idx, val in enumerate(my_list) if idx > 0 and my_list[idx - 1] == 'a'] +result = [val for idx, val in enumerate(my_list) if other_list[idx] == 'a'] +result = [my_list[idx] for idx, val in enumerate(my_list)] # [unnecessary-list-index-lookup] diff --git a/tests/functional/u/unnecessary/unnecessary_list_index_lookup.txt b/tests/functional/u/unnecessary/unnecessary_list_index_lookup.txt new file mode 100644 index 00000000000..0ff19dbf77a --- /dev/null +++ b/tests/functional/u/unnecessary/unnecessary_list_index_lookup.txt @@ -0,0 +1,3 @@ +unnecessary-list-index-lookup:8:10:8:22::Unnecessary list index lookup, use 'val' instead:HIGH +unnecessary-list-index-lookup:43:52:43:64::Unnecessary list index lookup, use 'val' instead:HIGH +unnecessary-list-index-lookup:46:10:46:22::Unnecessary list index lookup, use 'val' instead:HIGH diff --git a/tests/functional/u/unnecessary/unnecessary_list_index_lookup_py38.py b/tests/functional/u/unnecessary/unnecessary_list_index_lookup_py38.py new file mode 100644 index 00000000000..b1322e03f09 --- /dev/null +++ b/tests/functional/u/unnecessary/unnecessary_list_index_lookup_py38.py @@ -0,0 +1,9 @@ +"""Tests for unnecessary-list-index-lookup with assignment expressions.""" + +# pylint: disable=missing-docstring, too-few-public-methods, expression-not-assigned, line-too-long, unused-variable + +my_list = ['a', 'b'] + +for idx, val in enumerate(my_list): + if (val := 42) and my_list[idx] == 'b': + print(1) diff --git a/tests/functional/u/unnecessary/unnecessary_list_index_lookup_py38.rc b/tests/functional/u/unnecessary/unnecessary_list_index_lookup_py38.rc new file mode 100644 index 00000000000..85fc502b372 --- /dev/null +++ b/tests/functional/u/unnecessary/unnecessary_list_index_lookup_py38.rc @@ -0,0 +1,2 @@ +[testoptions] +min_pyver=3.8 From 2c29f4b7dff26730a067f135ee18f7a3c18263e3 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 26 Mar 2022 10:00:28 +0100 Subject: [PATCH 05/45] Loosen TypeVar name pattern (#5983) --- ChangeLog | 5 +++++ doc/user_guide/options.rst | 3 ++- pylint/checkers/base/name_checker/checker.py | 2 +- .../t/typevar_naming_style_default.py | 3 +++ .../t/typevar_naming_style_default.txt | 19 ++++++++++--------- 5 files changed, 21 insertions(+), 11 deletions(-) diff --git a/ChangeLog b/ChangeLog index 895325445e7..49a1b021060 100644 --- a/ChangeLog +++ b/ChangeLog @@ -46,6 +46,11 @@ Release date: TBA Closes #2793 +* Loosen TypeVar default name pattern a bit to allow names with multiple uppercase + characters. E.g. ``HVACModeT`` or ``IPAddressT``. + + Closes #5981 + * Fixed false positive for ``unused-argument`` when a ``nonlocal`` name is used in a nested function that is returned without being called by its parent. diff --git a/doc/user_guide/options.rst b/doc/user_guide/options.rst index 6e1535e249c..d89e5e988b3 100644 --- a/doc/user_guide/options.rst +++ b/doc/user_guide/options.rst @@ -95,7 +95,8 @@ The following type of names are checked with a predefined pattern: +--------------------+---------------------------------------------------+------------------------------------------------------------+ | Name type | Good names | Bad names | +====================+===================================================+============================================================+ -| ``typevar`` |`T`, `_CallableT`, `_T_co`, `AnyStr`, `DeviceTypeT`| `DICT_T`, `CALLABLE_T`, `ENUM_T`, `DeviceType`, `_StrType` | +| ``typevar`` | ``T``, ``_CallableT``, ``_T_co``, ``AnyStr``, | ``DICT_T``, ``CALLABLE_T``, ``ENUM_T``, ``DeviceType``, | +| | ``DeviceTypeT``, ``IPAddressT`` | ``_StrType`` | +--------------------+---------------------------------------------------+------------------------------------------------------------+ Custom regular expressions diff --git a/pylint/checkers/base/name_checker/checker.py b/pylint/checkers/base/name_checker/checker.py index 5943640c4ba..68adadc3c65 100644 --- a/pylint/checkers/base/name_checker/checker.py +++ b/pylint/checkers/base/name_checker/checker.py @@ -26,7 +26,7 @@ # Default patterns for name types that do not have styles DEFAULT_PATTERNS = { "typevar": re.compile( - r"^_{0,2}(?:[^\W\da-z_]+|(?:[^\W\da-z_][^\WA-Z_]+)+T?(? Date: Sat, 26 Mar 2022 12:22:05 +0100 Subject: [PATCH 06/45] Don't emit broken typing errors inside TYPE_CHECKING blocks (#5984) --- ChangeLog | 3 +++ pylint/checkers/utils.py | 11 ++++++++++ pylint/checkers/variables.py | 21 +++++++------------ pylint/constants.py | 3 +++ pylint/extensions/typing.py | 13 ++++++++---- .../ext/typing/typing_broken_callable.py | 6 +++++- .../ext/typing/typing_broken_callable.txt | 4 ++-- .../ext/typing/typing_broken_noreturn.py | 6 +++++- .../typing_broken_noreturn_future_import.py | 6 +++++- .../typing/typing_broken_noreturn_py372.py | 6 +++++- 10 files changed, 55 insertions(+), 24 deletions(-) diff --git a/ChangeLog b/ChangeLog index 49a1b021060..c23cadfd0b0 100644 --- a/ChangeLog +++ b/ChangeLog @@ -56,6 +56,9 @@ Release date: TBA Closes #5187 +* Don't emit ``broken-noreturn`` and ``broken-collections-callable`` errors + inside ``if TYPE_CHECKING`` blocks. + What's New in Pylint 2.13.0? ============================ diff --git a/pylint/checkers/utils.py b/pylint/checkers/utils.py index 344792d09bd..0267dfd582b 100644 --- a/pylint/checkers/utils.py +++ b/pylint/checkers/utils.py @@ -30,6 +30,8 @@ from astroid import TooManyLevelsError, nodes from astroid.context import InferenceContext +from pylint.constants import TYPING_TYPE_CHECKS_GUARDS + COMP_NODE_TYPES = ( nodes.ListComp, nodes.SetComp, @@ -1708,3 +1710,12 @@ def get_node_first_ancestor_of_type_and_its_child( return (ancestor, child) child = ancestor return None, None + + +def in_type_checking_block(node: nodes.NodeNG) -> bool: + """Check if a node is guarded by a TYPE_CHECKS guard.""" + return any( + isinstance(ancestor, nodes.If) + and ancestor.test.as_string() in TYPING_TYPE_CHECKS_GUARDS + for ancestor in node.node_ancestors() + ) diff --git a/pylint/checkers/variables.py b/pylint/checkers/variables.py index 15fa5008b28..58d2895baae 100644 --- a/pylint/checkers/variables.py +++ b/pylint/checkers/variables.py @@ -30,8 +30,11 @@ from astroid import nodes from pylint.checkers import BaseChecker, utils -from pylint.checkers.utils import is_postponed_evaluation_enabled -from pylint.constants import PY39_PLUS +from pylint.checkers.utils import ( + in_type_checking_block, + is_postponed_evaluation_enabled, +) +from pylint.constants import PY39_PLUS, TYPING_TYPE_CHECKS_GUARDS from pylint.interfaces import ( CONTROL_FLOW, HIGH, @@ -57,7 +60,6 @@ # by astroid. Unfortunately this also messes up our explicit checks # for `abc` METACLASS_NAME_TRANSFORMS = {"_py_abc": "abc"} -TYPING_TYPE_CHECKS_GUARDS = frozenset({"typing.TYPE_CHECKING", "TYPE_CHECKING"}) BUILTIN_RANGE = "builtins.range" TYPING_MODULE = "typing" TYPING_NAMES = frozenset( @@ -358,15 +360,6 @@ def _assigned_locally(name_node): return any(a.name == name_node.name for a in assign_stmts) -def _is_type_checking_import(node: Union[nodes.Import, nodes.ImportFrom]) -> bool: - """Check if an import node is guarded by a TYPE_CHECKS guard.""" - return any( - isinstance(ancestor, nodes.If) - and ancestor.test.as_string() in TYPING_TYPE_CHECKS_GUARDS - for ancestor in node.node_ancestors() - ) - - def _has_locals_call_after_node(stmt, scope): skip_nodes = ( nodes.FunctionDef, @@ -2735,7 +2728,7 @@ def _check_imports(self, not_consumed): msg = f"import {imported_name}" else: msg = f"{imported_name} imported as {as_name}" - if not _is_type_checking_import(stmt): + if not in_type_checking_block(stmt): self.add_message("unused-import", args=msg, node=stmt) elif isinstance(stmt, nodes.ImportFrom) and stmt.modname != FUTURE: if SPECIAL_OBJ.search(imported_name): @@ -2759,7 +2752,7 @@ def _check_imports(self, not_consumed): msg = f"{imported_name} imported from {stmt.modname}" else: msg = f"{imported_name} imported from {stmt.modname} as {as_name}" - if not _is_type_checking_import(stmt): + if not in_type_checking_block(stmt): self.add_message("unused-import", args=msg, node=stmt) # Construct string for unused-wildcard-import message diff --git a/pylint/constants.py b/pylint/constants.py index 2ecbd9778cd..adb8f5ca1aa 100644 --- a/pylint/constants.py +++ b/pylint/constants.py @@ -193,3 +193,6 @@ class DeletedMessage(NamedTuple): "W1513", # deprecated-decorator ] ) + + +TYPING_TYPE_CHECKS_GUARDS = frozenset({"typing.TYPE_CHECKING", "TYPE_CHECKING"}) diff --git a/pylint/extensions/typing.py b/pylint/extensions/typing.py index d6d67515a0b..ce25ff70d84 100644 --- a/pylint/extensions/typing.py +++ b/pylint/extensions/typing.py @@ -10,6 +10,7 @@ from pylint.checkers import BaseChecker from pylint.checkers.utils import ( check_messages, + in_type_checking_block, is_node_in_type_annotation_context, is_postponed_evaluation_enabled, safe_infer, @@ -355,8 +356,10 @@ def _check_broken_noreturn(self, node: Union[nodes.Name, nodes.Attribute]) -> No # NoReturn not part of a Union or Callable type return - if is_postponed_evaluation_enabled(node) and is_node_in_type_annotation_context( - node + if ( + in_type_checking_block(node) + or is_postponed_evaluation_enabled(node) + and is_node_in_type_annotation_context(node) ): return @@ -391,8 +394,10 @@ def _broken_callable_location( # pylint: disable=no-self-use self, node: Union[nodes.Name, nodes.Attribute] ) -> bool: """Check if node would be a broken location for collections.abc.Callable.""" - if is_postponed_evaluation_enabled(node) and is_node_in_type_annotation_context( - node + if ( + in_type_checking_block(node) + or is_postponed_evaluation_enabled(node) + and is_node_in_type_annotation_context(node) ): return False diff --git a/tests/functional/ext/typing/typing_broken_callable.py b/tests/functional/ext/typing/typing_broken_callable.py index 5cdac7dd75d..0713e17c87f 100644 --- a/tests/functional/ext/typing/typing_broken_callable.py +++ b/tests/functional/ext/typing/typing_broken_callable.py @@ -7,7 +7,7 @@ # pylint: disable=missing-docstring,unsubscriptable-object import collections.abc from collections.abc import Callable -from typing import Optional, Union +from typing import TYPE_CHECKING, Optional, Union Alias1 = Optional[Callable[[int], None]] # [broken-collections-callable] Alias2 = Union[Callable[[int], None], None] # [broken-collections-callable] @@ -17,6 +17,10 @@ Alias5 = list[Callable[..., None]] Alias6 = Callable[[int], None] +if TYPE_CHECKING: + # ok inside TYPE_CHECKING block + Alias7 = Optional[Callable[[int], None]] + def func1() -> Optional[Callable[[int], None]]: # [broken-collections-callable] ... diff --git a/tests/functional/ext/typing/typing_broken_callable.txt b/tests/functional/ext/typing/typing_broken_callable.txt index 360cd896bc5..6edcf211b26 100644 --- a/tests/functional/ext/typing/typing_broken_callable.txt +++ b/tests/functional/ext/typing/typing_broken_callable.txt @@ -1,4 +1,4 @@ broken-collections-callable:12:18:12:26::'collections.abc.Callable' inside Optional and Union is broken in 3.9.0 / 3.9.1 (use 'typing.Callable' instead):INFERENCE broken-collections-callable:13:15:13:23::'collections.abc.Callable' inside Optional and Union is broken in 3.9.0 / 3.9.1 (use 'typing.Callable' instead):INFERENCE -broken-collections-callable:21:24:21:32:func1:'collections.abc.Callable' inside Optional and Union is broken in 3.9.0 / 3.9.1 (use 'typing.Callable' instead):INFERENCE -broken-collections-callable:27:21:27:45:func3:'collections.abc.Callable' inside Optional and Union is broken in 3.9.0 / 3.9.1 (use 'typing.Callable' instead):INFERENCE +broken-collections-callable:25:24:25:32:func1:'collections.abc.Callable' inside Optional and Union is broken in 3.9.0 / 3.9.1 (use 'typing.Callable' instead):INFERENCE +broken-collections-callable:31:21:31:45:func3:'collections.abc.Callable' inside Optional and Union is broken in 3.9.0 / 3.9.1 (use 'typing.Callable' instead):INFERENCE diff --git a/tests/functional/ext/typing/typing_broken_noreturn.py b/tests/functional/ext/typing/typing_broken_noreturn.py index f675fce6aa4..4d10ed13abd 100644 --- a/tests/functional/ext/typing/typing_broken_noreturn.py +++ b/tests/functional/ext/typing/typing_broken_noreturn.py @@ -6,7 +6,7 @@ """ # pylint: disable=missing-docstring import typing -from typing import Callable, NoReturn, Union +from typing import TYPE_CHECKING, Callable, NoReturn, Union import typing_extensions @@ -30,3 +30,7 @@ def func5() -> Union[None, typing_extensions.NoReturn]: # [broken-noreturn] Alias1 = NoReturn Alias2 = Callable[..., NoReturn] # [broken-noreturn] Alias3 = Callable[..., "NoReturn"] + +if TYPE_CHECKING: + # ok inside TYPE_CHECKING block + Alias4 = Callable[..., NoReturn] diff --git a/tests/functional/ext/typing/typing_broken_noreturn_future_import.py b/tests/functional/ext/typing/typing_broken_noreturn_future_import.py index 83ec5474991..4743750bc55 100644 --- a/tests/functional/ext/typing/typing_broken_noreturn_future_import.py +++ b/tests/functional/ext/typing/typing_broken_noreturn_future_import.py @@ -11,7 +11,7 @@ from __future__ import annotations import typing -from typing import Callable, NoReturn, Union +from typing import TYPE_CHECKING, Callable, NoReturn, Union import typing_extensions @@ -35,3 +35,7 @@ def func5() -> Union[None, typing_extensions.NoReturn]: Alias1 = NoReturn Alias2 = Callable[..., NoReturn] # [broken-noreturn] Alias3 = Callable[..., "NoReturn"] + +if TYPE_CHECKING: + # ok inside TYPE_CHECKING block + Alias4 = Callable[..., NoReturn] diff --git a/tests/functional/ext/typing/typing_broken_noreturn_py372.py b/tests/functional/ext/typing/typing_broken_noreturn_py372.py index c4b18a03371..4ff1a71b75b 100644 --- a/tests/functional/ext/typing/typing_broken_noreturn_py372.py +++ b/tests/functional/ext/typing/typing_broken_noreturn_py372.py @@ -8,7 +8,7 @@ """ # pylint: disable=missing-docstring import typing -from typing import Callable, NoReturn, Union +from typing import TYPE_CHECKING, Callable, NoReturn, Union import typing_extensions @@ -32,3 +32,7 @@ def func5() -> Union[None, typing_extensions.NoReturn]: Alias1 = NoReturn Alias2 = Callable[..., NoReturn] Alias3 = Callable[..., "NoReturn"] + +if TYPE_CHECKING: + # ok inside TYPE_CHECKING block + Alias4 = Callable[..., NoReturn] From 5f33ca225756882589e2965703cae03e2860051a Mon Sep 17 00:00:00 2001 From: orSolocate <38433858+orSolocate@users.noreply.github.com> Date: Sat, 26 Mar 2022 15:47:26 +0300 Subject: [PATCH 07/45] Fix issue #5969 for `modified_iterating-list` (#5986) When the list/dict/set being iterated through is a function call. Co-authored-by: Pierre Sassoulas --- ChangeLog | 5 +++++ pylint/checkers/modified_iterating_checker.py | 5 +++-- tests/functional/m/modified_iterating.py | 16 ++++++++++++++++ 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/ChangeLog b/ChangeLog index c23cadfd0b0..9a54d1c9f2c 100644 --- a/ChangeLog +++ b/ChangeLog @@ -56,6 +56,11 @@ Release date: TBA Closes #5187 +* Fix program crash for ``modified_iterating-list/set/dict`` when the list/dict/set + being iterated through is a function call. + + Closes #5969 + * Don't emit ``broken-noreturn`` and ``broken-collections-callable`` errors inside ``if TYPE_CHECKING`` blocks. diff --git a/pylint/checkers/modified_iterating_checker.py b/pylint/checkers/modified_iterating_checker.py index d1fe08d40c7..ca2a0c9000a 100644 --- a/pylint/checkers/modified_iterating_checker.py +++ b/pylint/checkers/modified_iterating_checker.py @@ -57,8 +57,9 @@ class ModifiedIterationChecker(checkers.BaseChecker): ) def visit_for(self, node: nodes.For) -> None: iter_obj = node.iter - for body_node in node.body: - self._modified_iterating_check_on_node_and_children(body_node, iter_obj) + if isinstance(iter_obj, nodes.Name): + for body_node in node.body: + self._modified_iterating_check_on_node_and_children(body_node, iter_obj) def _modified_iterating_check_on_node_and_children( self, body_node: nodes.NodeNG, iter_obj: nodes.NodeNG diff --git a/tests/functional/m/modified_iterating.py b/tests/functional/m/modified_iterating.py index 32155fbc805..8817c1db0e2 100644 --- a/tests/functional/m/modified_iterating.py +++ b/tests/functional/m/modified_iterating.py @@ -55,3 +55,19 @@ item_list.remove(1) # [modified-iterating-list] for _ in []: item_list.append(1) # [modified-iterating-list] + + +def format_manifest_serializer_errors(errors): + """Regression test for issue #5969 - iter_obj is a function call.""" + errors_messages = [] + for key, value in errors.items(): + for message in format_manifest_serializer_errors(value): + error_message = f"{key}: {message}" + errors_messages.append(error_message) + return errors_messages + + +dict1 = {"1": 1} +dict2 = {"2": 2} +for item in dict1: + dict2[item] = 1 From 208328009ddeef3eb76ed96b2cbcd8854a65ad6c Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Fri, 25 Mar 2022 05:45:56 -0400 Subject: [PATCH 08/45] Fix nonlocal in try block regression for `used-before-assignment` (#5966) --- ChangeLog | 4 +++ pylint/checkers/variables.py | 7 +++++ .../used/used_before_assignment_issue4761.py | 28 +++++++++++++++++++ .../used/used_before_assignment_issue4761.txt | 17 +++++------ 4 files changed, 48 insertions(+), 8 deletions(-) diff --git a/ChangeLog b/ChangeLog index 4a4cb500b76..4973e7379e8 100644 --- a/ChangeLog +++ b/ChangeLog @@ -20,6 +20,10 @@ What's New in Pylint 2.13.1? ============================ Release date: TBA +* Fix a regression in 2.13.0 where ``used-before-assignment`` was emitted for + the usage of a nonlocal in a try block. + + Fixes #5965 What's New in Pylint 2.13.0? diff --git a/pylint/checkers/variables.py b/pylint/checkers/variables.py index 7600eb00ee3..ac4e02c5ef8 100644 --- a/pylint/checkers/variables.py +++ b/pylint/checkers/variables.py @@ -619,6 +619,13 @@ def get_next_to_consume(self, node: nodes.Name) -> Optional[List[nodes.NodeNG]]: ): found_nodes = None + # Before filtering, check that this node's name is not a nonlocal + if any( + isinstance(child, nodes.Nonlocal) and node.name in child.names + for child in node.frame(future=True).get_children() + ): + return found_nodes + # Filter out assignments in ExceptHandlers that node is not contained in # unless this is a test in a filtered comprehension # Example: [e for e in range(3) if e] <--- followed by except e: diff --git a/tests/functional/u/used/used_before_assignment_issue4761.py b/tests/functional/u/used/used_before_assignment_issue4761.py index ad4cca0baff..6f8e048e4c8 100644 --- a/tests/functional/u/used/used_before_assignment_issue4761.py +++ b/tests/functional/u/used/used_before_assignment_issue4761.py @@ -12,6 +12,34 @@ def function(): return some_message +def uses_nonlocal(): + """https://github.com/PyCQA/pylint/issues/5965""" + count = 0 + def inner(): + nonlocal count + try: + print(count) + except ValueError: + count +=1 + + return inner() + + +def uses_unrelated_nonlocal(): + """Unrelated nonlocals still let messages emit""" + count = 0 + unrelated = 0 + def inner(): + nonlocal unrelated + try: + print(count) # [used-before-assignment] + except ValueError: + count += 1 + + print(count) + return inner() + + # Cases related to a specific control flow where # the `else` of a loop can depend on a name only defined # in a single except handler because that except handler is the diff --git a/tests/functional/u/used/used_before_assignment_issue4761.txt b/tests/functional/u/used/used_before_assignment_issue4761.txt index e3251cbd7b2..833e0bf1854 100644 --- a/tests/functional/u/used/used_before_assignment_issue4761.txt +++ b/tests/functional/u/used/used_before_assignment_issue4761.txt @@ -1,9 +1,10 @@ used-before-assignment:9:11:9:23:function:Using variable 'some_message' before assignment:CONTROL_FLOW -used-before-assignment:46:10:46:15:invalid_no_outer_else:Using variable 'error' before assignment:CONTROL_FLOW -used-before-assignment:58:14:58:19:invalid_no_outer_else_2:Using variable 'error' before assignment:CONTROL_FLOW -used-before-assignment:72:14:72:19:invalid_no_inner_else:Using variable 'error' before assignment:CONTROL_FLOW -used-before-assignment:85:14:85:19:invalid_wrong_break_location:Using variable 'error' before assignment:CONTROL_FLOW -used-before-assignment:98:14:98:19:invalid_no_break:Using variable 'error' before assignment:CONTROL_FLOW -used-before-assignment:113:14:113:19:invalid_other_non_break_exit_from_loop_besides_except_handler:Using variable 'error' before assignment:CONTROL_FLOW -used-before-assignment:145:14:145:19:invalid_conditional_continue_after_break:Using variable 'error' before assignment:CONTROL_FLOW -used-before-assignment:161:14:161:19:invalid_unrelated_loops:Using variable 'error' before assignment:CONTROL_FLOW +used-before-assignment:35:18:35:23:uses_unrelated_nonlocal.inner:Using variable 'count' before assignment:CONTROL_FLOW +used-before-assignment:74:10:74:15:invalid_no_outer_else:Using variable 'error' before assignment:CONTROL_FLOW +used-before-assignment:86:14:86:19:invalid_no_outer_else_2:Using variable 'error' before assignment:CONTROL_FLOW +used-before-assignment:100:14:100:19:invalid_no_inner_else:Using variable 'error' before assignment:CONTROL_FLOW +used-before-assignment:113:14:113:19:invalid_wrong_break_location:Using variable 'error' before assignment:CONTROL_FLOW +used-before-assignment:126:14:126:19:invalid_no_break:Using variable 'error' before assignment:CONTROL_FLOW +used-before-assignment:141:14:141:19:invalid_other_non_break_exit_from_loop_besides_except_handler:Using variable 'error' before assignment:CONTROL_FLOW +used-before-assignment:173:14:173:19:invalid_conditional_continue_after_break:Using variable 'error' before assignment:CONTROL_FLOW +used-before-assignment:189:14:189:19:invalid_unrelated_loops:Using variable 'error' before assignment:CONTROL_FLOW From 54d03df1e6584ffcf0c8fafd308d4c167efffb53 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Fri, 25 Mar 2022 05:49:31 -0400 Subject: [PATCH 09/45] Don't emit `raising-bad-type` when there is ambiguity (#5968) Co-authored-by: Pierre Sassoulas --- ChangeLog | 4 ++++ doc/whatsnew/2.13.rst | 5 +++++ pylint/checkers/exceptions.py | 15 ++++++--------- .../invalid_exceptions_raised.py | 18 ++++++++++++++++++ 4 files changed, 33 insertions(+), 9 deletions(-) diff --git a/ChangeLog b/ChangeLog index 4973e7379e8..231c2032690 100644 --- a/ChangeLog +++ b/ChangeLog @@ -25,6 +25,10 @@ Release date: TBA Fixes #5965 +* Avoid emitting ``raising-bad-type`` when there is inference ambiguity on + the variable being raised. + + Closes #2793 What's New in Pylint 2.13.0? ============================ diff --git a/doc/whatsnew/2.13.rst b/doc/whatsnew/2.13.rst index 7b209042e5b..85655398387 100644 --- a/doc/whatsnew/2.13.rst +++ b/doc/whatsnew/2.13.rst @@ -547,3 +547,8 @@ Other Changes * Output better error message if unsupported file formats are used with ``pyreverse``. Closes #5950 + +* Avoid emitting ``raising-bad-type`` when there is inference ambiguity on + the variable being raised. + + Closes #2793 \ No newline at end of file diff --git a/pylint/checkers/exceptions.py b/pylint/checkers/exceptions.py index 6d0397e528a..9ef9ea42e64 100644 --- a/pylint/checkers/exceptions.py +++ b/pylint/checkers/exceptions.py @@ -62,8 +62,8 @@ def _is_raising(body: List) -> bool: "E0702": ( "Raising %s while only classes or instances are allowed", "raising-bad-type", - "Used when something which is neither a class, an instance or a " - "string is raised (i.e. a `TypeError` will be raised).", + "Used when something which is neither a class nor an instance " + "is raised (i.e. a `TypeError` will be raised).", ), "E0703": ( "Exception context set to something which is not an exception, nor None", @@ -272,13 +272,10 @@ def visit_raise(self, node: nodes.Raise) -> None: expr = node.exc ExceptionRaiseRefVisitor(self, node).visit(expr) - try: - inferred_value = expr.inferred()[-1] - except astroid.InferenceError: - pass - else: - if inferred_value: - ExceptionRaiseLeafVisitor(self, node).visit(inferred_value) + inferred = utils.safe_infer(expr) + if inferred is None or inferred is astroid.Uninferable: + return + ExceptionRaiseLeafVisitor(self, node).visit(inferred) def _check_misplaced_bare_raise(self, node): # Filter out if it's present in __exit__. diff --git a/tests/functional/i/invalid/invalid_exceptions/invalid_exceptions_raised.py b/tests/functional/i/invalid/invalid_exceptions/invalid_exceptions_raised.py index 47532654574..3b4229741ed 100644 --- a/tests/functional/i/invalid/invalid_exceptions/invalid_exceptions_raised.py +++ b/tests/functional/i/invalid/invalid_exceptions/invalid_exceptions_raised.py @@ -108,3 +108,21 @@ class Error(Exception): def bad_case10(): """raise string""" raise "string" # [raising-bad-type] + + +class AmbiguousValue: + """Don't emit when there is ambiguity on the node for the exception.""" + def __init__(self): + self.stored_exception = None + + def fail(self): + try: + 1 / 0 + except ZeroDivisionError as zde: + self.stored_exception = zde + + def raise_stored_exception(self): + if self.stored_exception is not None: + exc = self.stored_exception + self.stored_exception = None + raise exc From 903ce5849f075666f5170ab82658a3125a58dee7 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Fri, 25 Mar 2022 07:37:16 -0400 Subject: [PATCH 10/45] Fix false positive for `unused-argument` where nested function uses parent argument as a `nonlocal` (#5906) Co-authored-by: Pierre Sassoulas --- ChangeLog | 7 ++++++- doc/whatsnew/2.13.rst | 7 ++++++- pylint/checkers/variables.py | 19 +++++++++++++++---- .../u/unused/unused_argument_py3.py | 9 +++++++++ ...py => unused_name_from_wildcard_import.py} | 0 ...t => unused_name_from_wildcard_import.txt} | 2 +- 6 files changed, 37 insertions(+), 7 deletions(-) rename tests/functional/u/unused/{unused_name_from_wilcard_import.py => unused_name_from_wildcard_import.py} (100%) rename tests/functional/u/unused/{unused_name_from_wilcard_import.txt => unused_name_from_wildcard_import.txt} (77%) diff --git a/ChangeLog b/ChangeLog index 231c2032690..44c7c3a64b2 100644 --- a/ChangeLog +++ b/ChangeLog @@ -10,7 +10,6 @@ Release date: TBA Put new features here and also in 'doc/whatsnew/2.14.rst' - .. Insert your changelog randomly, it will reduce merge conflicts (Ie. not necessarily at the end) @@ -30,6 +29,12 @@ Release date: TBA Closes #2793 +* Fixed false positive for ``unused-argument`` when a ``nonlocal`` name is used + in a nested function that is returned without being called by its parent. + + Closes #5187 + + What's New in Pylint 2.13.0? ============================ Release date: 2022-03-24 diff --git a/doc/whatsnew/2.13.rst b/doc/whatsnew/2.13.rst index 85655398387..b0bfd132105 100644 --- a/doc/whatsnew/2.13.rst +++ b/doc/whatsnew/2.13.rst @@ -548,7 +548,12 @@ Other Changes Closes #5950 +* Fixed false positive for ``unused-argument`` when a ``nonlocal`` name is used + in a nested function that is returned without being called by its parent. + + Closes #5187 + * Avoid emitting ``raising-bad-type`` when there is inference ambiguity on the variable being raised. - Closes #2793 \ No newline at end of file + Closes #2793 diff --git a/pylint/checkers/variables.py b/pylint/checkers/variables.py index ac4e02c5ef8..49f1284fb5e 100644 --- a/pylint/checkers/variables.py +++ b/pylint/checkers/variables.py @@ -16,6 +16,8 @@ Any, DefaultDict, Dict, + Iterable, + Iterator, List, NamedTuple, Optional, @@ -343,7 +345,9 @@ def _import_name_is_global(stmt, global_names): return False -def _flattened_scope_names(iterator): +def _flattened_scope_names( + iterator: Iterator[Union[nodes.Global, nodes.Nonlocal]] +) -> Set[str]: values = (set(stmt.names) for stmt in iterator) return set(itertools.chain.from_iterable(values)) @@ -2265,7 +2269,9 @@ def _loopvar_name(self, node: astroid.Name) -> None: if not elements: self.add_message("undefined-loop-variable", args=node.name, node=node) - def _check_is_unused(self, name, node, stmt, global_names, nonlocal_names): + def _check_is_unused( + self, name, node, stmt, global_names, nonlocal_names: Iterable[str] + ): # Ignore some special names specified by user configuration. if self._is_name_ignored(stmt, name): return @@ -2287,7 +2293,7 @@ def _check_is_unused(self, name, node, stmt, global_names, nonlocal_names): argnames = node.argnames() # Care about functions with unknown argument (builtins) if name in argnames: - self._check_unused_arguments(name, node, stmt, argnames) + self._check_unused_arguments(name, node, stmt, argnames, nonlocal_names) else: if stmt.parent and isinstance( stmt.parent, (nodes.Assign, nodes.AnnAssign, nodes.Tuple) @@ -2354,7 +2360,9 @@ def _is_name_ignored(self, stmt, name): regex = authorized_rgx return regex and regex.match(name) - def _check_unused_arguments(self, name, node, stmt, argnames): + def _check_unused_arguments( + self, name, node, stmt, argnames, nonlocal_names: Iterable[str] + ): is_method = node.is_method() klass = node.parent.frame(future=True) if is_method and isinstance(klass, nodes.ClassDef): @@ -2395,6 +2403,9 @@ def _check_unused_arguments(self, name, node, stmt, argnames): if utils.is_protocol_class(klass): return + if name in nonlocal_names: + return + self.add_message("unused-argument", args=name, node=stmt, confidence=confidence) def _check_late_binding_closure(self, node: nodes.Name) -> None: diff --git a/tests/functional/u/unused/unused_argument_py3.py b/tests/functional/u/unused/unused_argument_py3.py index 4d0fd9adc0c..2529e6caa2a 100644 --- a/tests/functional/u/unused/unused_argument_py3.py +++ b/tests/functional/u/unused/unused_argument_py3.py @@ -7,3 +7,12 @@ def func(first, *, second): # [unused-argument, unused-argument] def only_raises(first, second=42): # [unused-argument] if first == 24: raise ValueError + + +def increment_factory(initial): + + def increment(): + nonlocal initial + initial += 1 + + return increment diff --git a/tests/functional/u/unused/unused_name_from_wilcard_import.py b/tests/functional/u/unused/unused_name_from_wildcard_import.py similarity index 100% rename from tests/functional/u/unused/unused_name_from_wilcard_import.py rename to tests/functional/u/unused/unused_name_from_wildcard_import.py diff --git a/tests/functional/u/unused/unused_name_from_wilcard_import.txt b/tests/functional/u/unused/unused_name_from_wildcard_import.txt similarity index 77% rename from tests/functional/u/unused/unused_name_from_wilcard_import.txt rename to tests/functional/u/unused/unused_name_from_wildcard_import.txt index c939b8260c2..0289a13fb12 100644 --- a/tests/functional/u/unused/unused_name_from_wilcard_import.txt +++ b/tests/functional/u/unused/unused_name_from_wildcard_import.txt @@ -1,4 +1,4 @@ -unused-wildcard-import:3:0:3:34::Unused import(s) func and only_raises from wildcard import of unused_argument_py3:UNDEFINED +unused-wildcard-import:3:0:3:34::Unused import(s) func, only_raises and increment_factory from wildcard import of unused_argument_py3:UNDEFINED wildcard-import:3:0:3:34::Wildcard import unused_argument_py3:UNDEFINED unused-wildcard-import:4:0:4:38::Unused import(s) VAR from wildcard import of unused_global_variable1:UNDEFINED wildcard-import:4:0:4:38::Wildcard import unused_global_variable1:UNDEFINED From 4a92de7d276bd5bfb61e326253d999e24c1012ac Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 26 Mar 2022 10:00:28 +0100 Subject: [PATCH 11/45] Loosen TypeVar name pattern (#5983) --- ChangeLog | 5 +++++ doc/user_guide/options.rst | 3 ++- pylint/checkers/base/name_checker/checker.py | 2 +- .../t/typevar_naming_style_default.py | 3 +++ .../t/typevar_naming_style_default.txt | 19 ++++++++++--------- 5 files changed, 21 insertions(+), 11 deletions(-) diff --git a/ChangeLog b/ChangeLog index 44c7c3a64b2..ed55e88b42f 100644 --- a/ChangeLog +++ b/ChangeLog @@ -29,6 +29,11 @@ Release date: TBA Closes #2793 +* Loosen TypeVar default name pattern a bit to allow names with multiple uppercase + characters. E.g. ``HVACModeT`` or ``IPAddressT``. + + Closes #5981 + * Fixed false positive for ``unused-argument`` when a ``nonlocal`` name is used in a nested function that is returned without being called by its parent. diff --git a/doc/user_guide/options.rst b/doc/user_guide/options.rst index 6e1535e249c..d89e5e988b3 100644 --- a/doc/user_guide/options.rst +++ b/doc/user_guide/options.rst @@ -95,7 +95,8 @@ The following type of names are checked with a predefined pattern: +--------------------+---------------------------------------------------+------------------------------------------------------------+ | Name type | Good names | Bad names | +====================+===================================================+============================================================+ -| ``typevar`` |`T`, `_CallableT`, `_T_co`, `AnyStr`, `DeviceTypeT`| `DICT_T`, `CALLABLE_T`, `ENUM_T`, `DeviceType`, `_StrType` | +| ``typevar`` | ``T``, ``_CallableT``, ``_T_co``, ``AnyStr``, | ``DICT_T``, ``CALLABLE_T``, ``ENUM_T``, ``DeviceType``, | +| | ``DeviceTypeT``, ``IPAddressT`` | ``_StrType`` | +--------------------+---------------------------------------------------+------------------------------------------------------------+ Custom regular expressions diff --git a/pylint/checkers/base/name_checker/checker.py b/pylint/checkers/base/name_checker/checker.py index 5943640c4ba..68adadc3c65 100644 --- a/pylint/checkers/base/name_checker/checker.py +++ b/pylint/checkers/base/name_checker/checker.py @@ -26,7 +26,7 @@ # Default patterns for name types that do not have styles DEFAULT_PATTERNS = { "typevar": re.compile( - r"^_{0,2}(?:[^\W\da-z_]+|(?:[^\W\da-z_][^\WA-Z_]+)+T?(? Date: Sat, 26 Mar 2022 12:22:05 +0100 Subject: [PATCH 12/45] Don't emit broken typing errors inside TYPE_CHECKING blocks (#5984) --- ChangeLog | 3 +++ pylint/checkers/utils.py | 11 ++++++++++ pylint/checkers/variables.py | 21 +++++++------------ pylint/constants.py | 3 +++ pylint/extensions/typing.py | 13 ++++++++---- .../ext/typing/typing_broken_callable.py | 6 +++++- .../ext/typing/typing_broken_callable.txt | 4 ++-- .../ext/typing/typing_broken_noreturn.py | 6 +++++- .../typing_broken_noreturn_future_import.py | 6 +++++- .../typing/typing_broken_noreturn_py372.py | 6 +++++- 10 files changed, 55 insertions(+), 24 deletions(-) diff --git a/ChangeLog b/ChangeLog index ed55e88b42f..ea6f7701ef2 100644 --- a/ChangeLog +++ b/ChangeLog @@ -39,6 +39,9 @@ Release date: TBA Closes #5187 +* Don't emit ``broken-noreturn`` and ``broken-collections-callable`` errors + inside ``if TYPE_CHECKING`` blocks. + What's New in Pylint 2.13.0? ============================ diff --git a/pylint/checkers/utils.py b/pylint/checkers/utils.py index 344792d09bd..0267dfd582b 100644 --- a/pylint/checkers/utils.py +++ b/pylint/checkers/utils.py @@ -30,6 +30,8 @@ from astroid import TooManyLevelsError, nodes from astroid.context import InferenceContext +from pylint.constants import TYPING_TYPE_CHECKS_GUARDS + COMP_NODE_TYPES = ( nodes.ListComp, nodes.SetComp, @@ -1708,3 +1710,12 @@ def get_node_first_ancestor_of_type_and_its_child( return (ancestor, child) child = ancestor return None, None + + +def in_type_checking_block(node: nodes.NodeNG) -> bool: + """Check if a node is guarded by a TYPE_CHECKS guard.""" + return any( + isinstance(ancestor, nodes.If) + and ancestor.test.as_string() in TYPING_TYPE_CHECKS_GUARDS + for ancestor in node.node_ancestors() + ) diff --git a/pylint/checkers/variables.py b/pylint/checkers/variables.py index 49f1284fb5e..002eaf628c2 100644 --- a/pylint/checkers/variables.py +++ b/pylint/checkers/variables.py @@ -30,8 +30,11 @@ from astroid import nodes from pylint.checkers import BaseChecker, utils -from pylint.checkers.utils import is_postponed_evaluation_enabled -from pylint.constants import PY39_PLUS +from pylint.checkers.utils import ( + in_type_checking_block, + is_postponed_evaluation_enabled, +) +from pylint.constants import PY39_PLUS, TYPING_TYPE_CHECKS_GUARDS from pylint.interfaces import ( CONTROL_FLOW, HIGH, @@ -57,7 +60,6 @@ # by astroid. Unfortunately this also messes up our explicit checks # for `abc` METACLASS_NAME_TRANSFORMS = {"_py_abc": "abc"} -TYPING_TYPE_CHECKS_GUARDS = frozenset({"typing.TYPE_CHECKING", "TYPE_CHECKING"}) BUILTIN_RANGE = "builtins.range" TYPING_MODULE = "typing" TYPING_NAMES = frozenset( @@ -358,15 +360,6 @@ def _assigned_locally(name_node): return any(a.name == name_node.name for a in assign_stmts) -def _is_type_checking_import(node: Union[nodes.Import, nodes.ImportFrom]) -> bool: - """Check if an import node is guarded by a TYPE_CHECKS guard.""" - return any( - isinstance(ancestor, nodes.If) - and ancestor.test.as_string() in TYPING_TYPE_CHECKS_GUARDS - for ancestor in node.node_ancestors() - ) - - def _has_locals_call_after_node(stmt, scope): skip_nodes = ( nodes.FunctionDef, @@ -2729,7 +2722,7 @@ def _check_imports(self, not_consumed): msg = f"import {imported_name}" else: msg = f"{imported_name} imported as {as_name}" - if not _is_type_checking_import(stmt): + if not in_type_checking_block(stmt): self.add_message("unused-import", args=msg, node=stmt) elif isinstance(stmt, nodes.ImportFrom) and stmt.modname != FUTURE: if SPECIAL_OBJ.search(imported_name): @@ -2753,7 +2746,7 @@ def _check_imports(self, not_consumed): msg = f"{imported_name} imported from {stmt.modname}" else: msg = f"{imported_name} imported from {stmt.modname} as {as_name}" - if not _is_type_checking_import(stmt): + if not in_type_checking_block(stmt): self.add_message("unused-import", args=msg, node=stmt) # Construct string for unused-wildcard-import message diff --git a/pylint/constants.py b/pylint/constants.py index 2ecbd9778cd..adb8f5ca1aa 100644 --- a/pylint/constants.py +++ b/pylint/constants.py @@ -193,3 +193,6 @@ class DeletedMessage(NamedTuple): "W1513", # deprecated-decorator ] ) + + +TYPING_TYPE_CHECKS_GUARDS = frozenset({"typing.TYPE_CHECKING", "TYPE_CHECKING"}) diff --git a/pylint/extensions/typing.py b/pylint/extensions/typing.py index d6d67515a0b..ce25ff70d84 100644 --- a/pylint/extensions/typing.py +++ b/pylint/extensions/typing.py @@ -10,6 +10,7 @@ from pylint.checkers import BaseChecker from pylint.checkers.utils import ( check_messages, + in_type_checking_block, is_node_in_type_annotation_context, is_postponed_evaluation_enabled, safe_infer, @@ -355,8 +356,10 @@ def _check_broken_noreturn(self, node: Union[nodes.Name, nodes.Attribute]) -> No # NoReturn not part of a Union or Callable type return - if is_postponed_evaluation_enabled(node) and is_node_in_type_annotation_context( - node + if ( + in_type_checking_block(node) + or is_postponed_evaluation_enabled(node) + and is_node_in_type_annotation_context(node) ): return @@ -391,8 +394,10 @@ def _broken_callable_location( # pylint: disable=no-self-use self, node: Union[nodes.Name, nodes.Attribute] ) -> bool: """Check if node would be a broken location for collections.abc.Callable.""" - if is_postponed_evaluation_enabled(node) and is_node_in_type_annotation_context( - node + if ( + in_type_checking_block(node) + or is_postponed_evaluation_enabled(node) + and is_node_in_type_annotation_context(node) ): return False diff --git a/tests/functional/ext/typing/typing_broken_callable.py b/tests/functional/ext/typing/typing_broken_callable.py index 5cdac7dd75d..0713e17c87f 100644 --- a/tests/functional/ext/typing/typing_broken_callable.py +++ b/tests/functional/ext/typing/typing_broken_callable.py @@ -7,7 +7,7 @@ # pylint: disable=missing-docstring,unsubscriptable-object import collections.abc from collections.abc import Callable -from typing import Optional, Union +from typing import TYPE_CHECKING, Optional, Union Alias1 = Optional[Callable[[int], None]] # [broken-collections-callable] Alias2 = Union[Callable[[int], None], None] # [broken-collections-callable] @@ -17,6 +17,10 @@ Alias5 = list[Callable[..., None]] Alias6 = Callable[[int], None] +if TYPE_CHECKING: + # ok inside TYPE_CHECKING block + Alias7 = Optional[Callable[[int], None]] + def func1() -> Optional[Callable[[int], None]]: # [broken-collections-callable] ... diff --git a/tests/functional/ext/typing/typing_broken_callable.txt b/tests/functional/ext/typing/typing_broken_callable.txt index 360cd896bc5..6edcf211b26 100644 --- a/tests/functional/ext/typing/typing_broken_callable.txt +++ b/tests/functional/ext/typing/typing_broken_callable.txt @@ -1,4 +1,4 @@ broken-collections-callable:12:18:12:26::'collections.abc.Callable' inside Optional and Union is broken in 3.9.0 / 3.9.1 (use 'typing.Callable' instead):INFERENCE broken-collections-callable:13:15:13:23::'collections.abc.Callable' inside Optional and Union is broken in 3.9.0 / 3.9.1 (use 'typing.Callable' instead):INFERENCE -broken-collections-callable:21:24:21:32:func1:'collections.abc.Callable' inside Optional and Union is broken in 3.9.0 / 3.9.1 (use 'typing.Callable' instead):INFERENCE -broken-collections-callable:27:21:27:45:func3:'collections.abc.Callable' inside Optional and Union is broken in 3.9.0 / 3.9.1 (use 'typing.Callable' instead):INFERENCE +broken-collections-callable:25:24:25:32:func1:'collections.abc.Callable' inside Optional and Union is broken in 3.9.0 / 3.9.1 (use 'typing.Callable' instead):INFERENCE +broken-collections-callable:31:21:31:45:func3:'collections.abc.Callable' inside Optional and Union is broken in 3.9.0 / 3.9.1 (use 'typing.Callable' instead):INFERENCE diff --git a/tests/functional/ext/typing/typing_broken_noreturn.py b/tests/functional/ext/typing/typing_broken_noreturn.py index f675fce6aa4..4d10ed13abd 100644 --- a/tests/functional/ext/typing/typing_broken_noreturn.py +++ b/tests/functional/ext/typing/typing_broken_noreturn.py @@ -6,7 +6,7 @@ """ # pylint: disable=missing-docstring import typing -from typing import Callable, NoReturn, Union +from typing import TYPE_CHECKING, Callable, NoReturn, Union import typing_extensions @@ -30,3 +30,7 @@ def func5() -> Union[None, typing_extensions.NoReturn]: # [broken-noreturn] Alias1 = NoReturn Alias2 = Callable[..., NoReturn] # [broken-noreturn] Alias3 = Callable[..., "NoReturn"] + +if TYPE_CHECKING: + # ok inside TYPE_CHECKING block + Alias4 = Callable[..., NoReturn] diff --git a/tests/functional/ext/typing/typing_broken_noreturn_future_import.py b/tests/functional/ext/typing/typing_broken_noreturn_future_import.py index 83ec5474991..4743750bc55 100644 --- a/tests/functional/ext/typing/typing_broken_noreturn_future_import.py +++ b/tests/functional/ext/typing/typing_broken_noreturn_future_import.py @@ -11,7 +11,7 @@ from __future__ import annotations import typing -from typing import Callable, NoReturn, Union +from typing import TYPE_CHECKING, Callable, NoReturn, Union import typing_extensions @@ -35,3 +35,7 @@ def func5() -> Union[None, typing_extensions.NoReturn]: Alias1 = NoReturn Alias2 = Callable[..., NoReturn] # [broken-noreturn] Alias3 = Callable[..., "NoReturn"] + +if TYPE_CHECKING: + # ok inside TYPE_CHECKING block + Alias4 = Callable[..., NoReturn] diff --git a/tests/functional/ext/typing/typing_broken_noreturn_py372.py b/tests/functional/ext/typing/typing_broken_noreturn_py372.py index c4b18a03371..4ff1a71b75b 100644 --- a/tests/functional/ext/typing/typing_broken_noreturn_py372.py +++ b/tests/functional/ext/typing/typing_broken_noreturn_py372.py @@ -8,7 +8,7 @@ """ # pylint: disable=missing-docstring import typing -from typing import Callable, NoReturn, Union +from typing import TYPE_CHECKING, Callable, NoReturn, Union import typing_extensions @@ -32,3 +32,7 @@ def func5() -> Union[None, typing_extensions.NoReturn]: Alias1 = NoReturn Alias2 = Callable[..., NoReturn] Alias3 = Callable[..., "NoReturn"] + +if TYPE_CHECKING: + # ok inside TYPE_CHECKING block + Alias4 = Callable[..., NoReturn] From 4a057941a0a30e2fa6d485416ca4167cda6bf6fb Mon Sep 17 00:00:00 2001 From: orSolocate <38433858+orSolocate@users.noreply.github.com> Date: Sat, 26 Mar 2022 15:47:26 +0300 Subject: [PATCH 13/45] Fix issue #5969 for `modified_iterating-list` (#5986) When the list/dict/set being iterated through is a function call. Co-authored-by: Pierre Sassoulas --- ChangeLog | 5 +++++ pylint/checkers/modified_iterating_checker.py | 5 +++-- tests/functional/m/modified_iterating.py | 16 ++++++++++++++++ 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/ChangeLog b/ChangeLog index ea6f7701ef2..be25eceb2b4 100644 --- a/ChangeLog +++ b/ChangeLog @@ -39,6 +39,11 @@ Release date: TBA Closes #5187 +* Fix program crash for ``modified_iterating-list/set/dict`` when the list/dict/set + being iterated through is a function call. + + Closes #5969 + * Don't emit ``broken-noreturn`` and ``broken-collections-callable`` errors inside ``if TYPE_CHECKING`` blocks. diff --git a/pylint/checkers/modified_iterating_checker.py b/pylint/checkers/modified_iterating_checker.py index d1fe08d40c7..ca2a0c9000a 100644 --- a/pylint/checkers/modified_iterating_checker.py +++ b/pylint/checkers/modified_iterating_checker.py @@ -57,8 +57,9 @@ class ModifiedIterationChecker(checkers.BaseChecker): ) def visit_for(self, node: nodes.For) -> None: iter_obj = node.iter - for body_node in node.body: - self._modified_iterating_check_on_node_and_children(body_node, iter_obj) + if isinstance(iter_obj, nodes.Name): + for body_node in node.body: + self._modified_iterating_check_on_node_and_children(body_node, iter_obj) def _modified_iterating_check_on_node_and_children( self, body_node: nodes.NodeNG, iter_obj: nodes.NodeNG diff --git a/tests/functional/m/modified_iterating.py b/tests/functional/m/modified_iterating.py index 32155fbc805..8817c1db0e2 100644 --- a/tests/functional/m/modified_iterating.py +++ b/tests/functional/m/modified_iterating.py @@ -55,3 +55,19 @@ item_list.remove(1) # [modified-iterating-list] for _ in []: item_list.append(1) # [modified-iterating-list] + + +def format_manifest_serializer_errors(errors): + """Regression test for issue #5969 - iter_obj is a function call.""" + errors_messages = [] + for key, value in errors.items(): + for message in format_manifest_serializer_errors(value): + error_message = f"{key}: {message}" + errors_messages.append(error_message) + return errors_messages + + +dict1 = {"1": 1} +dict2 = {"2": 2} +for item in dict1: + dict2[item] = 1 From 0e1ca11ac65cbe5a65437518fca1e25f1ad0e48e Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Sat, 26 Mar 2022 14:00:28 +0100 Subject: [PATCH 14/45] Bump pylint to 2.13.1, update changelog --- CONTRIBUTORS.txt | 2 +- ChangeLog | 9 ++++++++- doc/release.md | 7 +++++-- pylint/__pkginfo__.py | 2 +- tbump.toml | 2 +- 5 files changed, 16 insertions(+), 6 deletions(-) diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 9a86723b36d..a36661ab9a9 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -121,6 +121,7 @@ contributors: - David Gilman - Andrew Haigh (nelfin) - へーさん +- orSolocate <38433858+orSolocate@users.noreply.github.com> - Thomas Hisch - Marianna Polatoglou : minor contribution for wildcard import check - Manuel Vázquez Acosta @@ -138,7 +139,6 @@ contributors: * Added new extension which detects comparing integers to zero, * Added new useless-return checker, * Added new try-except-raise checker -- orSolocate <38433858+orSolocate@users.noreply.github.com> - Téo Bouvard - Tushar Sadhwani (tusharsadhwani) - Mihai Balint diff --git a/ChangeLog b/ChangeLog index be25eceb2b4..1e701b22db5 100644 --- a/ChangeLog +++ b/ChangeLog @@ -10,15 +10,22 @@ Release date: TBA Put new features here and also in 'doc/whatsnew/2.14.rst' + .. Insert your changelog randomly, it will reduce merge conflicts (Ie. not necessarily at the end) -What's New in Pylint 2.13.1? +What's New in Pylint 2.13.2? ============================ Release date: TBA + + +What's New in Pylint 2.13.1? +============================ +Release date: 2022-03-26 + * Fix a regression in 2.13.0 where ``used-before-assignment`` was emitted for the usage of a nonlocal in a try block. diff --git a/doc/release.md b/doc/release.md index 07b4a647bbd..c68ef00bf1e 100644 --- a/doc/release.md +++ b/doc/release.md @@ -45,6 +45,8 @@ branch appropriate changelog in the description. This triggers the PyPI release. - Delete the `maintenance/X.Y-1.x` branch. (For example: `maintenance/2.3.x`) - Create a `maintenance/X.Y.x` (For example: `maintenance/2.4.x` from the `v2.4.0` tag.) +- Close the current milestone and create the new ones (For example: close `2.4.0`, + create `2.4.1` and `2.6.0`) ## Backporting a fix from `main` to the maintenance branch @@ -67,8 +69,7 @@ cherry-picked on the maintenance branch. - Install the release dependencies: `pip3 install -r requirements_test.txt` - Bump the version and release by using `tbump X.Y-1.Z --no-push`. (For example: `tbump 2.3.5 --no-push`) -- Check the result visually and then by triggering the "release tests" workflow in - GitHub Actions first. +- Check the result visually with `git show`. - Push the tag. - Release the version on GitHub with the same name as the tag and copy and paste the appropriate changelog in the description. This triggers the PyPI release. @@ -77,6 +78,8 @@ cherry-picked on the maintenance branch. `pre-commit autoupdate` works for pylint. - Fix version conflicts properly, or bump the version to `X.Y.0-devZ` (For example: `2.4.0-dev6`) before pushing on the main branch +- Close the current milestone and create the new one (For example: close `2.3.5`, create + `2.3.6`) ## Milestone handling diff --git a/pylint/__pkginfo__.py b/pylint/__pkginfo__.py index 1b459ff0132..588fb54a02d 100644 --- a/pylint/__pkginfo__.py +++ b/pylint/__pkginfo__.py @@ -4,7 +4,7 @@ from typing import Tuple -__version__ = "2.13.0" +__version__ = "2.13.1" def get_numversion_from_version(v: str) -> Tuple: diff --git a/tbump.toml b/tbump.toml index 83785652fca..fa371dd6e3b 100644 --- a/tbump.toml +++ b/tbump.toml @@ -1,7 +1,7 @@ github_url = "https://github.com/PyCQA/pylint" [version] -current = "2.13.0" +current = "2.13.1" regex = ''' ^(?P0|[1-9]\d*) \. From 556304eb1e54c67940d0f9d0497ffa368b50a22a Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Sat, 26 Mar 2022 14:15:52 +0100 Subject: [PATCH 15/45] Fix merge issue following 2.13.1 release --- ChangeLog | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/ChangeLog b/ChangeLog index dafdb18db6a..d713e5d30fd 100644 --- a/ChangeLog +++ b/ChangeLog @@ -36,33 +36,6 @@ What's New in Pylint 2.13.2? ============================ Release date: TBA -* Fix a regression in 2.13.0 where ``used-before-assignment`` was emitted for - the usage of a nonlocal in a try block. - - Fixes #5965 - -* Avoid emitting ``raising-bad-type`` when there is inference ambiguity on - the variable being raised. - - Closes #2793 - -* Loosen TypeVar default name pattern a bit to allow names with multiple uppercase - characters. E.g. ``HVACModeT`` or ``IPAddressT``. - - Closes #5981 - -* Fixed false positive for ``unused-argument`` when a ``nonlocal`` name is used - in a nested function that is returned without being called by its parent. - - Closes #5187 - -* Fix program crash for ``modified_iterating-list/set/dict`` when the list/dict/set - being iterated through is a function call. - - Closes #5969 - -* Don't emit ``broken-noreturn`` and ``broken-collections-callable`` errors - inside ``if TYPE_CHECKING`` blocks. What's New in Pylint 2.13.1? From 96d7e898cc863a6854cd0870ba4069c8f7d7851b Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Sat, 26 Mar 2022 18:31:49 -0400 Subject: [PATCH 16/45] Fix false positive for `superfluous-parens` for `return (a or b) in iterable` (#5964) Co-authored-by: Pierre Sassoulas --- ChangeLog | 4 ++++ doc/whatsnew/2.13.rst | 5 +++++ pylint/checkers/format.py | 23 ++++++++++------------- tests/functional/s/superfluous_parens.py | 5 ++++- 4 files changed, 23 insertions(+), 14 deletions(-) diff --git a/ChangeLog b/ChangeLog index d713e5d30fd..956d266348c 100644 --- a/ChangeLog +++ b/ChangeLog @@ -36,6 +36,10 @@ What's New in Pylint 2.13.2? ============================ Release date: TBA +* Fix false positive for ``superfluous-parens`` for patterns like + "return (a or b) in iterable". + + Closes #5803 What's New in Pylint 2.13.1? diff --git a/doc/whatsnew/2.13.rst b/doc/whatsnew/2.13.rst index b0bfd132105..2d425d7a4a1 100644 --- a/doc/whatsnew/2.13.rst +++ b/doc/whatsnew/2.13.rst @@ -557,3 +557,8 @@ Other Changes the variable being raised. Closes #2793 + +* Fix false positive for ``superfluous-parens`` for patterns like + "return (a or b) in iterable". + + Closes #5803 diff --git a/pylint/checkers/format.py b/pylint/checkers/format.py index fae6085ca85..70c56a621a6 100644 --- a/pylint/checkers/format.py +++ b/pylint/checkers/format.py @@ -320,6 +320,7 @@ def new_line(self, tokens, line_end, line_start): def process_module(self, _node: nodes.Module) -> None: pass + # pylint: disable-next=too-many-return-statements def _check_keyword_parentheses( self, tokens: List[tokenize.TokenInfo], start: int ) -> None: @@ -382,19 +383,15 @@ def _check_keyword_parentheses( # The empty tuple () is always accepted. if i == start + 2: return - if keyword_token == "not": - if not found_and_or: - self.add_message( - "superfluous-parens", line=line_num, args=keyword_token - ) - elif keyword_token in {"return", "yield"}: - self.add_message( - "superfluous-parens", line=line_num, args=keyword_token - ) - elif not found_and_or and keyword_token != "in": - self.add_message( - "superfluous-parens", line=line_num, args=keyword_token - ) + if found_and_or: + return + if keyword_token == "in": + # This special case was added in https://github.com/PyCQA/pylint/pull/4948 + # but it could be removed in the future. Avoid churn for now. + return + self.add_message( + "superfluous-parens", line=line_num, args=keyword_token + ) return elif depth == 1: # This is a tuple, which is always acceptable. diff --git a/tests/functional/s/superfluous_parens.py b/tests/functional/s/superfluous_parens.py index 22c4c3ab4b3..ee835f6d084 100644 --- a/tests/functional/s/superfluous_parens.py +++ b/tests/functional/s/superfluous_parens.py @@ -44,8 +44,11 @@ def function_A(): def function_B(var): return (var.startswith(('A', 'B', 'C')) or var == 'D') +def function_C(first, second): + return (first or second) in (0, 1) + # TODO: Test string combinations, see https://github.com/PyCQA/pylint/issues/4792 -# Lines 45, 46 & 47 should raise the superfluous-parens message +# The lines with "+" should raise the superfluous-parens message J = "TestString" K = ("Test " + "String") L = ("Test " + "String") in I From 57c6ea8b21de95b730ec9da6c5e2ca579a03d136 Mon Sep 17 00:00:00 2001 From: Matus Valo Date: Sun, 27 Mar 2022 00:16:55 +0100 Subject: [PATCH 17/45] Added message doc for assigning-non-slot (#5991) Co-authored-by: Vladyslav Krylasov --- doc/data/messages/a/assigning-non-slot/bad.py | 10 ++++++++++ doc/data/messages/a/assigning-non-slot/good.py | 10 ++++++++++ 2 files changed, 20 insertions(+) create mode 100644 doc/data/messages/a/assigning-non-slot/bad.py create mode 100644 doc/data/messages/a/assigning-non-slot/good.py diff --git a/doc/data/messages/a/assigning-non-slot/bad.py b/doc/data/messages/a/assigning-non-slot/bad.py new file mode 100644 index 00000000000..a1a658ba298 --- /dev/null +++ b/doc/data/messages/a/assigning-non-slot/bad.py @@ -0,0 +1,10 @@ +class Student: + __slots__ = ('name',) + + def __init__(self, name, surname): + self.name = name + self.surname = surname # [assigning-non-slot] + self.setup() + + def setup(self): + pass diff --git a/doc/data/messages/a/assigning-non-slot/good.py b/doc/data/messages/a/assigning-non-slot/good.py new file mode 100644 index 00000000000..feb5a10fa1d --- /dev/null +++ b/doc/data/messages/a/assigning-non-slot/good.py @@ -0,0 +1,10 @@ +class Student: + __slots__ = ('name', 'surname') + + def __init__(self, name, surname): + self.name = name + self.surname = surname + self.setup() + + def setup(self): + pass From 3356ed43f24d0e58919305361ec5780dc38a5bc7 Mon Sep 17 00:00:00 2001 From: Matus Valo Date: Sun, 27 Mar 2022 00:33:43 +0100 Subject: [PATCH 18/45] Added example for attribute-defined-outside-init Co-authored-by: Vladyslav Krylasov --- doc/data/messages/a/attribute-defined-outside-init/bad.py | 3 +++ doc/data/messages/a/attribute-defined-outside-init/good.py | 6 ++++++ 2 files changed, 9 insertions(+) create mode 100644 doc/data/messages/a/attribute-defined-outside-init/bad.py create mode 100644 doc/data/messages/a/attribute-defined-outside-init/good.py diff --git a/doc/data/messages/a/attribute-defined-outside-init/bad.py b/doc/data/messages/a/attribute-defined-outside-init/bad.py new file mode 100644 index 00000000000..e62884d497d --- /dev/null +++ b/doc/data/messages/a/attribute-defined-outside-init/bad.py @@ -0,0 +1,3 @@ +class Student: + def register(self): + self.is_registered = True # [attribute-defined-outside-init] diff --git a/doc/data/messages/a/attribute-defined-outside-init/good.py b/doc/data/messages/a/attribute-defined-outside-init/good.py new file mode 100644 index 00000000000..cc7d7acb186 --- /dev/null +++ b/doc/data/messages/a/attribute-defined-outside-init/good.py @@ -0,0 +1,6 @@ +class Student: + def __init__(self): + self.is_registered = False + + def register(self): + self.is_registered = True From 6270d825c523a1a432d556dbd0a4b1585ad4de77 Mon Sep 17 00:00:00 2001 From: Matus Valo Date: Sun, 27 Mar 2022 14:10:47 +0200 Subject: [PATCH 19/45] Added bad-classmethod-argument example (#5996) --- doc/data/messages/b/bad-classmethod-argument/bad.py | 5 +++++ doc/data/messages/b/bad-classmethod-argument/good.py | 5 +++++ 2 files changed, 10 insertions(+) create mode 100644 doc/data/messages/b/bad-classmethod-argument/bad.py create mode 100644 doc/data/messages/b/bad-classmethod-argument/good.py diff --git a/doc/data/messages/b/bad-classmethod-argument/bad.py b/doc/data/messages/b/bad-classmethod-argument/bad.py new file mode 100644 index 00000000000..7e43001b593 --- /dev/null +++ b/doc/data/messages/b/bad-classmethod-argument/bad.py @@ -0,0 +1,5 @@ +class Klass: + + @classmethod + def get_instance(self): # [bad-classmethod-argument] + return self() diff --git a/doc/data/messages/b/bad-classmethod-argument/good.py b/doc/data/messages/b/bad-classmethod-argument/good.py new file mode 100644 index 00000000000..3d4decea365 --- /dev/null +++ b/doc/data/messages/b/bad-classmethod-argument/good.py @@ -0,0 +1,5 @@ +class Klass: + + @classmethod + def get_instance(cls): + return cls() From 6279ab18f5cb9e74f83ff48e353ddef769fac7a7 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Sun, 27 Mar 2022 08:31:10 -0400 Subject: [PATCH 20/45] Add regression test for #5982 upgrade astroid to 2.11.2 (#5988) Co-authored-by: Pierre Sassoulas --- ChangeLog | 4 ++++ requirements_test_min.txt | 2 +- setup.cfg | 2 +- tests/functional/u/used/used_before_assignment_py37.py | 7 +++++++ tests/functional/u/used/used_before_assignment_py37.txt | 2 +- 5 files changed, 14 insertions(+), 3 deletions(-) diff --git a/ChangeLog b/ChangeLog index 956d266348c..f867ea4027e 100644 --- a/ChangeLog +++ b/ChangeLog @@ -36,6 +36,10 @@ What's New in Pylint 2.13.2? ============================ Release date: TBA +* Fix crash when subclassing a ``namedtuple``. + + Closes #5982 + * Fix false positive for ``superfluous-parens`` for patterns like "return (a or b) in iterable". diff --git a/requirements_test_min.txt b/requirements_test_min.txt index 83f1a88101a..10e6ce2e0e8 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.0 # Pinned to a specific version for tests +astroid==2.11.2 # 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 17e97a70cec..461a3cffeca 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.0,<=2.12.0-dev0 + astroid>=2.11.2,<=2.12.0-dev0 isort>=4.2.5,<6 mccabe>=0.6,<0.8 tomli>=1.1.0;python_version<"3.11" diff --git a/tests/functional/u/used/used_before_assignment_py37.py b/tests/functional/u/used/used_before_assignment_py37.py index 08a585a95b5..c64bf7cf55e 100644 --- a/tests/functional/u/used/used_before_assignment_py37.py +++ b/tests/functional/u/used/used_before_assignment_py37.py @@ -1,6 +1,7 @@ """Tests for used-before-assignment with functions added in python 3.7""" # pylint: disable=missing-function-docstring from __future__ import annotations +from collections import namedtuple from typing import List @@ -26,3 +27,9 @@ def inner_method(self, other: MyClass) -> bool: return self == other return inner_method(self, MyClass()) + + +class NamedTupleSubclass(namedtuple("NamedTupleSubclass", [])): + """Taken from https://github.com/PyCQA/pylint/issues/5982""" + def method(self) -> NamedTupleSubclass: + """Variables checker crashed when astroid did not supply a lineno""" diff --git a/tests/functional/u/used/used_before_assignment_py37.txt b/tests/functional/u/used/used_before_assignment_py37.txt index ad06560e215..fa0a0b77a05 100644 --- a/tests/functional/u/used/used_before_assignment_py37.txt +++ b/tests/functional/u/used/used_before_assignment_py37.txt @@ -1 +1 @@ -undefined-variable:17:20:17:27:MyClass.incorrect_default_method:Undefined variable 'MyClass':UNDEFINED +undefined-variable:18:20:18:27:MyClass.incorrect_default_method:Undefined variable 'MyClass':UNDEFINED From 5c8384e811e39aa59bf7d6f627293f0b7675e057 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Sun, 27 Mar 2022 08:31:53 -0400 Subject: [PATCH 21/45] Fix false negative for `protected-access` on functions (#5990) --- ChangeLog | 5 +++++ pylint/checkers/classes/class_checker.py | 5 ++++- tests/functional/p/protected_access.py | 14 ++++++++++++++ tests/functional/p/protected_access.txt | 2 ++ 4 files changed, 25 insertions(+), 1 deletion(-) diff --git a/ChangeLog b/ChangeLog index f867ea4027e..56c636c7d1f 100644 --- a/ChangeLog +++ b/ChangeLog @@ -45,6 +45,11 @@ Release date: TBA Closes #5803 +* Fix a false negative regression in 2.13.0 where ``protected-access`` was not + raised on functions. + + Fixes #5989 + What's New in Pylint 2.13.1? ============================ diff --git a/pylint/checkers/classes/class_checker.py b/pylint/checkers/classes/class_checker.py index e258f6642ad..f5efc4dc7c7 100644 --- a/pylint/checkers/classes/class_checker.py +++ b/pylint/checkers/classes/class_checker.py @@ -980,7 +980,7 @@ def _check_unused_private_attributes(self, node: nodes.ClassDef) -> None: if attribute.attrname != assign_attr.attrname: continue - if self._is_type_self_call(attribute.expr): + if isinstance(attribute.expr, nodes.Call): continue if assign_attr.expr.name in { @@ -2106,6 +2106,7 @@ def _is_mandatory_method_param(self, node: nodes.NodeNG) -> bool: """Check if nodes.Name corresponds to first attribute variable name. Name is `self` for method, `cls` for classmethod and `mcs` for metaclass. + Static methods return False. """ if self._first_attrs: first_attr = self._first_attrs[-1] @@ -2116,6 +2117,8 @@ def _is_mandatory_method_param(self, node: nodes.NodeNG) -> bool: ) if closest_func is None: return False + if not closest_func.is_bound(): + return False if not closest_func.args.args: return False first_attr = closest_func.args.args[0].name diff --git a/tests/functional/p/protected_access.py b/tests/functional/p/protected_access.py index 39769fe76a8..8d2f945d289 100644 --- a/tests/functional/p/protected_access.py +++ b/tests/functional/p/protected_access.py @@ -27,3 +27,17 @@ def nargs(self): class Application(metaclass=MC): def __no_special__(cls): nargs = obj._nargs # [protected-access] + + +class Light: + @property + def _light_internal(self) -> None: + return None + + @staticmethod + def func(light) -> None: + print(light._light_internal) # [protected-access] + + +def func(light: Light) -> None: + print(light._light_internal) # [protected-access] diff --git a/tests/functional/p/protected_access.txt b/tests/functional/p/protected_access.txt index 570a9063810..50e6c5d5b66 100644 --- a/tests/functional/p/protected_access.txt +++ b/tests/functional/p/protected_access.txt @@ -1,2 +1,4 @@ protected-access:17:0:17:9::Access to a protected member _teta of a client class:UNDEFINED protected-access:29:16:29:26:Application.__no_special__:Access to a protected member _nargs of a client class:UNDEFINED +protected-access:39:14:39:35:Light.func:Access to a protected member _light_internal of a client class:UNDEFINED +protected-access:43:10:43:31:func:Access to a protected member _light_internal of a client class:UNDEFINED From 49e666685ee1e573cef8e99587e7d877356406fe Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Sun, 27 Mar 2022 14:32:27 +0200 Subject: [PATCH 22/45] Better error message when we cant write the crash files (#5987) * Display the error correctly if we can't write a crash report. Also catch any exceptions. --- pylint/config/__init__.py | 5 ++--- pylint/lint/utils.py | 11 +++++++---- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/pylint/config/__init__.py b/pylint/config/__init__.py index 12f834d81b1..16df5fd8936 100644 --- a/pylint/config/__init__.py +++ b/pylint/config/__init__.py @@ -71,11 +71,10 @@ pathlib.Path(PYLINT_HOME).mkdir(parents=True, exist_ok=True) with open(spam_prevention_file, "w", encoding="utf8") as f: f.write("") - except Exception: # pylint: disable=broad-except - # Can't write in PYLINT_HOME ? + except Exception as exc: # pylint: disable=broad-except print( "Can't write the file that was supposed to " - f"prevent pylint.d deprecation spam in {PYLINT_HOME}." + f"prevent 'pylint.d' deprecation spam in {PYLINT_HOME} because of {exc}." ) diff --git a/pylint/lint/utils.py b/pylint/lint/utils.py index 8377dfdd441..7d402a0088d 100644 --- a/pylint/lint/utils.py +++ b/pylint/lint/utils.py @@ -51,13 +51,16 @@ def prepare_crash_report(ex: Exception, filepath: str, crash_file_path: str) -> pylint crashed with a ``{ex.__class__.__name__}`` and with the following stacktrace: ``` """ + template += traceback.format_exc() + template += "```\n" try: with open(issue_template_path, "a", encoding="utf8") as f: f.write(template) - traceback.print_exc(file=f) - f.write("```\n") - except FileNotFoundError: - print(f"Can't write the issue template for the crash in {issue_template_path}.") + except Exception as exc: # pylint: disable=broad-except + print( + f"Can't write the issue template for the crash in {issue_template_path} " + f"because of: '{exc}'\nHere's the content anyway:\n{template}." + ) return issue_template_path From b25859c4a56ccce61087f7a1270f40deaed68169 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Sat, 26 Mar 2022 18:31:49 -0400 Subject: [PATCH 23/45] Fix false positive for `superfluous-parens` for `return (a or b) in iterable` (#5964) Co-authored-by: Pierre Sassoulas --- ChangeLog | 4 ++++ doc/whatsnew/2.13.rst | 5 +++++ pylint/checkers/format.py | 23 ++++++++++------------- tests/functional/s/superfluous_parens.py | 5 ++++- 4 files changed, 23 insertions(+), 14 deletions(-) diff --git a/ChangeLog b/ChangeLog index 1e701b22db5..9caec4e3713 100644 --- a/ChangeLog +++ b/ChangeLog @@ -20,6 +20,10 @@ What's New in Pylint 2.13.2? ============================ Release date: TBA +* Fix false positive for ``superfluous-parens`` for patterns like + "return (a or b) in iterable". + + Closes #5803 What's New in Pylint 2.13.1? diff --git a/doc/whatsnew/2.13.rst b/doc/whatsnew/2.13.rst index b0bfd132105..2d425d7a4a1 100644 --- a/doc/whatsnew/2.13.rst +++ b/doc/whatsnew/2.13.rst @@ -557,3 +557,8 @@ Other Changes the variable being raised. Closes #2793 + +* Fix false positive for ``superfluous-parens`` for patterns like + "return (a or b) in iterable". + + Closes #5803 diff --git a/pylint/checkers/format.py b/pylint/checkers/format.py index fae6085ca85..70c56a621a6 100644 --- a/pylint/checkers/format.py +++ b/pylint/checkers/format.py @@ -320,6 +320,7 @@ def new_line(self, tokens, line_end, line_start): def process_module(self, _node: nodes.Module) -> None: pass + # pylint: disable-next=too-many-return-statements def _check_keyword_parentheses( self, tokens: List[tokenize.TokenInfo], start: int ) -> None: @@ -382,19 +383,15 @@ def _check_keyword_parentheses( # The empty tuple () is always accepted. if i == start + 2: return - if keyword_token == "not": - if not found_and_or: - self.add_message( - "superfluous-parens", line=line_num, args=keyword_token - ) - elif keyword_token in {"return", "yield"}: - self.add_message( - "superfluous-parens", line=line_num, args=keyword_token - ) - elif not found_and_or and keyword_token != "in": - self.add_message( - "superfluous-parens", line=line_num, args=keyword_token - ) + if found_and_or: + return + if keyword_token == "in": + # This special case was added in https://github.com/PyCQA/pylint/pull/4948 + # but it could be removed in the future. Avoid churn for now. + return + self.add_message( + "superfluous-parens", line=line_num, args=keyword_token + ) return elif depth == 1: # This is a tuple, which is always acceptable. diff --git a/tests/functional/s/superfluous_parens.py b/tests/functional/s/superfluous_parens.py index 22c4c3ab4b3..ee835f6d084 100644 --- a/tests/functional/s/superfluous_parens.py +++ b/tests/functional/s/superfluous_parens.py @@ -44,8 +44,11 @@ def function_A(): def function_B(var): return (var.startswith(('A', 'B', 'C')) or var == 'D') +def function_C(first, second): + return (first or second) in (0, 1) + # TODO: Test string combinations, see https://github.com/PyCQA/pylint/issues/4792 -# Lines 45, 46 & 47 should raise the superfluous-parens message +# The lines with "+" should raise the superfluous-parens message J = "TestString" K = ("Test " + "String") L = ("Test " + "String") in I From dec241b1787e6c99a092bb9ef6a993abf51fea91 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Sun, 27 Mar 2022 08:31:10 -0400 Subject: [PATCH 24/45] Add regression test for #5982 upgrade astroid to 2.11.2 (#5988) Co-authored-by: Pierre Sassoulas --- ChangeLog | 4 ++++ requirements_test_min.txt | 2 +- setup.cfg | 2 +- tests/functional/u/used/used_before_assignment_py37.py | 7 +++++++ tests/functional/u/used/used_before_assignment_py37.txt | 2 +- 5 files changed, 14 insertions(+), 3 deletions(-) diff --git a/ChangeLog b/ChangeLog index 9caec4e3713..8fed9ccb770 100644 --- a/ChangeLog +++ b/ChangeLog @@ -20,6 +20,10 @@ What's New in Pylint 2.13.2? ============================ Release date: TBA +* Fix crash when subclassing a ``namedtuple``. + + Closes #5982 + * Fix false positive for ``superfluous-parens`` for patterns like "return (a or b) in iterable". diff --git a/requirements_test_min.txt b/requirements_test_min.txt index 83f1a88101a..10e6ce2e0e8 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.0 # Pinned to a specific version for tests +astroid==2.11.2 # 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 42b349e3809..25e66a3a0bd 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.0,<=2.12.0-dev0 + astroid>=2.11.2,<=2.12.0-dev0 isort>=4.2.5,<6 mccabe>=0.6,<0.8 tomli>=1.1.0;python_version<"3.11" diff --git a/tests/functional/u/used/used_before_assignment_py37.py b/tests/functional/u/used/used_before_assignment_py37.py index 08a585a95b5..c64bf7cf55e 100644 --- a/tests/functional/u/used/used_before_assignment_py37.py +++ b/tests/functional/u/used/used_before_assignment_py37.py @@ -1,6 +1,7 @@ """Tests for used-before-assignment with functions added in python 3.7""" # pylint: disable=missing-function-docstring from __future__ import annotations +from collections import namedtuple from typing import List @@ -26,3 +27,9 @@ def inner_method(self, other: MyClass) -> bool: return self == other return inner_method(self, MyClass()) + + +class NamedTupleSubclass(namedtuple("NamedTupleSubclass", [])): + """Taken from https://github.com/PyCQA/pylint/issues/5982""" + def method(self) -> NamedTupleSubclass: + """Variables checker crashed when astroid did not supply a lineno""" diff --git a/tests/functional/u/used/used_before_assignment_py37.txt b/tests/functional/u/used/used_before_assignment_py37.txt index ad06560e215..fa0a0b77a05 100644 --- a/tests/functional/u/used/used_before_assignment_py37.txt +++ b/tests/functional/u/used/used_before_assignment_py37.txt @@ -1 +1 @@ -undefined-variable:17:20:17:27:MyClass.incorrect_default_method:Undefined variable 'MyClass':UNDEFINED +undefined-variable:18:20:18:27:MyClass.incorrect_default_method:Undefined variable 'MyClass':UNDEFINED From c42fe73a1613bbfb52a5ba9129efa45a3fd76401 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Sun, 27 Mar 2022 08:31:53 -0400 Subject: [PATCH 25/45] Fix false negative for `protected-access` on functions (#5990) --- ChangeLog | 5 +++++ pylint/checkers/classes/class_checker.py | 5 ++++- tests/functional/p/protected_access.py | 14 ++++++++++++++ tests/functional/p/protected_access.txt | 2 ++ 4 files changed, 25 insertions(+), 1 deletion(-) diff --git a/ChangeLog b/ChangeLog index 8fed9ccb770..1df22b6b5f7 100644 --- a/ChangeLog +++ b/ChangeLog @@ -29,6 +29,11 @@ Release date: TBA Closes #5803 +* Fix a false negative regression in 2.13.0 where ``protected-access`` was not + raised on functions. + + Fixes #5989 + What's New in Pylint 2.13.1? ============================ diff --git a/pylint/checkers/classes/class_checker.py b/pylint/checkers/classes/class_checker.py index e258f6642ad..f5efc4dc7c7 100644 --- a/pylint/checkers/classes/class_checker.py +++ b/pylint/checkers/classes/class_checker.py @@ -980,7 +980,7 @@ def _check_unused_private_attributes(self, node: nodes.ClassDef) -> None: if attribute.attrname != assign_attr.attrname: continue - if self._is_type_self_call(attribute.expr): + if isinstance(attribute.expr, nodes.Call): continue if assign_attr.expr.name in { @@ -2106,6 +2106,7 @@ def _is_mandatory_method_param(self, node: nodes.NodeNG) -> bool: """Check if nodes.Name corresponds to first attribute variable name. Name is `self` for method, `cls` for classmethod and `mcs` for metaclass. + Static methods return False. """ if self._first_attrs: first_attr = self._first_attrs[-1] @@ -2116,6 +2117,8 @@ def _is_mandatory_method_param(self, node: nodes.NodeNG) -> bool: ) if closest_func is None: return False + if not closest_func.is_bound(): + return False if not closest_func.args.args: return False first_attr = closest_func.args.args[0].name diff --git a/tests/functional/p/protected_access.py b/tests/functional/p/protected_access.py index 39769fe76a8..8d2f945d289 100644 --- a/tests/functional/p/protected_access.py +++ b/tests/functional/p/protected_access.py @@ -27,3 +27,17 @@ def nargs(self): class Application(metaclass=MC): def __no_special__(cls): nargs = obj._nargs # [protected-access] + + +class Light: + @property + def _light_internal(self) -> None: + return None + + @staticmethod + def func(light) -> None: + print(light._light_internal) # [protected-access] + + +def func(light: Light) -> None: + print(light._light_internal) # [protected-access] diff --git a/tests/functional/p/protected_access.txt b/tests/functional/p/protected_access.txt index 570a9063810..50e6c5d5b66 100644 --- a/tests/functional/p/protected_access.txt +++ b/tests/functional/p/protected_access.txt @@ -1,2 +1,4 @@ protected-access:17:0:17:9::Access to a protected member _teta of a client class:UNDEFINED protected-access:29:16:29:26:Application.__no_special__:Access to a protected member _nargs of a client class:UNDEFINED +protected-access:39:14:39:35:Light.func:Access to a protected member _light_internal of a client class:UNDEFINED +protected-access:43:10:43:31:func:Access to a protected member _light_internal of a client class:UNDEFINED From 6a25d7048edadc18a05e999021049ade86ef2bd9 Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Sun, 27 Mar 2022 14:32:27 +0200 Subject: [PATCH 26/45] Better error message when we cant write the crash files (#5987) * Display the error correctly if we can't write a crash report. Also catch any exceptions. --- pylint/config/__init__.py | 5 ++--- pylint/lint/utils.py | 11 +++++++---- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/pylint/config/__init__.py b/pylint/config/__init__.py index 12f834d81b1..16df5fd8936 100644 --- a/pylint/config/__init__.py +++ b/pylint/config/__init__.py @@ -71,11 +71,10 @@ pathlib.Path(PYLINT_HOME).mkdir(parents=True, exist_ok=True) with open(spam_prevention_file, "w", encoding="utf8") as f: f.write("") - except Exception: # pylint: disable=broad-except - # Can't write in PYLINT_HOME ? + except Exception as exc: # pylint: disable=broad-except print( "Can't write the file that was supposed to " - f"prevent pylint.d deprecation spam in {PYLINT_HOME}." + f"prevent 'pylint.d' deprecation spam in {PYLINT_HOME} because of {exc}." ) diff --git a/pylint/lint/utils.py b/pylint/lint/utils.py index 8377dfdd441..7d402a0088d 100644 --- a/pylint/lint/utils.py +++ b/pylint/lint/utils.py @@ -51,13 +51,16 @@ def prepare_crash_report(ex: Exception, filepath: str, crash_file_path: str) -> pylint crashed with a ``{ex.__class__.__name__}`` and with the following stacktrace: ``` """ + template += traceback.format_exc() + template += "```\n" try: with open(issue_template_path, "a", encoding="utf8") as f: f.write(template) - traceback.print_exc(file=f) - f.write("```\n") - except FileNotFoundError: - print(f"Can't write the issue template for the crash in {issue_template_path}.") + except Exception as exc: # pylint: disable=broad-except + print( + f"Can't write the issue template for the crash in {issue_template_path} " + f"because of: '{exc}'\nHere's the content anyway:\n{template}." + ) return issue_template_path From 2066cab9bbe43341b84014ac9610e275db586431 Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Sun, 27 Mar 2022 14:42:22 +0200 Subject: [PATCH 27/45] Bump pylint to 2.13.2, update changelog --- ChangeLog | 12 +++++++++++- doc/release.md | 1 + pylint/__pkginfo__.py | 2 +- tbump.toml | 2 +- 4 files changed, 14 insertions(+), 3 deletions(-) diff --git a/ChangeLog b/ChangeLog index 1df22b6b5f7..d3a6411bd92 100644 --- a/ChangeLog +++ b/ChangeLog @@ -16,10 +16,16 @@ Release date: TBA (Ie. not necessarily at the end) -What's New in Pylint 2.13.2? +What's New in Pylint 2.13.3? ============================ Release date: TBA + + +What's New in Pylint 2.13.2? +============================ +Release date: 2022-03-27 + * Fix crash when subclassing a ``namedtuple``. Closes #5982 @@ -34,6 +40,10 @@ Release date: TBA Fixes #5989 +* Better error messages in case of crash if pylint can't write the issue template. + + Refer to #5987 + What's New in Pylint 2.13.1? ============================ diff --git a/doc/release.md b/doc/release.md index c68ef00bf1e..556de5c0ae2 100644 --- a/doc/release.md +++ b/doc/release.md @@ -57,6 +57,7 @@ maintenance branch we cherry-pick the commit from `main`. version `X.Y-1.Z'`. (For example: `v2.3.5`) - After the PR is merged on `main` cherry-pick the commits on the `maintenance/X.Y.x` branch (For example: from `maintenance/2.4.x` cherry-pick a commit from `main`) +- Remove the "need backport" label from cherry-picked issues - Release a patch version ## Releasing a patch version diff --git a/pylint/__pkginfo__.py b/pylint/__pkginfo__.py index 588fb54a02d..7d04c14d36e 100644 --- a/pylint/__pkginfo__.py +++ b/pylint/__pkginfo__.py @@ -4,7 +4,7 @@ from typing import Tuple -__version__ = "2.13.1" +__version__ = "2.13.2" def get_numversion_from_version(v: str) -> Tuple: diff --git a/tbump.toml b/tbump.toml index fa371dd6e3b..67c389d7a28 100644 --- a/tbump.toml +++ b/tbump.toml @@ -1,7 +1,7 @@ github_url = "https://github.com/PyCQA/pylint" [version] -current = "2.13.1" +current = "2.13.2" regex = ''' ^(?P0|[1-9]\d*) \. From 19dc9a67145d89b2f9f5b998574eaa38ba090bee Mon Sep 17 00:00:00 2001 From: Matus Valo Date: Mon, 28 Mar 2022 08:34:42 +0200 Subject: [PATCH 28/45] Added example for bad-builtin (#5993) Co-authored-by: Pierre Sassoulas --- doc/data/messages/b/bad-builtin/bad.py | 2 ++ doc/data/messages/b/bad-builtin/good.py | 2 ++ 2 files changed, 4 insertions(+) create mode 100644 doc/data/messages/b/bad-builtin/bad.py create mode 100644 doc/data/messages/b/bad-builtin/good.py diff --git a/doc/data/messages/b/bad-builtin/bad.py b/doc/data/messages/b/bad-builtin/bad.py new file mode 100644 index 00000000000..1c60815e598 --- /dev/null +++ b/doc/data/messages/b/bad-builtin/bad.py @@ -0,0 +1,2 @@ +numbers = list(map(lambda x: 2 * x, [1, 2, 3])) # [bad-builtin] +print(numbers) diff --git a/doc/data/messages/b/bad-builtin/good.py b/doc/data/messages/b/bad-builtin/good.py new file mode 100644 index 00000000000..c56dbfbe3cc --- /dev/null +++ b/doc/data/messages/b/bad-builtin/good.py @@ -0,0 +1,2 @@ +numbers = [2 * x for x in [1, 2, 3]] +print(numbers) From 1b85ca5177a032d61fa20454a7f2496fb1ef54a3 Mon Sep 17 00:00:00 2001 From: Matus Valo Date: Mon, 28 Mar 2022 10:12:40 +0200 Subject: [PATCH 29/45] Added example for bad-staticmethod-argument (#6002) Co-authored-by: Vladyslav Krylasov Co-authored-by: Pierre Sassoulas --- doc/data/messages/b/bad-staticmethod-argument/bad.py | 4 ++++ doc/data/messages/b/bad-staticmethod-argument/good.py | 4 ++++ 2 files changed, 8 insertions(+) create mode 100644 doc/data/messages/b/bad-staticmethod-argument/bad.py create mode 100644 doc/data/messages/b/bad-staticmethod-argument/good.py diff --git a/doc/data/messages/b/bad-staticmethod-argument/bad.py b/doc/data/messages/b/bad-staticmethod-argument/bad.py new file mode 100644 index 00000000000..a46d65a48a9 --- /dev/null +++ b/doc/data/messages/b/bad-staticmethod-argument/bad.py @@ -0,0 +1,4 @@ +class Wolf: + @staticmethod + def eat(self): # [bad-staticmethod-argument] + pass diff --git a/doc/data/messages/b/bad-staticmethod-argument/good.py b/doc/data/messages/b/bad-staticmethod-argument/good.py new file mode 100644 index 00000000000..7ad83f4929f --- /dev/null +++ b/doc/data/messages/b/bad-staticmethod-argument/good.py @@ -0,0 +1,4 @@ +class Wolf: + @staticmethod + def eat(sheep): + pass From de1cca2a6ab665a0529782d83a1c470b056a398d Mon Sep 17 00:00:00 2001 From: Andreas Finkler <3929834+DudeNr33@users.noreply.github.com> Date: Mon, 28 Mar 2022 10:13:31 +0200 Subject: [PATCH 30/45] Add documentation and code examples for ``consider-using-with`` (#6009) --- doc/data/messages/c/consider-using-with/bad.py | 3 +++ doc/data/messages/c/consider-using-with/details.rst | 6 ++++++ doc/data/messages/c/consider-using-with/good.py | 2 ++ doc/data/messages/c/consider-using-with/related.rst | 2 ++ 4 files changed, 13 insertions(+) create mode 100644 doc/data/messages/c/consider-using-with/bad.py create mode 100644 doc/data/messages/c/consider-using-with/details.rst create mode 100644 doc/data/messages/c/consider-using-with/good.py create mode 100644 doc/data/messages/c/consider-using-with/related.rst diff --git a/doc/data/messages/c/consider-using-with/bad.py b/doc/data/messages/c/consider-using-with/bad.py new file mode 100644 index 00000000000..bd657299b8c --- /dev/null +++ b/doc/data/messages/c/consider-using-with/bad.py @@ -0,0 +1,3 @@ +file = open("foo.txt", "r", encoding="utf8") # [consider-using-with] +contents = file.read() +file.close() diff --git a/doc/data/messages/c/consider-using-with/details.rst b/doc/data/messages/c/consider-using-with/details.rst new file mode 100644 index 00000000000..1c990988eda --- /dev/null +++ b/doc/data/messages/c/consider-using-with/details.rst @@ -0,0 +1,6 @@ +This message applies to callables of Python's stdlib which can be replaced by a ``with`` statement. +It is suppressed in the following cases: + +- the call is located inside a context manager +- the call result is returned from the enclosing function +- the call result is used in a ``with`` statement itself diff --git a/doc/data/messages/c/consider-using-with/good.py b/doc/data/messages/c/consider-using-with/good.py new file mode 100644 index 00000000000..70e09b146cd --- /dev/null +++ b/doc/data/messages/c/consider-using-with/good.py @@ -0,0 +1,2 @@ +with open("foo.txt", "r", encoding="utf8") as file: + contents = file.read() diff --git a/doc/data/messages/c/consider-using-with/related.rst b/doc/data/messages/c/consider-using-with/related.rst new file mode 100644 index 00000000000..c4864e02e13 --- /dev/null +++ b/doc/data/messages/c/consider-using-with/related.rst @@ -0,0 +1,2 @@ +- `PEP 343 `_ +- `Context managers in Python `_ by John Lekberg From f1378716e4548e934d4b8c2c30e1d9f94766f038 Mon Sep 17 00:00:00 2001 From: Matus Valo Date: Mon, 28 Mar 2022 10:23:17 +0200 Subject: [PATCH 31/45] Added example for bad-indentation (#6000) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Vladyslav Krylasov Co-authored-by: Daniël van Noord <13665637+DanielNoord@users.noreply.github.com> --- doc/data/messages/b/bad-indentation/bad.py | 2 ++ doc/data/messages/b/bad-indentation/details.rst | 1 + doc/data/messages/b/bad-indentation/good.py | 2 ++ 3 files changed, 5 insertions(+) create mode 100644 doc/data/messages/b/bad-indentation/bad.py create mode 100644 doc/data/messages/b/bad-indentation/details.rst create mode 100644 doc/data/messages/b/bad-indentation/good.py diff --git a/doc/data/messages/b/bad-indentation/bad.py b/doc/data/messages/b/bad-indentation/bad.py new file mode 100644 index 00000000000..dd5a8d90de7 --- /dev/null +++ b/doc/data/messages/b/bad-indentation/bad.py @@ -0,0 +1,2 @@ +if input(): + print('yes') # [bad-indentation] diff --git a/doc/data/messages/b/bad-indentation/details.rst b/doc/data/messages/b/bad-indentation/details.rst new file mode 100644 index 00000000000..1403e993363 --- /dev/null +++ b/doc/data/messages/b/bad-indentation/details.rst @@ -0,0 +1 @@ +The option ``--indent-string`` can be used to set the indentation unit for this check. diff --git a/doc/data/messages/b/bad-indentation/good.py b/doc/data/messages/b/bad-indentation/good.py new file mode 100644 index 00000000000..1a1f33c1171 --- /dev/null +++ b/doc/data/messages/b/bad-indentation/good.py @@ -0,0 +1,2 @@ +if input(): + print('yes') From 215d00dbb77710541993ae2de15b4cdedeb7c407 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Sun, 27 Mar 2022 14:46:18 -0400 Subject: [PATCH 32/45] Fix crash on unbalanced tuple unpacking --- ChangeLog | 3 +++ pylint/checkers/base/name_checker/checker.py | 2 ++ tests/functional/u/unbalanced_tuple_unpacking.py | 3 +++ tests/functional/u/unbalanced_tuple_unpacking.txt | 1 + 4 files changed, 9 insertions(+) diff --git a/ChangeLog b/ChangeLog index 1cd05d078a6..6b57b6ae67f 100644 --- a/ChangeLog +++ b/ChangeLog @@ -36,6 +36,9 @@ What's New in Pylint 2.13.3? ============================ Release date: TBA +* Fix crash involving unbalanced tuple unpacking. + + Closes #5998 What's New in Pylint 2.13.2? diff --git a/pylint/checkers/base/name_checker/checker.py b/pylint/checkers/base/name_checker/checker.py index 68adadc3c65..9527ae13051 100644 --- a/pylint/checkers/base/name_checker/checker.py +++ b/pylint/checkers/base/name_checker/checker.py @@ -386,6 +386,8 @@ def visit_assignname(self, node: nodes.AssignName) -> None: elif ( isinstance(node.parent, nodes.Tuple) and isinstance(assign_type.value, nodes.Tuple) + # protect against unbalanced tuple unpacking + and node.parent.elts.index(node) < len(assign_type.value.elts) and self._assigns_typevar( assign_type.value.elts[node.parent.elts.index(node)] ) diff --git a/tests/functional/u/unbalanced_tuple_unpacking.py b/tests/functional/u/unbalanced_tuple_unpacking.py index d3816f9a9b4..a4b72f8a808 100644 --- a/tests/functional/u/unbalanced_tuple_unpacking.py +++ b/tests/functional/u/unbalanced_tuple_unpacking.py @@ -157,3 +157,6 @@ def my_function(mystring): a, b = my_function("12") # [unbalanced-tuple-unpacking] c = my_function("12") d, *_ = my_function("12") + +# https://github.com/PyCQA/pylint/issues/5998 +x, y, z = (1, 2) # [unbalanced-tuple-unpacking] diff --git a/tests/functional/u/unbalanced_tuple_unpacking.txt b/tests/functional/u/unbalanced_tuple_unpacking.txt index 6298dd5315e..e3206984778 100644 --- a/tests/functional/u/unbalanced_tuple_unpacking.txt +++ b/tests/functional/u/unbalanced_tuple_unpacking.txt @@ -6,3 +6,4 @@ unbalanced-tuple-unpacking:96:8:96:33:UnbalancedUnpacking.test:"Possible unbalan unbalanced-tuple-unpacking:140:8:140:43:MyClass.sum_unpack_3_into_4:"Possible unbalanced tuple unpacking with sequence defined at line 128: left side has 4 label(s), right side has 3 value(s)":UNDEFINED unbalanced-tuple-unpacking:145:8:145:28:MyClass.sum_unpack_3_into_2:"Possible unbalanced tuple unpacking with sequence defined at line 128: left side has 2 label(s), right side has 3 value(s)":UNDEFINED unbalanced-tuple-unpacking:157:0:157:24::"Possible unbalanced tuple unpacking with sequence defined at line 151: left side has 2 label(s), right side has 0 value(s)":UNDEFINED +unbalanced-tuple-unpacking:162:0:162:16::"Possible unbalanced tuple unpacking with sequence (1, 2): left side has 3 label(s), right side has 2 value(s)":UNDEFINED From fa0a1ae3ed78c8d2d6ad72db3debdf82b5b1a7c3 Mon Sep 17 00:00:00 2001 From: Matus Valo Date: Sun, 27 Mar 2022 15:06:05 +0200 Subject: [PATCH 33/45] Added example for bad-except-order Co-authored-by: Vladyslav Krylasov --- doc/data/messages/b/bad-except-order/bad.py | 8 ++++++++ doc/data/messages/b/bad-except-order/good.py | 6 ++++++ 2 files changed, 14 insertions(+) create mode 100644 doc/data/messages/b/bad-except-order/bad.py create mode 100644 doc/data/messages/b/bad-except-order/good.py diff --git a/doc/data/messages/b/bad-except-order/bad.py b/doc/data/messages/b/bad-except-order/bad.py new file mode 100644 index 00000000000..482b5154ddb --- /dev/null +++ b/doc/data/messages/b/bad-except-order/bad.py @@ -0,0 +1,8 @@ +try: + print(int(input())) +except Exception: + raise +except TypeError: # [bad-except-order] + # This block cannot be reached since TypeError exception + # is caught by previous exception handler. + raise diff --git a/doc/data/messages/b/bad-except-order/good.py b/doc/data/messages/b/bad-except-order/good.py new file mode 100644 index 00000000000..e9cd318371c --- /dev/null +++ b/doc/data/messages/b/bad-except-order/good.py @@ -0,0 +1,6 @@ +try: + print(int(input())) +except TypeError: + raise +except Exception: + raise From b5f15ce31a8ba4ed719f811f8cba6deba101464f Mon Sep 17 00:00:00 2001 From: Matus Valo Date: Mon, 28 Mar 2022 10:27:04 +0200 Subject: [PATCH 34/45] Added assignment-from-no-return message example (#5992) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Vladyslav Krylasov Co-authored-by: Daniël van Noord <13665637+DanielNoord@users.noreply.github.com> --- doc/data/messages/a/assignment-from-no-return/bad.py | 5 +++++ doc/data/messages/a/assignment-from-no-return/good.py | 5 +++++ 2 files changed, 10 insertions(+) create mode 100644 doc/data/messages/a/assignment-from-no-return/bad.py create mode 100644 doc/data/messages/a/assignment-from-no-return/good.py diff --git a/doc/data/messages/a/assignment-from-no-return/bad.py b/doc/data/messages/a/assignment-from-no-return/bad.py new file mode 100644 index 00000000000..ea3822a7951 --- /dev/null +++ b/doc/data/messages/a/assignment-from-no-return/bad.py @@ -0,0 +1,5 @@ +def add(x, y): + print(x + y) + + +value = add(10, 10) # [assignment-from-no-return] diff --git a/doc/data/messages/a/assignment-from-no-return/good.py b/doc/data/messages/a/assignment-from-no-return/good.py new file mode 100644 index 00000000000..c019007e17e --- /dev/null +++ b/doc/data/messages/a/assignment-from-no-return/good.py @@ -0,0 +1,5 @@ +def add(x, y): + return x + y + + +value = add(10, 10) From 576b3eb6395ec6746cdae7b37f34fd871ef70ef1 Mon Sep 17 00:00:00 2001 From: Matus Valo Date: Sun, 27 Mar 2022 00:31:17 +0100 Subject: [PATCH 35/45] Added example for assignment-from-none Co-authored-by: Vladyslav Krylasov --- doc/data/messages/a/assignment-from-none/bad.py | 5 +++++ doc/data/messages/a/assignment-from-none/good.py | 5 +++++ 2 files changed, 10 insertions(+) create mode 100644 doc/data/messages/a/assignment-from-none/bad.py create mode 100644 doc/data/messages/a/assignment-from-none/good.py diff --git a/doc/data/messages/a/assignment-from-none/bad.py b/doc/data/messages/a/assignment-from-none/bad.py new file mode 100644 index 00000000000..033bd8974e2 --- /dev/null +++ b/doc/data/messages/a/assignment-from-none/bad.py @@ -0,0 +1,5 @@ +def function(): + return None + + +f = function() # [assignment-from-none] diff --git a/doc/data/messages/a/assignment-from-none/good.py b/doc/data/messages/a/assignment-from-none/good.py new file mode 100644 index 00000000000..d2f4cbc5865 --- /dev/null +++ b/doc/data/messages/a/assignment-from-none/good.py @@ -0,0 +1,5 @@ +def function(): + return None + + +f = function() if function() else 1 From 209030d9330d5ab2b88ae3fe023fb029c200a33e Mon Sep 17 00:00:00 2001 From: Joe Young <80432516+jpy-git@users.noreply.github.com> Date: Mon, 28 Mar 2022 10:27:32 +0100 Subject: [PATCH 36/45] `unnecessary-ellipsis` false positive: allow ellipsis as default argument (#6007) --- ChangeLog | 4 ++++ doc/whatsnew/2.13.rst | 4 ++++ pylint/checkers/ellipsis_checker.py | 5 ++++- tests/functional/u/unnecessary/unnecessary_ellipsis.py | 6 +++++- 4 files changed, 17 insertions(+), 2 deletions(-) diff --git a/ChangeLog b/ChangeLog index 6b57b6ae67f..d4c184705a8 100644 --- a/ChangeLog +++ b/ChangeLog @@ -36,6 +36,10 @@ What's New in Pylint 2.13.3? ============================ Release date: TBA +* Fix false positive for ``unnecessary-ellipsis`` when using an ellipsis as a default argument. + + Closes #5973 + * Fix crash involving unbalanced tuple unpacking. Closes #5998 diff --git a/doc/whatsnew/2.13.rst b/doc/whatsnew/2.13.rst index 2d425d7a4a1..b2c40e87e03 100644 --- a/doc/whatsnew/2.13.rst +++ b/doc/whatsnew/2.13.rst @@ -128,6 +128,10 @@ Extensions Other Changes ============= +* Fix false positive for ``unnecessary-ellipsis`` when using an ellipsis as a default argument. + + Closes #5973 + * Add missing dunder methods to ``unexpected-special-method-signature`` check. * No longer emit ``no-member`` in for loops that reference ``self`` if the binary operation that diff --git a/pylint/checkers/ellipsis_checker.py b/pylint/checkers/ellipsis_checker.py index 2d904a28d25..ac5e04095e3 100644 --- a/pylint/checkers/ellipsis_checker.py +++ b/pylint/checkers/ellipsis_checker.py @@ -41,7 +41,10 @@ def visit_const(self, node: nodes.Const) -> None: """ if ( node.pytype() == "builtins.Ellipsis" - and not isinstance(node.parent, (nodes.Assign, nodes.AnnAssign, nodes.Call)) + and not isinstance( + node.parent, + (nodes.Assign, nodes.AnnAssign, nodes.Call, nodes.Arguments), + ) and ( len(node.parent.parent.child_sequence(node.parent)) > 1 or ( diff --git a/tests/functional/u/unnecessary/unnecessary_ellipsis.py b/tests/functional/u/unnecessary/unnecessary_ellipsis.py index b5a61e34934..1a6ed777c21 100644 --- a/tests/functional/u/unnecessary/unnecessary_ellipsis.py +++ b/tests/functional/u/unnecessary/unnecessary_ellipsis.py @@ -1,6 +1,6 @@ """Emit a warning when the ellipsis constant is used and can be avoided""" -# pylint: disable=missing-docstring, too-few-public-methods +# pylint: disable=missing-docstring, too-few-public-methods, invalid-name, unused-argument from typing import List, overload, Union @@ -97,3 +97,7 @@ def __getitem__(self, index: Union[int, slice]) -> Union[int, List[int]]: ... else: raise TypeError(...) + +# Ellipsis is allowed as a default argument +def func_with_ellipsis_default_arg(a = ...) -> None: + "Some docstring." From 250ff9dd739283867c5c0b2e0d8a5209c03b1726 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20Noord?= <13665637+DanielNoord@users.noreply.github.com> Date: Mon, 28 Mar 2022 12:52:38 +0200 Subject: [PATCH 37/45] Split checks and tests in CI (#6010) --- .github/workflows/checks.yaml | 184 +++++++++++++++++++++ .github/workflows/{ci.yaml => tests.yaml} | 191 ++-------------------- 2 files changed, 197 insertions(+), 178 deletions(-) create mode 100644 .github/workflows/checks.yaml rename .github/workflows/{ci.yaml => tests.yaml} (64%) diff --git a/.github/workflows/checks.yaml b/.github/workflows/checks.yaml new file mode 100644 index 00000000000..32e05a8e906 --- /dev/null +++ b/.github/workflows/checks.yaml @@ -0,0 +1,184 @@ +name: Checks + +on: + push: + branches: + - main + - 2.* + pull_request: ~ + +env: + # Also change CACHE_VERSION in the other workflows + CACHE_VERSION: 5 + DEFAULT_PYTHON: 3.8 + PRE_COMMIT_CACHE: ~/.cache/pre-commit + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + prepare-base: + name: Prepare base dependencies + runs-on: ubuntu-latest + timeout-minutes: 10 + outputs: + python-key: ${{ steps.generate-python-key.outputs.key }} + pre-commit-key: ${{ steps.generate-pre-commit-key.outputs.key }} + steps: + - name: Check out code from GitHub + uses: actions/checkout@v3.0.0 + with: + fetch-depth: 0 + - name: Set up Python ${{ env.DEFAULT_PYTHON }} + id: python + uses: actions/setup-python@v3.0.0 + with: + python-version: ${{ env.DEFAULT_PYTHON }} + - name: Generate partial Python venv restore key + id: generate-python-key + run: >- + echo "::set-output name=key::base-venv-${{ env.CACHE_VERSION }}-${{ + hashFiles('setup.cfg', 'requirements_test.txt', 'requirements_test_min.txt') + }}" + - name: Restore Python virtual environment + id: cache-venv + uses: actions/cache@v3.0.0 + with: + path: venv + key: >- + ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + steps.generate-python-key.outputs.key }} + restore-keys: | + ${{ runner.os }}-${{ steps.python.outputs.python-version }}-base-venv-${{ env.CACHE_VERSION }}- + - name: Create Python virtual environment + if: steps.cache-venv.outputs.cache-hit != 'true' + run: | + python -m venv venv + . venv/bin/activate + python -m pip install -U pip setuptools wheel + pip install -U -r requirements_test.txt + - name: Generate pre-commit restore key + id: generate-pre-commit-key + run: >- + echo "::set-output name=key::pre-commit-${{ env.CACHE_VERSION }}-${{ + hashFiles('.pre-commit-config.yaml') }}" + - name: Restore pre-commit environment + id: cache-precommit + uses: actions/cache@v3.0.0 + with: + path: ${{ env.PRE_COMMIT_CACHE }} + key: >- + ${{ runner.os }}-${{ steps.generate-pre-commit-key.outputs.key }} + restore-keys: | + ${{ runner.os }}-pre-commit-${{ env.CACHE_VERSION }}- + - name: Install pre-commit dependencies + if: steps.cache-precommit.outputs.cache-hit != 'true' + run: | + . venv/bin/activate + pre-commit install --install-hooks + + pylint: + name: pylint + runs-on: ubuntu-latest + timeout-minutes: 10 + needs: prepare-base + steps: + - name: Check out code from GitHub + uses: actions/checkout@v3.0.0 + - name: Set up Python ${{ env.DEFAULT_PYTHON }} + id: python + uses: actions/setup-python@v3.0.0 + with: + python-version: ${{ env.DEFAULT_PYTHON }} + - name: Restore Python virtual environment + id: cache-venv + uses: actions/cache@v3.0.0 + with: + path: venv + key: + ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + needs.prepare-base.outputs.python-key }} + - name: Fail job if Python cache restore failed + if: steps.cache-venv.outputs.cache-hit != 'true' + run: | + echo "Failed to restore Python venv from cache" + exit 1 + - name: Restore pre-commit environment + id: cache-precommit + uses: actions/cache@v3.0.0 + with: + path: ${{ env.PRE_COMMIT_CACHE }} + key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }} + - name: Fail job if pre-commit cache restore failed + if: steps.cache-precommit.outputs.cache-hit != 'true' + run: | + echo "Failed to restore pre-commit environment from cache" + exit 1 + - name: Run pylint checks + run: | + . venv/bin/activate + pip install -e . + pre-commit run pylint --all-files + + spelling: + name: spelling + runs-on: ubuntu-latest + timeout-minutes: 5 + needs: prepare-base + steps: + - name: Check out code from GitHub + uses: actions/checkout@v3.0.0 + - name: Set up Python ${{ env.DEFAULT_PYTHON }} + id: python + uses: actions/setup-python@v3.0.0 + with: + python-version: ${{ env.DEFAULT_PYTHON }} + - name: Restore Python virtual environment + id: cache-venv + uses: actions/cache@v3.0.0 + with: + path: venv + key: + ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + needs.prepare-base.outputs.python-key }} + - name: Fail job if Python cache restore failed + if: steps.cache-venv.outputs.cache-hit != 'true' + run: | + echo "Failed to restore Python venv from cache" + exit 1 + - name: Run spelling checks + run: | + . venv/bin/activate + pytest tests/ -k unittest_spelling + + documentation: + name: documentation + runs-on: ubuntu-latest + timeout-minutes: 5 + needs: prepare-base + steps: + - name: Check out code from GitHub + uses: actions/checkout@v3.0.0 + - name: Set up Python ${{ env.DEFAULT_PYTHON }} + id: python + uses: actions/setup-python@v3.0.0 + with: + python-version: ${{ env.DEFAULT_PYTHON }} + - name: Restore Python virtual environment + id: cache-venv + uses: actions/cache@v3.0.0 + with: + path: venv + key: + ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + needs.prepare-base.outputs.python-key }} + - name: Fail job if Python cache restore failed + if: steps.cache-venv.outputs.cache-hit != 'true' + run: | + echo "Failed to restore Python venv from cache" + exit 1 + - name: Run checks on documentation code examples + run: | + . venv/bin/activate + pytest doc/test_messages_documentation.py diff --git a/.github/workflows/ci.yaml b/.github/workflows/tests.yaml similarity index 64% rename from .github/workflows/ci.yaml rename to .github/workflows/tests.yaml index 8432c93fbea..4a022659518 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/tests.yaml @@ -1,186 +1,21 @@ -name: CI +name: Tests on: push: branches: - main - 2.* - pull_request: ~ + pull_request: + paths-ignore: + - doc/data/messages/** env: - # Also change CACHE_VERSION in the primer workflow + # Also change CACHE_VERSION in the other workflows CACHE_VERSION: 5 - DEFAULT_PYTHON: 3.8 - PRE_COMMIT_CACHE: ~/.cache/pre-commit jobs: - prepare-base: - name: Prepare base dependencies - runs-on: ubuntu-latest - timeout-minutes: 10 - outputs: - python-key: ${{ steps.generate-python-key.outputs.key }} - pre-commit-key: ${{ steps.generate-pre-commit-key.outputs.key }} - steps: - - name: Check out code from GitHub - uses: actions/checkout@v3.0.0 - with: - fetch-depth: 0 - - name: Set up Python ${{ env.DEFAULT_PYTHON }} - id: python - uses: actions/setup-python@v3.0.0 - with: - python-version: ${{ env.DEFAULT_PYTHON }} - - name: Generate partial Python venv restore key - id: generate-python-key - run: >- - echo "::set-output name=key::base-venv-${{ env.CACHE_VERSION }}-${{ - hashFiles('setup.cfg', 'requirements_test.txt', 'requirements_test_min.txt') - }}" - - name: Restore Python virtual environment - id: cache-venv - uses: actions/cache@v3.0.0 - with: - path: venv - key: >- - ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ - steps.generate-python-key.outputs.key }} - restore-keys: | - ${{ runner.os }}-${{ steps.python.outputs.python-version }}-base-venv-${{ env.CACHE_VERSION }}- - - name: Create Python virtual environment - if: steps.cache-venv.outputs.cache-hit != 'true' - run: | - python -m venv venv - . venv/bin/activate - python -m pip install -U pip setuptools wheel - pip install -U -r requirements_test.txt - - name: Generate pre-commit restore key - id: generate-pre-commit-key - run: >- - echo "::set-output name=key::pre-commit-${{ env.CACHE_VERSION }}-${{ - hashFiles('.pre-commit-config.yaml') }}" - - name: Restore pre-commit environment - id: cache-precommit - uses: actions/cache@v3.0.0 - with: - path: ${{ env.PRE_COMMIT_CACHE }} - key: >- - ${{ runner.os }}-${{ steps.generate-pre-commit-key.outputs.key }} - restore-keys: | - ${{ runner.os }}-pre-commit-${{ env.CACHE_VERSION }}- - - name: Install pre-commit dependencies - if: steps.cache-precommit.outputs.cache-hit != 'true' - run: | - . venv/bin/activate - pre-commit install --install-hooks - - formatting: - name: checks / pre-commit - runs-on: ubuntu-latest - timeout-minutes: 10 - needs: prepare-base - steps: - - name: Check out code from GitHub - uses: actions/checkout@v3.0.0 - - name: Set up Python ${{ env.DEFAULT_PYTHON }} - id: python - uses: actions/setup-python@v3.0.0 - with: - python-version: ${{ env.DEFAULT_PYTHON }} - - name: Restore Python virtual environment - id: cache-venv - uses: actions/cache@v3.0.0 - with: - path: venv - key: - ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ - needs.prepare-base.outputs.python-key }} - - name: Fail job if Python cache restore failed - if: steps.cache-venv.outputs.cache-hit != 'true' - run: | - echo "Failed to restore Python venv from cache" - exit 1 - - name: Restore pre-commit environment - id: cache-precommit - uses: actions/cache@v3.0.0 - with: - path: ${{ env.PRE_COMMIT_CACHE }} - key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }} - - name: Fail job if pre-commit cache restore failed - if: steps.cache-precommit.outputs.cache-hit != 'true' - run: | - echo "Failed to restore pre-commit environment from cache" - exit 1 - - name: Run formatting check - run: | - . venv/bin/activate - pip install -e . - pre-commit run pylint --all-files - - spelling: - name: checks / spelling - runs-on: ubuntu-latest - timeout-minutes: 5 - needs: prepare-base - steps: - - name: Check out code from GitHub - uses: actions/checkout@v3.0.0 - - name: Set up Python ${{ env.DEFAULT_PYTHON }} - id: python - uses: actions/setup-python@v3.0.0 - with: - python-version: ${{ env.DEFAULT_PYTHON }} - - name: Restore Python virtual environment - id: cache-venv - uses: actions/cache@v3.0.0 - with: - path: venv - key: - ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ - needs.prepare-base.outputs.python-key }} - - name: Fail job if Python cache restore failed - if: steps.cache-venv.outputs.cache-hit != 'true' - run: | - echo "Failed to restore Python venv from cache" - exit 1 - - name: Run spelling checks - run: | - . venv/bin/activate - pytest tests/ -k unittest_spelling - - documentation: - name: checks / documentation - runs-on: ubuntu-latest - timeout-minutes: 5 - needs: prepare-base - steps: - - name: Check out code from GitHub - uses: actions/checkout@v3.0.0 - - name: Set up Python ${{ env.DEFAULT_PYTHON }} - id: python - uses: actions/setup-python@v3.0.0 - with: - python-version: ${{ env.DEFAULT_PYTHON }} - - name: Restore Python virtual environment - id: cache-venv - uses: actions/cache@v3.0.0 - with: - path: venv - key: - ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ - needs.prepare-base.outputs.python-key }} - - name: Fail job if Python cache restore failed - if: steps.cache-venv.outputs.cache-hit != 'true' - run: | - echo "Failed to restore Python venv from cache" - exit 1 - - name: Run checks on documentation code examples - run: | - . venv/bin/activate - pytest doc/test_messages_documentation.py - prepare-tests-linux: - name: tests / prepare / ${{ matrix.python-version }} / Linux + name: prepare / ${{ matrix.python-version }} / Linux runs-on: ubuntu-latest timeout-minutes: 10 strategy: @@ -223,7 +58,7 @@ jobs: pip install -U -r requirements_test.txt pytest-linux: - name: tests / run / ${{ matrix.python-version }} / Linux + name: run / ${{ matrix.python-version }} / Linux runs-on: ubuntu-latest timeout-minutes: 15 needs: prepare-tests-linux @@ -263,7 +98,7 @@ jobs: path: .coverage coverage: - name: tests / process / coverage + name: process / coverage runs-on: ubuntu-latest timeout-minutes: 5 needs: ["prepare-tests-linux", "pytest-linux"] @@ -308,7 +143,7 @@ jobs: coveralls --rcfile=${{ env.COVERAGERC_FILE }} --service=github benchmark-linux: - name: tests / run benchmark / ${{ matrix.python-version }} / Linux + name: run benchmark / ${{ matrix.python-version }} / Linux runs-on: ubuntu-latest timeout-minutes: 10 needs: ["prepare-tests-linux", "pytest-linux"] @@ -360,7 +195,7 @@ jobs: path: .benchmarks/ prepare-tests-windows: - name: tests / prepare / ${{ matrix.python-version }} / Windows + name: prepare / ${{ matrix.python-version }} / Windows runs-on: windows-latest timeout-minutes: 10 needs: pytest-linux @@ -404,7 +239,7 @@ jobs: pip install -U -r requirements_test_min.txt pytest-windows: - name: tests / run / ${{ matrix.python-version }} / Windows + name: run / ${{ matrix.python-version }} / Windows runs-on: windows-latest timeout-minutes: 15 needs: prepare-tests-windows @@ -443,7 +278,7 @@ jobs: pytest -vv --durations=20 --benchmark-disable tests/ prepare-tests-pypy: - name: tests / prepare / ${{ matrix.python-version }} / Linux + name: prepare / ${{ matrix.python-version }} / Linux runs-on: ubuntu-latest timeout-minutes: 10 strategy: @@ -486,7 +321,7 @@ jobs: pip install -U -r requirements_test_min.txt pytest-pypy: - name: tests / run / ${{ matrix.python-version }} / Linux + name: run / ${{ matrix.python-version }} / Linux runs-on: ubuntu-latest timeout-minutes: 15 needs: prepare-tests-pypy From 0bbc01c0221b3e6ce4ecb746acd5a86720e07c26 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Mar 2022 17:35:10 +0200 Subject: [PATCH 38/45] Bump mypy from 0.941 to 0.942 (#6013) Bumps [mypy](https://github.com/python/mypy) from 0.941 to 0.942. - [Release notes](https://github.com/python/mypy/releases) - [Commits](https://github.com/python/mypy/compare/v0.941...v0.942) --- updated-dependencies: - dependency-name: mypy dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements_test_pre_commit.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index 0f5bf008f8b..1f2c030fb2b 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -4,4 +4,4 @@ black==22.1.0 flake8==4.0.1 flake8-typing-imports==1.12.0 isort==5.10.1 -mypy==0.941 +mypy==0.942 From d9d45ba09ea47d6af4dc4a24448ac47eef081ede Mon Sep 17 00:00:00 2001 From: Andreas Finkler <3929834+DudeNr33@users.noreply.github.com> Date: Mon, 28 Mar 2022 18:26:24 +0200 Subject: [PATCH 39/45] Add examples for ``await-outside-async`` and fix activation of message (#6016) --- ChangeLog | 1 + .../messages/a/await-outside-async/bad.py | 5 ++++ .../messages/a/await-outside-async/good.py | 5 ++++ .../a/await-outside-async/related.rst | 1 + pylint/checkers/typecheck.py | 28 +++++++++---------- 5 files changed, 26 insertions(+), 14 deletions(-) create mode 100644 doc/data/messages/a/await-outside-async/bad.py create mode 100644 doc/data/messages/a/await-outside-async/good.py create mode 100644 doc/data/messages/a/await-outside-async/related.rst diff --git a/ChangeLog b/ChangeLog index d4c184705a8..e7a9cb06e1a 100644 --- a/ChangeLog +++ b/ChangeLog @@ -26,6 +26,7 @@ Release date: TBA Closes #1555 +* Fix bug where specifically enabling just ``await-outside-async`` was not possible. .. Insert your changelog randomly, it will reduce merge conflicts diff --git a/doc/data/messages/a/await-outside-async/bad.py b/doc/data/messages/a/await-outside-async/bad.py new file mode 100644 index 00000000000..35fd8c3439e --- /dev/null +++ b/doc/data/messages/a/await-outside-async/bad.py @@ -0,0 +1,5 @@ +import asyncio + + +def main(): + await asyncio.sleep(1) # [await-outside-async] diff --git a/doc/data/messages/a/await-outside-async/good.py b/doc/data/messages/a/await-outside-async/good.py new file mode 100644 index 00000000000..231794be5ea --- /dev/null +++ b/doc/data/messages/a/await-outside-async/good.py @@ -0,0 +1,5 @@ +import asyncio + + +async def main(): + await asyncio.sleep(1) diff --git a/doc/data/messages/a/await-outside-async/related.rst b/doc/data/messages/a/await-outside-async/related.rst new file mode 100644 index 00000000000..6cc66c68639 --- /dev/null +++ b/doc/data/messages/a/await-outside-async/related.rst @@ -0,0 +1 @@ +- `PEP 492 `_ diff --git a/pylint/checkers/typecheck.py b/pylint/checkers/typecheck.py index cd5b210a212..5c3196a3999 100644 --- a/pylint/checkers/typecheck.py +++ b/pylint/checkers/typecheck.py @@ -1953,6 +1953,20 @@ def visit_for(self, node: nodes.For) -> None: self.add_message("dict-iter-missing-items", node=node) + @check_messages("await-outside-async") + def visit_await(self, node: nodes.Await) -> None: + self._check_await_outside_coroutine(node) + + def _check_await_outside_coroutine(self, node: nodes.Await) -> None: + node_scope = node.scope() + while not isinstance(node_scope, nodes.Module): + if isinstance(node_scope, nodes.AsyncFunctionDef): + return + if isinstance(node_scope, nodes.FunctionDef): + break + node_scope = node_scope.parent.scope() + self.add_message("await-outside-async", node=node) + class IterableChecker(BaseChecker): """Checks for non-iterables used in an iterable context. @@ -2064,20 +2078,6 @@ def visit_generatorexp(self, node: nodes.GeneratorExp) -> None: for gen in node.generators: self._check_iterable(gen.iter, check_async=gen.is_async) - @check_messages("await-outside-async") - def visit_await(self, node: nodes.Await) -> None: - self._check_await_outside_coroutine(node) - - def _check_await_outside_coroutine(self, node: nodes.Await) -> None: - node_scope = node.scope() - while not isinstance(node_scope, nodes.Module): - if isinstance(node_scope, nodes.AsyncFunctionDef): - return - if isinstance(node_scope, nodes.FunctionDef): - break - node_scope = node_scope.parent.scope() - self.add_message("await-outside-async", node=node) - def register(linter: "PyLinter") -> None: linter.register_checker(TypeChecker(linter)) From 42376b8b70502f81c407a474d30f9b1c97b52448 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Mar 2022 20:43:03 +0200 Subject: [PATCH 40/45] Bump sphinx from 4.4.0 to 4.5.0 (#6014) Bumps [sphinx](https://github.com/sphinx-doc/sphinx) from 4.4.0 to 4.5.0. - [Release notes](https://github.com/sphinx-doc/sphinx/releases) - [Changelog](https://github.com/sphinx-doc/sphinx/blob/4.x/CHANGES) - [Commits](https://github.com/sphinx-doc/sphinx/compare/v4.4.0...v4.5.0) --- updated-dependencies: - dependency-name: sphinx dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- doc/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/requirements.txt b/doc/requirements.txt index acaea1e8532..4528f5da4bc 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -1,3 +1,3 @@ -Sphinx==4.4.0 +Sphinx==4.5.0 python-docs-theme==2022.1 -e . From fff636cdec49c1f9d595b29ebbd585d58f2b686a Mon Sep 17 00:00:00 2001 From: Matus Valo Date: Mon, 28 Mar 2022 22:15:44 +0200 Subject: [PATCH 41/45] Added example of bare-except (#6020) Co-authored-by: Vladyslav Krylasov --- doc/data/messages/b/bare-except/bad.py | 4 ++++ doc/data/messages/b/bare-except/good.py | 4 ++++ 2 files changed, 8 insertions(+) create mode 100644 doc/data/messages/b/bare-except/bad.py create mode 100644 doc/data/messages/b/bare-except/good.py diff --git a/doc/data/messages/b/bare-except/bad.py b/doc/data/messages/b/bare-except/bad.py new file mode 100644 index 00000000000..fc88c6fd3ec --- /dev/null +++ b/doc/data/messages/b/bare-except/bad.py @@ -0,0 +1,4 @@ +try: + 1 / 0 +except: # [bare-except] + pass diff --git a/doc/data/messages/b/bare-except/good.py b/doc/data/messages/b/bare-except/good.py new file mode 100644 index 00000000000..b02b365b065 --- /dev/null +++ b/doc/data/messages/b/bare-except/good.py @@ -0,0 +1,4 @@ +try: + 1 / 0 +except ZeroDivisionError: + pass From 78334cb32f553e6487a7a9021bb4090c2da61b8d Mon Sep 17 00:00:00 2001 From: Matus Valo Date: Mon, 28 Mar 2022 22:16:02 +0200 Subject: [PATCH 42/45] Added example of bad-reversed-sequence (#6021) Co-authored-by: Vladyslav Krylasov --- doc/data/messages/b/bad-reversed-sequence/bad.py | 1 + doc/data/messages/b/bad-reversed-sequence/good.py | 1 + 2 files changed, 2 insertions(+) create mode 100644 doc/data/messages/b/bad-reversed-sequence/bad.py create mode 100644 doc/data/messages/b/bad-reversed-sequence/good.py diff --git a/doc/data/messages/b/bad-reversed-sequence/bad.py b/doc/data/messages/b/bad-reversed-sequence/bad.py new file mode 100644 index 00000000000..19c5d1a8a52 --- /dev/null +++ b/doc/data/messages/b/bad-reversed-sequence/bad.py @@ -0,0 +1 @@ +reversed({1, 2, 3, 4}) # [bad-reversed-sequence] diff --git a/doc/data/messages/b/bad-reversed-sequence/good.py b/doc/data/messages/b/bad-reversed-sequence/good.py new file mode 100644 index 00000000000..d39621893b0 --- /dev/null +++ b/doc/data/messages/b/bad-reversed-sequence/good.py @@ -0,0 +1 @@ +reversed([1, 2, 3, 4]) From edf753e6864224ac0144c0ac12c8d89baa93e097 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 29 Mar 2022 00:27:43 +0200 Subject: [PATCH 43/45] [pre-commit.ci] pre-commit autoupdate (#6023) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [pre-commit.ci] pre-commit autoupdate updates: - [github.com/Pierre-Sassoulas/copyright_notice_precommit: f683ab7d10d5f7e779c75aea17197007583d14e4 → 0.1.2](https://github.com/Pierre-Sassoulas/copyright_notice_precommit/compare/f683ab7d10d5f7e779c75aea17197007583d14e4...0.1.2) - [github.com/psf/black: 22.1.0 → 22.3.0](https://github.com/psf/black/compare/22.1.0...22.3.0) - [github.com/pre-commit/mirrors-mypy: v0.941 → v0.942](https://github.com/pre-commit/mirrors-mypy/compare/v0.941...v0.942) - [github.com/pre-commit/mirrors-prettier: v2.6.0 → v2.6.1](https://github.com/pre-commit/mirrors-prettier/compare/v2.6.0...v2.6.1) * Update .pre-commit-config.yaml Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Pierre Sassoulas --- .pre-commit-config.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c8a6c58f615..51c5b331f30 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,7 +21,7 @@ repos: - --remove-duplicate-keys - --remove-unused-variables - repo: https://github.com/Pierre-Sassoulas/copyright_notice_precommit - rev: f683ab7d10d5f7e779c75aea17197007583d14e4 + rev: 0.1.2 hooks: - id: copyright-notice args: ["--notice=script/copyright.txt", "--enforce-all"] @@ -38,7 +38,7 @@ repos: hooks: - id: isort - repo: https://github.com/psf/black - rev: 22.1.0 + rev: 22.3.0 hooks: - id: black args: [--safe, --quiet] @@ -76,7 +76,7 @@ repos: types: [text] # necessary to include ChangeLog file files: ^(ChangeLog|doc/(.*/)*.*\.rst) - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.941 + rev: v0.942 hooks: - id: mypy name: mypy @@ -88,7 +88,7 @@ repos: additional_dependencies: ["platformdirs==2.2.0", "types-pkg_resources==0.1.3"] exclude: tests/functional/|tests/input|tests(/.*)+/conftest.py|doc/data/messages|tests(/\w*)*data/ - repo: https://github.com/pre-commit/mirrors-prettier - rev: v2.6.0 + rev: v2.6.1 hooks: - id: prettier args: [--prose-wrap=always, --print-width=88] From f616061efb22bd771f8b4e0dad92534b6c29a061 Mon Sep 17 00:00:00 2001 From: KotlinIsland <65446343+KotlinIsland@users.noreply.github.com> Date: Tue, 29 Mar 2022 15:26:13 +1000 Subject: [PATCH 44/45] Fix workflow badge in readme (#6025) Co-authored-by: KotlinIsland --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 4967a00f9e2..30348b11d2b 100644 --- a/README.rst +++ b/README.rst @@ -2,7 +2,7 @@ README for Pylint - https://pylint.pycqa.org/ ============================================= -.. image:: https://github.com/PyCQA/pylint/actions/workflows/ci.yaml/badge.svg?branch=main +.. image:: https://github.com/PyCQA/pylint/actions/workflows/tests.yaml/badge.svg?branch=main :target: https://github.com/PyCQA/pylint/actions .. image:: https://coveralls.io/repos/github/PyCQA/pylint/badge.svg?branch=main From 44ddbdf495924297d57de2df96700800e66f5605 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Mon, 28 Mar 2022 22:10:58 -0400 Subject: [PATCH 45/45] Document method of clearing inference cache --- doc/user_guide/run.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/doc/user_guide/run.rst b/doc/user_guide/run.rst index e49da5e5178..898a1b732a7 100644 --- a/doc/user_guide/run.rst +++ b/doc/user_guide/run.rst @@ -114,6 +114,14 @@ the stream outputs to a file: This would be useful to capture pylint output in an open stream which can be passed onto another program. +If your program expects that the files being linted might be edited +between runs, you will need to clear pylint's inference cache: + +.. sourcecode:: python + + from pylint.lint import pylinter + pylinter.MANAGER.clear_cache() + Command line options --------------------